carousel.rs revision f7c724da4bb4fcd3cd02add04a7bb8052e07e4c3
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#pragma version(1)
18#pragma rs java_package_name(com.android.ex.carousel);
19#pragma rs set_reflect_license()
20
21#include "rs_graphics.rsh"
22
23typedef struct __attribute__((aligned(4))) Card {
24    // *** Update copyCard if you add/remove fields here.
25    rs_allocation texture; // basic card texture
26    rs_allocation detailTexture; // screen-aligned detail texture
27    float2 detailTextureOffset; // offset to add, in screen coordinates
28    float2 detailLineOffset; // offset to add to detail line, in screen coordinates
29    rs_mesh geometry;
30    rs_matrix4x4 matrix; // custom transform for this card/geometry
31    int textureState;  // whether or not the primary card texture is loaded.
32    int detailTextureState; // whether or not the detail for the card is loaded.
33    int geometryState; // whether or not geometry is loaded
34    int visible; // not bool because of packing bug?
35    // TODO: Change when int64_t is supported.  This will break after ~40 days of uptime.
36    unsigned int textureTimeStamp; // time when this texture was last updated, in seconds
37    unsigned int detailTextureTimeStamp; // time when this texture was last updated, in seconds
38} Card_t;
39
40typedef struct Ray_s {
41    float3 position;
42    float3 direction;
43} Ray;
44
45typedef struct PerspectiveCamera_s {
46    float3 from;
47    float3 at;
48    float3 up;
49    float  fov;
50    float  aspect;
51    float  near;
52    float  far;
53} PerspectiveCamera;
54
55typedef struct FragmentShaderConstants_s {
56    float fadeAmount;
57} FragmentShaderConstants;
58
59// Request states. Used for loading 3D object properties from the Java client.
60// Typical properties: texture, geometry and matrices.
61enum {
62    STATE_INVALID = 0, // item hasn't been loaded
63    STATE_LOADING, // we've requested an item but are waiting for it to load
64    STATE_LOADED // item was delivered
65};
66
67// Client messages *** THIS LIST MUST MATCH THOSE IN CarouselRS.java. ***
68static const int CMD_CARD_SELECTED = 100;
69static const int CMD_REQUEST_TEXTURE = 200;
70static const int CMD_INVALIDATE_TEXTURE = 210;
71static const int CMD_REQUEST_GEOMETRY = 300;
72static const int CMD_INVALIDATE_GEOMETRY = 310;
73static const int CMD_ANIMATION_STARTED = 400;
74static const int CMD_ANIMATION_FINISHED = 500;
75static const int CMD_REQUEST_DETAIL_TEXTURE = 600;
76static const int CMD_INVALIDATE_DETAIL_TEXTURE = 610;
77static const int CMD_REPORT_FIRST_CARD_POSITION = 700;
78static const int CMD_PING = 1000;
79
80// Constants
81static const int ANIMATION_SCALE_TIME = 200; // Time it takes to animate selected card, in ms
82static const float3 SELECTED_SCALE_FACTOR = { 0.2f, 0.2f, 0.2f }; // increase by this %
83
84// Debug flags
85const bool debugCamera = false; // dumps ray/camera coordinate stuff
86const bool debugPicking = false; // renders picking area on top of geometry
87const bool debugTextureLoading = false; // for debugging texture load/unload
88const bool debugGeometryLoading = false; // for debugging geometry load/unload
89const bool debugDetails = false; // for debugging detail texture geometry
90const bool debugRendering = false; // flashes display when the frame changes
91
92// Exported variables. These will be reflected to Java set_* variables.
93Card_t *cards; // array of cards to draw
94// TODO: remove tmpCards code when allocations support resizing
95Card_t *tmpCards; // temporary array used to prevent flashing when we add more cards
96float startAngle; // position of initial card, in radians
97int slotCount; // number of positions where a card can be
98int cardCount; // number of cards in stack
99int visibleSlotCount; // number of visible slots (for culling)
100int visibleDetailCount; // number of visible detail textures to show
101bool drawDetailBelowCard; // whether detail goes above (false) or below (true) the card
102bool drawRuler; // whether to draw a ruler from the card to the detail texture
103float radius; // carousel radius. Cards will be centered on a circle with this radius
104float cardRotation; // rotation of card in XY plane relative to Z=1
105float swaySensitivity; // how much to rotate cards in relation to the rotation velocity
106float frictionCoeff; // how much to slow down the carousel over time
107float dragFactor; // a scale factor for how sensitive the carousel is to user dragging
108int fadeInDuration; // amount of time (in ms) for smoothly switching out textures
109float rezInCardCount; // this controls how rapidly distant card textures will be rez-ed in
110float detailFadeRate; // rate at which details fade as they move into the distance
111rs_program_store programStore;
112rs_program_fragment singleTextureFragmentProgram;
113rs_program_fragment multiTextureFragmentProgram;
114rs_program_vertex vertexProgram;
115rs_program_raster rasterProgram;
116rs_allocation defaultTexture; // shown when no other texture is assigned
117rs_allocation loadingTexture; // progress texture (shown when app is fetching the texture)
118rs_allocation backgroundTexture; // drawn behind everything, if set
119rs_allocation detailLineTexture; // used to draw detail line (as a quad, of course)
120rs_allocation detailLoadingTexture; // used when detail texture is loading
121rs_mesh defaultGeometry; // shown when no geometry is loaded
122rs_mesh loadingGeometry; // shown when geometry is loading
123rs_matrix4x4 projectionMatrix;
124rs_matrix4x4 modelviewMatrix;
125FragmentShaderConstants* shaderConstants;
126rs_sampler linearClamp;
127
128#pragma rs export_var(radius, cards, tmpCards, slotCount, visibleSlotCount, cardRotation, backgroundColor)
129#pragma rs export_var(swaySensitivity, frictionCoeff, dragFactor)
130#pragma rs export_var(visibleDetailCount, drawDetailBelowCard, drawRuler)
131#pragma rs export_var(programStore, vertexProgram, rasterProgram)
132#pragma rs export_var(singleTextureFragmentProgram, multiTextureFragmentProgram)
133#pragma rs export_var(detailLineTexture, detailLoadingTexture, backgroundTexture)
134#pragma rs export_var(linearClamp, shaderConstants)
135#pragma rs export_var(startAngle, defaultTexture, loadingTexture, defaultGeometry, loadingGeometry)
136#pragma rs export_var(fadeInDuration, rezInCardCount)
137#pragma rs export_func(createCards, copyCards, lookAt, doStart, doStop, doMotion, doSelection)
138#pragma rs export_func(setTexture, setGeometry, setDetailTexture, debugCamera, debugPicking)
139#pragma rs export_func(requestFirstCardPosition)
140
141// Local variables
142static float bias; // rotation bias, in radians. Used for animation and dragging.
143static bool updateCamera;    // force a recompute of projection and lookat matrices
144static bool initialized;
145static float4 backgroundColor;
146static const float FLT_MAX = 1.0e37;
147static int currentSelection = -1;
148static int currentFirstCard = -1;
149static int64_t touchTime = -1;  // time of first touch (see doStart())
150static float touchBias = 0.0f; // bias on first touch
151static float velocity = 0.0f;  // angular velocity in radians/s
152
153// Because allocations can't have 0 dimensions, we have to track whether or not
154// cards are valid separately.
155// TODO: Remove this dependency once allocations can have a zero dimension.
156static bool cardAllocationValid = false;
157
158// Default geometry when card.geometry is not set.
159static const float3 cardVertices[4] = {
160        { -1.0, -1.0, 0.0 },
161        { 1.0, -1.0, 0.0 },
162        { 1.0, 1.0, 0.0 },
163        {-1.0, 1.0, 0.0 }
164};
165
166// Default camera
167static PerspectiveCamera camera = {
168        {2,2,2}, // from
169        {0,0,0}, // at
170        {0,1,0}, // up
171        25.0f,   // field of view
172        1.0f,    // aspect
173        0.1f,    // near
174        100.0f   // far
175};
176
177// Forward references
178static int intersectGeometry(Ray* ray, float *bestTime);
179static bool __attribute__((overloadable))
180        makeRayForPixelAt(Ray* ray, PerspectiveCamera* cam, float x, float y);
181static bool __attribute__((overloadable))
182        makeRayForPixelAt(Ray* ray, rs_matrix4x4* model, rs_matrix4x4* proj, float x, float y);
183static float deltaTimeInSeconds(int64_t current);
184
185void init() {
186    // initializers currently have a problem when the variables are exported, so initialize
187    // globals here.
188    // rsDebug("Renderscript: init()", 0);
189    startAngle = 0.0f;
190    slotCount = 10;
191    visibleSlotCount = 1;
192    visibleDetailCount = 3;
193    bias = 0.0f;
194    radius = 1.0f;
195    cardRotation = 0.0f;
196    updateCamera = true;
197    initialized = false;
198    backgroundColor = (float4) { 0.0f, 0.0f, 0.0f, 1.0f };
199    cardAllocationValid = false;
200    cardCount = 0;
201    fadeInDuration = 250;
202    rezInCardCount = 0.0f; // alpha will ramp to 1.0f over this many cards (0.0f means disabled)
203    detailFadeRate = 0.5f; // fade details over this many slot positions.
204}
205
206static void updateAllocationVars(Card_t* newcards)
207{
208    // Cards
209    rs_allocation cardAlloc = rsGetAllocation(newcards);
210    // TODO: use new rsIsObject()
211    cardCount = (cardAllocationValid && cardAlloc.p != 0) ? rsAllocationGetDimX(cardAlloc) : 0;
212}
213
214void createCards(int n)
215{
216    if (debugTextureLoading) rsDebug("CreateCards: ", n);
217    cardAllocationValid = n > 0;
218    initialized = false;
219    updateAllocationVars(cards);
220}
221
222void copyCard(Card_t* dest, Card_t * src)
223{
224    rsSetObject(&dest->texture, src->texture);
225    rsSetObject(&dest->detailTexture, src->detailTexture);
226    dest->detailTextureOffset = src->detailTextureOffset;
227    dest->detailLineOffset = src->detailLineOffset;
228    rsSetObject(&dest->geometry, src->geometry);
229    dest->matrix = src->matrix;
230    dest->textureState = src->textureState;
231    dest->detailTextureState = src->detailTextureState;
232    dest->geometryState = src->geometryState;
233    dest->visible = src->visible;
234    dest->textureTimeStamp = src->textureTimeStamp;
235    dest->detailTextureTimeStamp = src->detailTextureTimeStamp;
236 }
237
238void copyCards()
239{
240    unsigned int newsize = rsAllocationGetDimX(rsGetAllocation(tmpCards));
241    if (cardAllocationValid) {
242        int size = min(rsAllocationGetDimX(rsGetAllocation(cards)), newsize);
243        for (int i = 0; i < size; i++) {
244            rsDebug("copying card ", i);
245            copyCard(tmpCards + i, cards + i);
246        }
247    }
248    cardAllocationValid = newsize > 0;
249    updateAllocationVars(tmpCards);
250}
251
252// Computes an alpha value for a card using elapsed time and constant fadeInDuration
253float getAnimatedAlpha(int64_t startTime, int64_t currentTime)
254{
255    double timeElapsed = (double) (currentTime - startTime); // in ms
256    double alpha = (double) timeElapsed / fadeInDuration;
257    return min(1.0f, (float) alpha);
258}
259
260// Return angle for position p. Typically p will be an integer position, but can be fractional.
261static float cardPosition(float p)
262{
263    return startAngle + bias + 2.0f * M_PI * p / slotCount;
264}
265
266// Return slot for a card in position p. Typically p will be an integer slot, but can be fractional.
267static float slotPosition(float p)
268{
269    return startAngle + 2.0f * M_PI * p / slotCount;
270}
271
272// Returns total angle for given number of cards
273static float wedgeAngle(float cards)
274{
275    return cards * 2.0f * M_PI / slotCount;
276}
277
278// Return the lowest slot number for a given angular position.
279static int cardIndex(float angle)
280{
281    return floor(angle - startAngle - bias) * slotCount / (2.0f * M_PI);
282}
283
284// Set basic camera properties:
285//    from - position of the camera in x,y,z
286//    at - target we're looking at - used to compute view direction
287//    up - a normalized vector indicating up (typically { 0, 1, 0})
288//
289// NOTE: the view direction and up vector cannot be parallel/antiparallel with each other
290void lookAt(float fromX, float fromY, float fromZ,
291        float atX, float atY, float atZ,
292        float upX, float upY, float upZ)
293{
294    camera.from.x = fromX;
295    camera.from.y = fromY;
296    camera.from.z = fromZ;
297    camera.at.x = atX;
298    camera.at.y = atY;
299    camera.at.z = atZ;
300    camera.up.x = upX;
301    camera.up.y = upY;
302    camera.up.z = upZ;
303    updateCamera = true;
304}
305
306// Load a projection matrix for the given parameters.  This is equivalent to gluPerspective()
307static void loadPerspectiveMatrix(rs_matrix4x4* matrix, float fovy, float aspect, float near, float far)
308{
309    rsMatrixLoadIdentity(matrix);
310    float top = near * tan((float) (fovy * M_PI / 360.0f));
311    float bottom = -top;
312    float left = bottom * aspect;
313    float right = top * aspect;
314    rsMatrixLoadFrustum(matrix, left, right, bottom, top, near, far);
315}
316
317// Construct a matrix based on eye point, center and up direction. Based on the
318// man page for gluLookat(). Up must be normalized.
319static void loadLookatMatrix(rs_matrix4x4* matrix, float3 eye, float3 center, float3 up)
320{
321    float3 f = normalize(center - eye);
322    float3 s = normalize(cross(f, up));
323    float3 u = cross(s, f);
324    float m[16];
325    m[0] = s.x;
326    m[4] = s.y;
327    m[8] = s.z;
328    m[12] = 0.0f;
329    m[1] = u.x;
330    m[5] = u.y;
331    m[9] = u.z;
332    m[13] = 0.0f;
333    m[2] = -f.x;
334    m[6] = -f.y;
335    m[10] = -f.z;
336    m[14] = 0.0f;
337    m[3] = m[7] = m[11] = 0.0f;
338    m[15] = 1.0f;
339    rsMatrixLoad(matrix, m);
340    rsMatrixTranslate(matrix, -eye.x, -eye.y, -eye.z);
341}
342
343void setTexture(int n, rs_allocation texture)
344{
345    if (n < 0 || n >= cardCount) return;
346    rsSetObject(&cards[n].texture, texture);
347    cards[n].textureState = (texture.p != 0) ? STATE_LOADED : STATE_INVALID;
348    cards[n].textureTimeStamp = rsUptimeMillis();
349}
350
351void setDetailTexture(int n, float offx, float offy, float loffx, float loffy, rs_allocation texture)
352{
353    if (n < 0 || n >= cardCount) return;
354    rsSetObject(&cards[n].detailTexture, texture);
355    cards[n].detailTextureOffset.x = offx;
356    cards[n].detailTextureOffset.y = offy;
357    cards[n].detailLineOffset.x = loffx;
358    cards[n].detailLineOffset.y = loffy;
359    cards[n].detailTextureState = (texture.p != 0) ? STATE_LOADED : STATE_INVALID;
360    cards[n].detailTextureTimeStamp = rsUptimeMillis();
361}
362
363void setGeometry(int n, rs_mesh geometry)
364{
365    if (n < 0 || n >= cardCount) return;
366    rsSetObject(&cards[n].geometry, geometry);
367    if (cards[n].geometry.p != 0)
368        cards[n].geometryState = STATE_LOADED;
369    else
370        cards[n].geometryState = STATE_INVALID;
371}
372
373void requestFirstCardPosition()
374{
375    int data[1];
376    data[0] = currentFirstCard;
377    bool enqueued = rsSendToClient(CMD_REPORT_FIRST_CARD_POSITION, data, sizeof(data));
378    if (!enqueued) {
379        rsDebug("Couldn't send CMD_REPORT_FIRST_CARD_POSITION", 0);
380    }
381}
382
383static float3 getAnimatedScaleForSelected()
384{
385    int64_t dt = (rsUptimeMillis() - touchTime);
386    float fraction = (dt < ANIMATION_SCALE_TIME) ? (float) dt / ANIMATION_SCALE_TIME : 1.0f;
387    const float3 one = { 1.0f, 1.0f, 1.0f };
388    return one + fraction * SELECTED_SCALE_FACTOR;
389}
390
391// The Verhulst logistic function: http://en.wikipedia.org/wiki/Logistic_function
392//    P(t) = 1 / (1 + e^(-t))
393// Parameter t: Any real number
394// Returns: A float in the range (0,1), with P(0.5)=0
395static float logistic(float t) {
396    return 1.f / (1.f + exp(-t));
397}
398
399static float getSwayAngleForVelocity(float v, bool enableSway)
400{
401    float sway = 0.0f;
402
403    if (enableSway) {
404        const float range = M_PI; // How far we can deviate from center, peak-to-peak
405        sway = M_PI * (logistic(-v * swaySensitivity) - 0.5f);
406    }
407
408    return sway;
409}
410
411// matrix: The output matrix.
412// i: The card we're getting the matrix for.
413// enableSway: Whether to enable swaying. (We want it on for cards, and off for detail textures.)
414static void getMatrixForCard(rs_matrix4x4* matrix, int i, bool enableSway)
415{
416    float theta = cardPosition(i);
417    float swayAngle = getSwayAngleForVelocity(velocity, enableSway);
418    rsMatrixRotate(matrix, degrees(theta), 0, 1, 0);
419    rsMatrixTranslate(matrix, radius, 0, 0);
420    rsMatrixRotate(matrix, degrees(-theta + cardRotation + swayAngle), 0, 1, 0);
421    if (i == currentSelection) {
422        float3 scale = getAnimatedScaleForSelected();
423        rsMatrixScale(matrix, scale.x, scale.y, scale.z);
424    }
425    // TODO: apply custom matrix for cards[i].geometry
426}
427
428/*
429 * Draws cards around the Carousel.
430 * Returns true if we're still animating any property of the cards (e.g. fades).
431 */
432static bool drawCards(int64_t currentTime)
433{
434    const float wedgeAngle = 2.0f * M_PI / slotCount;
435    const float endAngle = startAngle + visibleSlotCount * wedgeAngle;
436    bool stillAnimating = false;
437    for (int i = cardCount-1; i >= 0; i--) {
438        if (cards[i].visible) {
439            // If this card was recently loaded, this will be < 1.0f until the animation completes
440            float animatedAlpha = getAnimatedAlpha(cards[i].textureTimeStamp, currentTime);
441            if (animatedAlpha < 1.0f) {
442                stillAnimating = true;
443            }
444
445            // Compute fade out for cards in the distance
446            float positionAlpha;
447            if (rezInCardCount > 0.0f) {
448                positionAlpha = (endAngle - cardPosition(i)) / wedgeAngle;
449                positionAlpha = min(1.0f, positionAlpha / rezInCardCount);
450            } else {
451                positionAlpha = 1.0f;
452            }
453
454            // Set alpha for blending between the textures
455            shaderConstants->fadeAmount = min(1.0f, animatedAlpha * positionAlpha);
456            rsAllocationMarkDirty(rsGetAllocation(shaderConstants));
457
458            // Bind the appropriate shader network.  If there's no alpha blend, then
459            // switch to single shader for better performance.
460            const bool loaded = cards[i].textureState == STATE_LOADED;
461            if (shaderConstants->fadeAmount == 1.0f || shaderConstants->fadeAmount < 0.01f) {
462                rsgBindProgramFragment(singleTextureFragmentProgram);
463                rsgBindTexture(singleTextureFragmentProgram, 0,
464                        (loaded && shaderConstants->fadeAmount == 1.0f) ?
465                        cards[i].texture : loadingTexture);
466            } else {
467                rsgBindProgramFragment(multiTextureFragmentProgram);
468                rsgBindTexture(multiTextureFragmentProgram, 0, loadingTexture);
469                rsgBindTexture(multiTextureFragmentProgram, 1, loaded ?
470                        cards[i].texture : loadingTexture);
471            }
472
473            // Draw geometry
474            rs_matrix4x4 matrix = modelviewMatrix;
475            getMatrixForCard(&matrix, i, true);
476            rsgProgramVertexLoadModelMatrix(&matrix);
477            if (cards[i].geometryState == STATE_LOADED && cards[i].geometry.p != 0) {
478                rsgDrawMesh(cards[i].geometry);
479            } else if (cards[i].geometryState == STATE_LOADING && loadingGeometry.p != 0) {
480                rsgDrawMesh(loadingGeometry);
481            } else if (defaultGeometry.p != 0) {
482                rsgDrawMesh(defaultGeometry);
483            } else {
484                // Draw place-holder geometry
485                rsgDrawQuad(
486                    cardVertices[0].x, cardVertices[0].y, cardVertices[0].z,
487                    cardVertices[1].x, cardVertices[1].y, cardVertices[1].z,
488                    cardVertices[2].x, cardVertices[2].y, cardVertices[2].z,
489                    cardVertices[3].x, cardVertices[3].y, cardVertices[3].z);
490            }
491        }
492    }
493    return stillAnimating;
494}
495
496/*
497 * Draws a screen-aligned card with the exact dimensions from the detail texture.
498 * This is used to display information about the object being displayed above the geomertry.
499 * Returns true if we're still animating any property of the cards (e.g. fades).
500 */
501static bool drawDetails(int64_t currentTime)
502{
503    const float width = rsgGetWidth();
504    const float height = rsgGetHeight();
505
506    bool stillAnimating = false;
507
508    // We'll be drawing in screen space, sampled on pixel centers
509    rs_matrix4x4 projection, model;
510    rsMatrixLoadOrtho(&projection, 0.0f, width, 0.0f, height, 0.0f, 1.0f);
511    rsgProgramVertexLoadProjectionMatrix(&projection);
512    rsMatrixLoadIdentity(&model);
513    rsgProgramVertexLoadModelMatrix(&model);
514    updateCamera = true; // we messed with the projection matrix. Reload on next pass...
515
516    const float yPadding = 5.0f; // draw line this far (in pixels) away from top and geometry
517
518    // This can be done once...
519    rsgBindTexture(multiTextureFragmentProgram, 0, detailLoadingTexture);
520
521    const float wedgeAngle = 2.0f * M_PI / slotCount;
522    // Angle where details start fading from 1.0f
523    const float startDetailFadeAngle = startAngle + (visibleDetailCount - 1) * wedgeAngle;
524    // Angle where detail alpha is 0.0f
525    const float endDetailFadeAngle = startDetailFadeAngle + detailFadeRate * wedgeAngle;
526
527    for (int i = cardCount-1; i >= 0; --i) {
528        if (cards[i].visible) {
529            if (cards[i].detailTextureState == STATE_LOADED && cards[i].detailTexture.p != 0) {
530                const float lineWidth = rsAllocationGetDimX(detailLineTexture);
531
532                // Compute position in screen space of upper left corner of card
533                rsMatrixLoad(&model, &modelviewMatrix);
534                getMatrixForCard(&model, i, false);
535                rs_matrix4x4 matrix;
536                rsMatrixLoadMultiply(&matrix, &projectionMatrix, &model);
537
538                float4 screenCoord = rsMatrixMultiply(&matrix,
539                    cardVertices[drawDetailBelowCard ? 0 : 3]);
540                if (screenCoord.w == 0.0f) {
541                    // this shouldn't happen
542                    rsDebug("Bad transform: ", screenCoord);
543                    continue;
544                }
545
546                // Compute alpha for gradually fading in details. Applied to both line and
547                // detail texture. TODO: use a separate background texture for line.
548                float animatedAlpha = getAnimatedAlpha(cards[i].detailTextureTimeStamp, currentTime);
549                if (animatedAlpha < 1.0f) {
550                    stillAnimating = true;
551                }
552
553                // Compute alpha based on position. We fade cards quickly so they cannot overlap
554                float positionAlpha = ((float)endDetailFadeAngle - cardPosition(i))
555                        / (endDetailFadeAngle - startDetailFadeAngle);
556                positionAlpha = max(0.0f, positionAlpha);
557                positionAlpha = min(1.0f, positionAlpha);
558
559                const float blendedAlpha = min(1.0f, animatedAlpha * positionAlpha);
560
561                if (blendedAlpha == 0.0f) continue; // nothing to draw
562                if (blendedAlpha == 1.0f) {
563                    rsgBindProgramFragment(singleTextureFragmentProgram);
564                } else {
565                    rsgBindProgramFragment(multiTextureFragmentProgram);
566                }
567
568                // Set alpha for blending between the textures
569                shaderConstants->fadeAmount = blendedAlpha;
570                rsAllocationMarkDirty(rsGetAllocation(shaderConstants));
571
572                // Convert projection from normalized coordinates to pixel coordinates.
573                // This is probably cheaper than pre-multiplying the above with another matrix.
574                screenCoord *= 1.0f / screenCoord.w;
575                screenCoord.x += 1.0f;
576                screenCoord.y += 1.0f;
577                screenCoord.z += 1.0f;
578                screenCoord.x = round(screenCoord.x * 0.5f * width);
579                screenCoord.y = round(screenCoord.y * 0.5f * height);
580                screenCoord.z = - 0.5f * screenCoord.z;
581                if (debugDetails) {
582                    RS_DEBUG(screenCoord);
583                }
584
585                // Draw line from upper left card corner to the top of the screen
586                if (drawRuler) {
587                    const float halfWidth = lineWidth * 0.5f;
588                    const float rulerTop = drawDetailBelowCard ? screenCoord.y : height;
589                    const float rulerBottom = drawDetailBelowCard ? 0 : screenCoord.y;
590                    const float x0 = cards[i].detailLineOffset.x + screenCoord.x - halfWidth;
591                    const float x1 = cards[i].detailLineOffset.x + screenCoord.x + halfWidth;
592                    const float y0 = rulerBottom + yPadding;
593                    const float y1 = rulerTop - yPadding - cards[i].detailLineOffset.y;
594
595                    if (blendedAlpha == 1.0f) {
596                        rsgBindTexture(singleTextureFragmentProgram, 0, detailLineTexture);
597                    } else {
598                        rsgBindTexture(multiTextureFragmentProgram, 1, detailLineTexture);
599                    }
600                    rsgDrawQuad(x0, y0, screenCoord.z,  x1, y0, screenCoord.z,
601                            x1, y1, screenCoord.z,  x0, y1, screenCoord.z);
602                }
603
604                // Draw the detail texture next to it using the offsets provided.
605                const float textureWidth = rsAllocationGetDimX(cards[i].detailTexture);
606                const float textureHeight = rsAllocationGetDimY(cards[i].detailTexture);
607                const float offx = cards[i].detailTextureOffset.x;
608                const float offy = -cards[i].detailTextureOffset.y;
609                const float textureTop = drawDetailBelowCard ? screenCoord.y : height;
610                const float x0 = cards[i].detailLineOffset.x + screenCoord.x + offx;
611                const float x1 = cards[i].detailLineOffset.x + screenCoord.x + offx + textureWidth;
612                const float y0 = textureTop + offy - textureHeight - cards[i].detailLineOffset.y;
613                const float y1 = textureTop + offy - cards[i].detailLineOffset.y;
614
615                if (blendedAlpha == 1.0f) {
616                    rsgBindTexture(singleTextureFragmentProgram, 0, cards[i].detailTexture);
617                } else {
618                    rsgBindTexture(multiTextureFragmentProgram, 1, cards[i].detailTexture);
619                }
620                rsgDrawQuad(x0, y0, screenCoord.z,  x1, y0, screenCoord.z,
621                        x1, y1, screenCoord.z,  x0, y1, screenCoord.z);
622            }
623        }
624    }
625    return stillAnimating;
626}
627
628static void drawBackground()
629{
630    static bool toggle;
631    if (backgroundTexture.p != 0) {
632        // Unfortunately, we also need to clear the background because some textures may be
633        // drawn with alpha. This takes about 1ms-2ms in my tests. May be worth optimizing at
634        // some point.
635        rsgClearColor(backgroundColor.x, backgroundColor.y, backgroundColor.z, backgroundColor.w);
636
637        rsgClearDepth(1.0f);
638        rs_matrix4x4 projection, model;
639        rsMatrixLoadOrtho(&projection, -1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f);
640        rsgProgramVertexLoadProjectionMatrix(&projection);
641        rsMatrixLoadIdentity(&model);
642        rsgProgramVertexLoadModelMatrix(&model);
643        rsgBindTexture(singleTextureFragmentProgram, 0, backgroundTexture);
644        float z = -0.9999f;
645        rsgDrawQuad(
646            cardVertices[0].x, cardVertices[0].y, z,
647            cardVertices[1].x, cardVertices[1].y, z,
648            cardVertices[2].x, cardVertices[2].y, z,
649            cardVertices[3].x, cardVertices[3].y, z);
650        updateCamera = true; // we mucked with the matrix.
651    } else {
652        rsgClearDepth(1.0f);
653        if (debugRendering) { // for debugging - flash the screen so we know we're still rendering
654            rsgClearColor(toggle ? backgroundColor.x : 1.0f,
655                        toggle ? backgroundColor.y : 0.0f,
656                        toggle ? backgroundColor.z : 0.0f,
657                        backgroundColor.w);
658            toggle = !toggle;
659        } else {
660           rsgClearColor(backgroundColor.x, backgroundColor.y, backgroundColor.z,
661                   backgroundColor.w);
662       }
663    }
664}
665
666static void updateCameraMatrix(float width, float height)
667{
668    float aspect = width / height;
669    if (aspect != camera.aspect || updateCamera) {
670        camera.aspect = aspect;
671        loadPerspectiveMatrix(&projectionMatrix, camera.fov, camera.aspect, camera.near, camera.far);
672        rsgProgramVertexLoadProjectionMatrix(&projectionMatrix);
673
674        loadLookatMatrix(&modelviewMatrix, camera.from, camera.at, camera.up);
675        rsgProgramVertexLoadModelMatrix(&modelviewMatrix);
676        updateCamera = false;
677    }
678}
679
680////////////////////////////////////////////////////////////////////////////////////////////////////
681// Behavior/Physics
682////////////////////////////////////////////////////////////////////////////////////////////////////
683static bool isDragging;
684static int64_t lastTime = 0L; // keep track of how much time has passed between frames
685static float2 lastPosition;
686static bool animating = false;
687static float velocityThreshold = 0.1f * M_PI / 180.0f;
688static float velocityTracker;
689static int velocityTrackerCount;
690static float mass = 5.0f; // kg
691
692static const float G = 9.80f; // gravity constant, in m/s
693static const float springConstant = 0.0f;
694
695static float dragFunction(float x, float y)
696{
697    return dragFactor * ((x - lastPosition.x) / rsgGetWidth()) * M_PI;
698}
699
700static float deltaTimeInSeconds(int64_t current)
701{
702    return (lastTime > 0L) ? (float) (current - lastTime) / 1000.0f : 0.0f;
703}
704
705int doSelection(float x, float y)
706{
707    Ray ray;
708    if (makeRayForPixelAt(&ray, &camera, x, y)) {
709        float bestTime = FLT_MAX;
710        return intersectGeometry(&ray, &bestTime);
711    }
712    return -1;
713}
714
715void doStart(float x, float y)
716{
717    lastPosition.x = x;
718    lastPosition.y = y;
719    velocity = 0.0f;
720    if (animating) {
721        rsSendToClient(CMD_ANIMATION_FINISHED);
722        animating = false;
723        currentSelection = -1;
724    } else {
725        currentSelection = doSelection(x, y);
726    }
727    velocityTracker = 0.0f;
728    velocityTrackerCount = 0;
729    touchTime = rsUptimeMillis();
730    touchBias = bias;
731}
732
733
734void doStop(float x, float y)
735{
736    int64_t currentTime = rsUptimeMillis();
737    updateAllocationVars(cards);
738    if (currentSelection != -1 && (currentTime - touchTime) < ANIMATION_SCALE_TIME) {
739        // rsDebug("HIT!", currentSelection);
740        int data[1];
741        data[0] = currentSelection;
742        rsSendToClientBlocking(CMD_CARD_SELECTED, data, sizeof(data));
743    } else {
744        velocity = velocityTrackerCount > 0 ?
745                    (velocityTracker / velocityTrackerCount) : 0.0f;  // avg velocity
746        if (fabs(velocity) > velocityThreshold) {
747            animating = true;
748            rsSendToClient(CMD_ANIMATION_STARTED);
749        }
750    }
751    currentSelection = -1;
752    lastTime = rsUptimeMillis();
753}
754
755void doMotion(float x, float y)
756{
757    int64_t currentTime = rsUptimeMillis();
758    float deltaOmega = dragFunction(x, y);
759    bias += deltaOmega;
760    lastPosition.x = x;
761    lastPosition.y = y;
762    float dt = deltaTimeInSeconds(currentTime);
763    if (dt > 0.0f) {
764        float v = deltaOmega / dt;
765        //if ((velocityTracker > 0.0f) == (v > 0.0f)) {
766            velocityTracker += v;
767            velocityTrackerCount++;
768        //} else {
769        //    velocityTracker = v;
770        //    velocityTrackerCount = 1;
771        //}
772    }
773    velocity = velocityTrackerCount > 0 ?
774                (velocityTracker / velocityTrackerCount) : 0.0f;  // avg velocity
775
776    // Drop current selection if user drags position +- a partial slot
777    if (currentSelection != -1) {
778        const float slotMargin = 0.5f * (2.0f * M_PI / slotCount);
779        if (fabs(touchBias - bias) > slotMargin) {
780            currentSelection = -1;
781        }
782    }
783    lastTime = currentTime;
784}
785
786////////////////////////////////////////////////////////////////////////////////////////////////////
787// Hit detection using ray casting.
788////////////////////////////////////////////////////////////////////////////////////////////////////
789
790static bool
791rayTriangleIntersect(Ray* ray, float3 p0, float3 p1, float3 p2, float *tout)
792{
793    static const float tmin = 0.0f;
794
795    float3 e1 = p1 - p0;
796    float3 e2 = p2 - p0;
797    float3 s1 = cross(ray->direction, e2);
798
799    float div = dot(s1, e1);
800    if (div == 0.0f) return false;  // ray is parallel to plane.
801
802    float3 d = ray->position - p0;
803    float invDiv = 1.0f / div;
804
805    float u = dot(d, s1) * invDiv;
806    if (u < 0.0f || u > 1.0f) return false;
807
808    float3 s2 = cross(d, e1);
809    float v = dot(ray->direction, s2) * invDiv;
810    if ( v < 0.0f || (u+v) > 1.0f) return false;
811
812    float t = dot(e2, s2) * invDiv;
813    if (t < tmin || t > *tout)
814        return false;
815    *tout = t;
816    return true;
817}
818
819static bool
820rayPlaneIntersect(Ray* ray, float3 point, float3 normal)
821{
822    return false; // TODO
823}
824
825static bool
826rayCylinderIntersect(Ray* ray, float3 center, float radius)
827{
828    return false; // TODO
829}
830
831// Creates a ray for an Android pixel coordinate given a camera, ray and coordinates.
832// Note that the Y coordinate is opposite of GL rendering coordinates.
833static bool __attribute__((overloadable))
834makeRayForPixelAt(Ray* ray, PerspectiveCamera* cam, float x, float y)
835{
836    if (debugCamera) {
837        rsDebug("------ makeRay() -------", 0);
838        rsDebug("Camera.from:", cam->from);
839        rsDebug("Camera.at:", cam->at);
840        rsDebug("Camera.dir:", normalize(cam->at - cam->from));
841    }
842
843    // Vector math.  This has the potential to be much faster.
844    // TODO: pre-compute lowerLeftRay, du, dv to eliminate most of this math.
845    const float u = x / rsgGetWidth();
846    const float v = 1.0f - (y / rsgGetHeight());
847    const float aspect = (float) rsgGetWidth() / rsgGetHeight();
848    const float tanfov2 = 2.0f * tan(radians(cam->fov / 2.0f));
849    float3 dir = normalize(cam->at - cam->from);
850    float3 du = tanfov2 * normalize(cross(dir, cam->up));
851    float3 dv = tanfov2 * normalize(cross(du, dir));
852    du *= aspect;
853    float3 lowerLeftRay = dir - (0.5f * du) - (0.5f * dv);
854    const float3 rayPoint = cam->from;
855    const float3 rayDir = normalize(lowerLeftRay + u*du + v*dv);
856    if (debugCamera) {
857        rsDebug("Ray direction (vector math) = ", rayDir);
858    }
859
860    ray->position =  rayPoint;
861    ray->direction = rayDir;
862    return true;
863}
864
865// Creates a ray for an Android pixel coordinate given a model view and projection matrix.
866// Note that the Y coordinate is opposite of GL rendering coordinates.
867static bool __attribute__((overloadable))
868makeRayForPixelAt(Ray* ray, rs_matrix4x4* model, rs_matrix4x4* proj, float x, float y)
869{
870    rs_matrix4x4 pm = *model;
871    rsMatrixLoadMultiply(&pm, proj, model);
872    if (!rsMatrixInverse(&pm)) {
873        rsDebug("ERROR: SINGULAR PM MATRIX", 0);
874        return false;
875    }
876    const float width = rsgGetWidth();
877    const float height = rsgGetHeight();
878    const float winx = 2.0f * x / width - 1.0f;
879    const float winy = 2.0f * y / height - 1.0f;
880
881    float4 eye = { 0.0f, 0.0f, 0.0f, 1.0f };
882    float4 at = { winx, winy, 1.0f, 1.0f };
883
884    eye = rsMatrixMultiply(&pm, eye);
885    eye *= 1.0f / eye.w;
886
887    at = rsMatrixMultiply(&pm, at);
888    at *= 1.0f / at.w;
889
890    const float3 rayPoint = { eye.x, eye.y, eye.z };
891    const float3 atPoint = { at.x, at.y, at.z };
892    const float3 rayDir = normalize(atPoint - rayPoint);
893    if (debugCamera) {
894        rsDebug("winx: ", winx);
895        rsDebug("winy: ", winy);
896        rsDebug("Ray position (transformed) = ", eye);
897        rsDebug("Ray direction (transformed) = ", rayDir);
898    }
899    ray->position =  rayPoint;
900    ray->direction = rayDir;
901    return true;
902}
903
904static int intersectGeometry(Ray* ray, float *bestTime)
905{
906    int hit = -1;
907    for (int id = 0; id < cardCount; id++) {
908        if (cards[id].visible) {
909            rs_matrix4x4 matrix;
910            float3 p[4];
911
912            // Transform card vertices to world space
913            rsMatrixLoadIdentity(&matrix);
914            getMatrixForCard(&matrix, id, true);
915            for (int vertex = 0; vertex < 4; vertex++) {
916                float4 tmp = rsMatrixMultiply(&matrix, cardVertices[vertex]);
917                if (tmp.w != 0.0f) {
918                    p[vertex].x = tmp.x;
919                    p[vertex].y = tmp.y;
920                    p[vertex].z = tmp.z;
921                    p[vertex] *= 1.0f / tmp.w;
922                } else {
923                    rsDebug("Bad w coord: ", tmp);
924                }
925            }
926
927            // Intersect card geometry
928            if (rayTriangleIntersect(ray, p[0], p[1], p[2], bestTime)
929                || rayTriangleIntersect(ray, p[2], p[3], p[0], bestTime)) {
930                hit = id;
931            }
932        }
933    }
934    return hit;
935}
936
937// This method computes the position of all the cards by updating bias based on a
938// simple physics model.
939// If the cards are still in motion, returns true.
940static bool updateNextPosition(int64_t currentTime)
941{
942    if (animating) {
943        float dt = deltaTimeInSeconds(currentTime);
944        if (dt <= 0.0f)
945            return animating;
946        const float minStepTime = 1.0f / 300.0f; // ~5 steps per frame
947        const int N = (dt > minStepTime) ? (1 + round(dt / minStepTime)) : 1;
948        dt /= N;
949        for (int i = 0; i < N; i++) {
950            // Force friction - always opposes motion
951            const float Ff = -frictionCoeff * velocity;
952
953            // Restoring force to match cards with slots
954            const float theta = startAngle + bias;
955            const float dtheta = 2.0f * M_PI / slotCount;
956            const float position = theta / dtheta;
957            const float fraction = position - floor(position); // fractional position between slots
958            float x;
959            if (fraction > 0.5f) {
960                x = - (1.0f - fraction);
961            } else {
962                x = fraction;
963            }
964            const float Fr = - springConstant * x;
965
966            // compute velocity
967            const float momentum = mass * velocity + (Ff + Fr)*dt;
968            velocity = momentum / mass;
969            bias += velocity * dt;
970        }
971
972        animating = fabs(velocity) > velocityThreshold;
973        if (!animating) {
974            rsSendToClient(CMD_ANIMATION_FINISHED);
975        }
976    }
977    lastTime = currentTime;
978
979    const float firstBias = wedgeAngle(0.0f);
980    const float lastBias = -max(0.0f, wedgeAngle(cardCount - visibleDetailCount));
981
982    if (bias > firstBias) {
983        bias = firstBias;
984    } else if (bias < lastBias) {
985        bias = lastBias;
986    }
987
988    return animating;
989}
990
991// Cull cards based on visibility and visibleSlotCount.
992// If visibleSlotCount is > 0, then only show those slots and cull the rest.
993// Otherwise, it should cull based on bounds of geometry.
994static int cullCards()
995{
996    const float thetaFirst = slotPosition(-1); // -1 keeps the card in front around a bit longer
997    const float thetaSelected = slotPosition(0);
998    const float thetaHalfAngle = (thetaSelected - thetaFirst) * 0.5f;
999    const float thetaSelectedLow = thetaSelected - thetaHalfAngle;
1000    const float thetaSelectedHigh = thetaSelected + thetaHalfAngle;
1001    const float thetaLast = slotPosition(visibleSlotCount);
1002
1003    int count = 0;
1004    int firstVisible = -1;
1005    for (int i = 0; i < cardCount; i++) {
1006        if (visibleSlotCount > 0) {
1007            // If visibleSlotCount is specified, then only show up to visibleSlotCount cards.
1008            float p = cardPosition(i);
1009            if (p >= thetaFirst && p < thetaLast) {
1010                if (firstVisible == -1 && p >= thetaSelectedLow && p < thetaSelectedHigh) {
1011                    firstVisible = i;
1012                }
1013                cards[i].visible = true;
1014                count++;
1015            } else {
1016                cards[i].visible = false;
1017            }
1018        } else {
1019            // Cull the rest of the cards using bounding box of geometry.
1020            // TODO
1021            cards[i].visible = true;
1022            count++;
1023        }
1024    }
1025    currentFirstCard = firstVisible;
1026    return count;
1027}
1028
1029// Request texture/geometry for items that have come into view
1030// or doesn't have a texture yet.
1031static void updateCardResources(int64_t currentTime)
1032{
1033    for (int i = cardCount-1; i >= 0; --i) {
1034        int data[1];
1035        if (cards[i].visible) {
1036            // request texture from client if not loaded
1037            if (cards[i].textureState == STATE_INVALID) {
1038                data[0] = i;
1039                bool enqueued = rsSendToClient(CMD_REQUEST_TEXTURE, data, sizeof(data));
1040                if (enqueued) {
1041                    cards[i].textureState = STATE_LOADING;
1042                } else {
1043                    if (debugTextureLoading) rsDebug("Couldn't send CMD_REQUEST_TEXTURE", 0);
1044                }
1045            }
1046            // request detail texture from client if not loaded
1047            if (cards[i].detailTextureState == STATE_INVALID) {
1048                data[0] = i;
1049                bool enqueued = rsSendToClient(CMD_REQUEST_DETAIL_TEXTURE, data, sizeof(data));
1050                if (enqueued) {
1051                    cards[i].detailTextureState = STATE_LOADING;
1052                } else {
1053                    if (debugTextureLoading) rsDebug("Couldn't send CMD_REQUEST_DETAIL_TEXTURE", 0);
1054                }
1055            }
1056            // request geometry from client if not loaded
1057            if (cards[i].geometryState == STATE_INVALID) {
1058                data[0] = i;
1059                bool enqueued = rsSendToClient(CMD_REQUEST_GEOMETRY, data, sizeof(data));
1060                if (enqueued) {
1061                    cards[i].geometryState = STATE_LOADING;
1062                } else {
1063                    if (debugGeometryLoading) rsDebug("Couldn't send CMD_REQUEST_GEOMETRY", 0);
1064                }
1065            }
1066        } else {
1067            // ask the host to remove the texture
1068            if (cards[i].textureState != STATE_INVALID) {
1069                data[0] = i;
1070                bool enqueued = rsSendToClient(CMD_INVALIDATE_TEXTURE, data, sizeof(data));
1071                if (enqueued) {
1072                    cards[i].textureState = STATE_INVALID;
1073                    cards[i].textureTimeStamp = currentTime;
1074                } else {
1075                    if (debugTextureLoading) rsDebug("Couldn't send CMD_INVALIDATE_TEXTURE", 0);
1076                }
1077            }
1078            // ask the host to remove the detail texture
1079            if (cards[i].detailTextureState != STATE_INVALID) {
1080                data[0] = i;
1081                bool enqueued = rsSendToClient(CMD_INVALIDATE_DETAIL_TEXTURE, data, sizeof(data));
1082                if (enqueued) {
1083                    cards[i].detailTextureState = STATE_INVALID;
1084                    cards[i].detailTextureTimeStamp = currentTime;
1085                } else {
1086                    if (debugTextureLoading) rsDebug("Can't send CMD_INVALIDATE_DETAIL_TEXTURE", 0);
1087                }
1088            }
1089            // ask the host to remove the geometry
1090            if (cards[i].geometryState != STATE_INVALID) {
1091                data[0] = i;
1092                bool enqueued = rsSendToClient(CMD_INVALIDATE_GEOMETRY, data, sizeof(data));
1093                if (enqueued) {
1094                    cards[i].geometryState = STATE_INVALID;
1095                } else {
1096                    if (debugGeometryLoading) rsDebug("Couldn't send CMD_INVALIDATE_GEOMETRY", 0);
1097                }
1098            }
1099        }
1100    }
1101}
1102
1103// Places dots on geometry to visually inspect that objects can be seen by rays.
1104// NOTE: the color of the dot is somewhat random, as it depends on texture of previously-rendered
1105// card.
1106static void renderWithRays()
1107{
1108    const float w = rsgGetWidth();
1109    const float h = rsgGetHeight();
1110    const int skip = 8;
1111    color(1.0f, 0.0f, 0.0f, 1.0f);
1112    for (int j = 0; j < (int) h; j+=skip) {
1113        float posY = (float) j;
1114        for (int i = 0; i < (int) w; i+=skip) {
1115            float posX = (float) i;
1116            Ray ray;
1117            if (makeRayForPixelAt(&ray, &camera, posX, posY)) {
1118                float bestTime = FLT_MAX;
1119                if (intersectGeometry(&ray, &bestTime) != -1) {
1120                    rsgDrawSpriteScreenspace(posX, h - posY - 1, 0.0f, 2.0f, 2.0f);
1121                }
1122            }
1123        }
1124    }
1125}
1126
1127int root() {
1128    int64_t currentTime = rsUptimeMillis();
1129
1130    rsgBindProgramVertex(vertexProgram);
1131    rsgBindProgramStore(programStore);
1132    rsgBindProgramRaster(rasterProgram);
1133    rsgBindSampler(singleTextureFragmentProgram, 0, linearClamp);
1134    rsgBindSampler(multiTextureFragmentProgram, 0, linearClamp);
1135    rsgBindSampler(multiTextureFragmentProgram, 1, linearClamp);
1136
1137    updateAllocationVars(cards);
1138
1139    if (!initialized) {
1140        const float2 zero = {0.0f, 0.0f};
1141        for (int i = 0; i < cardCount; i++) {
1142            cards[i].textureState = STATE_INVALID;
1143            cards[i].detailTextureState = STATE_INVALID;
1144            cards[i].detailTextureOffset = zero;
1145        }
1146        initialized = true;
1147    }
1148
1149    rsgBindProgramFragment(singleTextureFragmentProgram);
1150    drawBackground();
1151
1152    updateCameraMatrix(rsgGetWidth(), rsgGetHeight());
1153
1154    const bool timeExpired = (currentTime - touchTime) > ANIMATION_SCALE_TIME;
1155    bool stillAnimating = updateNextPosition(currentTime) || !timeExpired;
1156
1157    cullCards();
1158
1159    updateCardResources(currentTime);
1160
1161    stillAnimating |= drawCards(currentTime);
1162    stillAnimating |= drawDetails(currentTime);
1163
1164    if (debugPicking) {
1165        renderWithRays();
1166    }
1167
1168    //rsSendToClient(CMD_PING);
1169
1170    return stillAnimating ? 1 : 0;
1171}
1172