Skip to content

Instantly share code, notes, and snippets.

@pmalin
Last active November 26, 2020 12:53
Show Gist options
  • Save pmalin/e6267715ef3853498b96914ee3305cf0 to your computer and use it in GitHub Desktop.
Save pmalin/e6267715ef3853498b96914ee3305cf0 to your computer and use it in GitHub Desktop.
3D Spinning Glass - Annotated
// Breaking Down My ImGui Party Entry "3D Spinning Glass" - @P_Malin
// https://github.com/ocornut/imgui/issues/3606#issuecomment-733261493
// This is an annotated version of https://gist.github.com/pmalin/550058583492478beedd7b31ab468174
// I've added comments and whitespace to document how this works and some size optimizations used
// define types that we use often enough that having the #define is a character saving
#define V ImVec2
#define F float
// function R applies a generic 2d (R)otation by 'r' to the inputs x and y passed by reference
// We can apply rotations about arbitrary 3d axes by passing different combinations of x,y and z as the input
void R(F&x,F&y,F r){F s=sin(r),c=cos(r),t=c*x-s*y;y=s*x+c*y;x=t;}
// Declare all our float variables so we don't have to keep repeating the text 'float' (or 'F')
F c,e,f,g,l,x,y,z;
// L is the (L)ighting function.
// We call this function individually on the red,green and blue components to get a 0->1 value for each of those
// the parameter 'a' is the diffuse albedo
// the variable 'l' is "N dot L" (normal dot light direction) from our 3d scene.
// Really l should be a parameter that is passed in but we save a lot of characters
// not passing it in and accessing it via a global
F L(F a)
{
// clamp l to zero if negative
l*=l>0;
// use sqrt for cheap (small) gamma
return sqrt(
// tonemap
// the tonemap is basically OutputColour=1-exp(-SceneIntensity*Exposure). We can ignore Exposure (assume it is 1.0) but the SceneIntensity value here must be negated which is why all the components here are negative.
1-exp(
// diffuse albedo * (direct + ambient)
-a*(l+.1)
// "specular" - but not really specular, just add on (N dot L)^3
-l*l*l
)
);
}
// T is the (T)ransform function. It applies three rotations in turn by arbitrary scales of time.
// time is in the variable 'c'. Again, 'c' should be passed in but is much fewer characters if we reference it via a global.
// We use this function to rotate points and normals so it does not do any translation
// The first rotation (about the Up axis) also incorporates the sweep of the "revolution" of the object.
// the parameter u is how far around the object we are (in 0->1 units)
void T(F u)
{
// Rotate about Y axis
// 6.28 is approximately 2 * PI
// the /3 here is arbitrary scaling of time to make our rotation how we like
R(x,z,c/3+6.28*u);
// Rotate about Z axis
R(x,y,c);
// Rotate about X axis
// the /5 here is arbitrary scaling of time to make our rotation how we like
R(y,z,c/5);
}
// The Dear ImGui party function!
// See the party post https://github.com/ocornut/imgui/issues/3606
// a and b are the extents of the rectangle we are drawing to
// S is the size of the rectangle (a->b)
// t is time
void FX(ImDrawList*d,V a,V b,V S,ImVec4,F t)
{
// Declare all the integers we use so we don't have to keep repeating "int" everwhere
// K and J are the number of points in the cross-section and the number of segments in the revolution sweep respectively.
// we use n to count how many quads we have output (its fewer characters to count them up rather than calculate it) so we initialise this to zero here.
int i,j,k,n=0,u,v,K=7,J=16;
// This string defines the shape of the cross section, each pair of characters is a co-ordinate on a scale with A=0 and Z=1
// So there should be 2*K characters in this string
// We setup X as a pointer to the current cross section vertex data
char P[]="GZHXITGPBMBCGA",*X=P;
// Q is a structure representing each (Q)uad we draw
// we also declare an array of 99 of these here in 'q'
// 99 should really be something like J*(K-1) but J and K are non-const and 99 is fewer characters :D. As long as the array is "big enough" we're good :)
struct Q
{
// We sort the quads back to front by depth, 'd' is the "depth" value we sort with
// 'l' is the (l)ighting for this quad - it stores N dot L (quad normal dot light direction)
F d,l;
// 'o' is the array of (o)utput vertices for the quad. This is an array of 4 ImVec2s we can pass to the ImDrawList API functions
V o[4];
}q[99];
// 'a' and 'b' are passed in to our main function F as the min and max extents of the window rectangle we are drawing to.
// Here we calculate 'm' as the (m)iddle or (m)idpoint of these.
// Saving this here as an operation on the vector types saves a lot of characters as we don't need to repeat this logic and dereferencing for x and y.
V m=(a+b)/2;
// Loop through the verticies of the cross-section
// (we loop through 'k' for values from K-1 down to 1)
for(k=K;--k;)
{
// Increment the cross section vertex pointer
X+=2;
// Loop through the rotational sweep segments
// (we loop through 'j' for values from J-1 down to 0)
for(j=J;j--;)
{
// Take a reference 'o' to our (o)utput quad.
// Incrementing n here is also counting how many quads we have output
Q&o=q[n++];
// We sort the quads based on 'd' their average depth (actually average of 1/depth as its fewer characters) and we zero this here.
// We don't use this value for anything except for sorting and
// the number of vertices in every primitive is always 4 so we don't need to divide it by the number of vertices anywhere to get an actual average.
o.d=0;
// For each vertex in the quad
// (we loop through 'i' for values from 3 down to 0)
for(i=4;i--;)
{
// calculate 'u' us the rotational sweep segment index for this quad vertex
u=j+(i%3>0);
// calculate 'v' as an offset to this quad vertex's cross-section vertex
v=-(i/2)*2;
// Initialize x,y and z from the cross-section vertex.
// These are stored in a string so we calculate char-65 (where 65 is ASCII 'A') to zero values on the X axis
// and we caluclate 77-char (where 77 is ASCII 'M') to centre (and mirror) values on the Y axis
x=X[v]-65;
y=77-X[v+1];
z=0;
// Rotate the 3d point x,y,z (this incorporates the rotational sweep)
// we copy the time function parameter t into the global c and globally access it in the function T() as a character saving
c=t;
// pass in the fraction around the rotational sweep (we need to cast to float or this will do integer division)
T((F)u/J);
// We are doing a couple of things here, all together for character savings.
// For a backface test we need a vector from the viewer to one of the vertices in the quad
// we copy x,y,z into e,f,g each iteration in the loop so when the loop exits these values will hold the last vertex x,y,z position and we wont need to calculate that again.
e=x;
f=y;
// we add 40 to z to move it away from the camera before we store it in g
// we then update z to be a scale factor for our perspective transformation
// the scale factor is scaled by S.y so our output ends up scaling with the output window height
// and our scale factor is made proportional to 1/z for perspective
// we then update our "average depth" total in o.d with this
o.d+=z=S.y/(g=z+40);
// Here we calculate the screen co-ordinate of our quad vertex.
// We apply the perspective scale factor (now in z) to x and y and add an offset to the centre of the screen.
// We save a lot of characters here by constructing an ImVec2 from x and y and applying this as a vector operation.
o.o[i]=m+V(x,y)*z;
}
// We now calculate the normal of the quad and whether the quad is facing towards or away from us. These values are used for lighting.
// We calculate the 2d direction of the current cross-section edge rotated by 90 degrees
x=X[1]-X[-1];
y=*X-X[-2];
z=0;
// we figure out the length of this edge (we know z is zero so we can ignore the z component in this calculation)
l=sqrt(x*x+y*y);
// We apply the cross-section sweep rotation and animation rotation to our normal stored in x,y,z
// (we add .5/J here to our sweep rotation factor as we want the normal to be in the "middle" of the face)
T((j+.5)/J);
// We calculate N dot L for our Quad Normal and Light direction and invert this if the Quad Normal is facing away from the light
// This is all made a lot shorter as our light is coming from above, N dot L is just the normal Y component :)
// Most of this line is the two-sided lighting test.
// our rotated normal is in x,y,z and we stored a backup of the vector from the viewer to one of the quad vertices in e,f,g
// So the first part is checking the sign of (Normal dot View direction)
// finally we divide by l which was the length of the normal we calculated earlier (this is unaffected by rotation)
o.l=((x*e+y*f+z*g)>0?1:-1)*y/l;
}
}
// This section draws the background sky/ground
// We borrow the input rectangle variables a and b so we dont have to declare new variables
// also, these already have their x components setup as the correct min and max values we want to draw our horizontal rectangles with
// i is the "depth" of the current rectangle we are drawing
// (we loop through 'i' for values from 98 down to 0)
for(i=99;i--;)
{
// we use the y co-ordinate of the previous rectangle we drew as one of the y co-ordinates for our new rectangle
// for our first rectangle, as a is initialized with the minimum extents of our window, this will conveniently draw a rectangle over the whole window
// we use this to draw the "sky" by filling this with a special case colour
b.y=a.y;
// now we calculate the new y co-ordinate of the rectangle
a.y=m.y+9e2/(i+1);
// draw the rectangle
d->AddRectFilled(a,b,
// here we change the colour to the sky colour if it is the first rectangle in the loop (==98)
i>97 ? 0xffffd050
// otherwise we apply some alternating green stripes and "fog"
// 65793 is 0x10101 in hex. We multiply this by a value and add it to the base colour to add the value to all three channels
: 0xff608000+(i/9&1)*64+i*65793);}
// Now we draw our list of quads, sorted from back to front
// each time around the loop we will figure out the quad with the smallest positive "depth" value (actually 1/depth), draw that quad, then set its depth value to -1
// 'c' is the smallest depth we have found so far and 'j' is the index of the quad with the smallest depth
// this is basically an infinite loop but we initialize c to a large value and j to -1
for(;c=9e9,j=-1;)
{
// for each quad
// (we loop through 'i' for values from n-1 to 0)
for(i=n;i--;)
{
// get the quad's depth sort value into z to save characters
z=q[i].d;
// if the depth is positive and less than the smallest depth so far
if(z>0&z<c)
{
// update the smallest depth and the smallest index
c=z;
j=i;
}
}
// If we found no quads to draw then we have drawn them all so break out of our infinite loop and exit the function
if(j<0) break;
// get the quad structure into o to save characters
Q& o=q[j];
// set the depth value for this quad to -1 so we don't draw it again
o.d=-1;
// get the quad's N dot L value into 'l' ( expected to be in 'l' by the function L() )
l=o.l;
// Calculate the color to draw the polygon by calling L for each colour channel and draw the Quad Polygon
// o.o is our output vertex list. The vertex count is always 4
d->AddConvexPolyFilled(o.o,4,ImColor(L(.1),L(.3),L(1),.8));
// Draw a black outline around the quad
// ~0 is 0xffffffff if we shift this left by 24 we get 0xff000000 which is 1.0 alpha and 0 blue, green and red
d->AddPolyline(o.o,4,~0<<24,1,1);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment