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