256 bytes JavaScript signed distance field raymarcher using 2D Canvas. TEA STORM won at Function 2013
You've seen distance field raymarching here before with JSpongy, Hypersonic Mandelbulb and ANDES to name a few. This technique championned by Iñigo Quilez of Elevated and Brave fame is perfect to render complex shapes in a few kilo bytes or even hundred bytes, but only a handful of size optimizers extraordinaire did raymarching in 256 or even 128 bytes.
Source code
<body onload=setInterval("for(t-=.1,x=h,c.height=300,Q=Math.cos;x--;)for(y=h;y--;c.getContext('2d').fillRect(x*4,y*4,N,N))for(N=D=4;X=D*x/h-D/2,Y=D*y/h-D/2,Z=D/2-9,D+=d=(X*X+Y*Y*Q(t/6+Q(D-X-Y))+Z*Z)/9-1+Q(X+t)*Q(Y-t),.1<d*N;)N-=.1",t=h=75)><canvas id=c>
And voilà, 253 bytes of raymarching goodness.
How does it work ?
This time, this is the real deal: No fixed step raymarching but a distance function that gives an estimate of how far is the surface of the object at each point in 3D space and the rays march on until they get close enough.
The setup and loop
That part is trivial: A canvas
, a body
element with onload
event setting a timer that clears the canvas, adjust the time variable, go through each pixel and render them. Just make sure to reuse variables where possible, set the right properties and create alias variables where necessary.
<body onload=setInterval("for(t-=.1,x=h,c.height=300,Q=Math.cos;x--;)for(y=h;y--;/* draw */)/* compute */",t=h=75)><canvas id=c>
Once more with tons of comments:
<body onload=setInterval("/* X loop from 75 to 0 */for(/* adjust the time variable */t-=.1,x=h,/* clear the canvas and size it to 300x300 i.e. 75*4, remember that the default resolution of a Canvas is 300x150 */c.height=300,/* alias for Math.cos */Q=Math.cos;x--;)/* Y loop from 75 to 0 */for(y=h;y--;/* draw the pixel(x,y) */)/* compute the intensity of the pixel(x, y) */",t=h=75)><canvas id=c>
About the resolution
MINI DISTRICT is actually a 2.5D effect; casts 150 rays checking at most 150 position along the rays which amount to 150x150 = 22.500 tests.
TEA STORM on the other hand is purely a 3D effect; casts 75x75 rays with up to 40 checks along the rays which amounts to a maximum of 75x75x40 = 225,000 tests.
That is a 10x more calculations. But the good news is that the distance function helps a lot here and things are not as dire as they seemed.
The 75x75 rays are blown up 4 times to fill up a 300x300 pixels canvas.
The shades of grey
The default fillStyle
of Canvas is black and there is simply no bytes to waste on changing that. Instead the shades of grey are done by drawing axis aligned square with fractional size. This introduces anti-aliasing which results in various shades of grey instead of plain black and white. Simple.
c.getContext('2d').fillRect(x*4,y*4,N,N)
// with N in the range [0; 4]
The camera
This time around, the camera is static. The object evolves over time because we tweak the distance function. The origin of the camera lies in {0, 0, -9} and looks toward {0, 0, 0} like this:
for(x=h;x--;)
for(y=h;y--;/* draw the shade of grey */)
for(N=D=4;
X=D*x/h-D/2,
Y=D*y/h-D/2,
Z=D/2-9,
/* here be dragons and distance function */
The variable D
represents how far we marched along the current camera ray.
The distance function
What you see in TEA STORM is basically a sphere. A crazy twisted sphere, and here is the breakdown of the distance function and how this little monster came to life:
// Sphere
d=(X*X+Y*Y+Z*Z)/9-1
// Sphere morphing into a cylinder
d=(X*X+Y*Y*Q(t/6)+Z*Z)/9-1
// Bumpy sphere
d=(X*X+Y*Y+Z*Z)/9-1+Q(X+t)*Q(Y-t)
// Bumpy sphere-cylinder
d=(X*X+Y*Y*Q(t/6)+Z*Z)/9-1+Q(X+t)*Q(Y-t)
// Bumpy twirling morphing sphere-cylinder \(';;')/
d=(X*X+Y*Y*Q(t/6+Q(D-X-Y))+Z*Z)/9-1+Q(X+t)*Q(Y-t)
The loop for each ray look like this:
for(N=D=4;X=D*x/h-D/2,Y=D*y/h-D/2,Z=D/2-9,D+=d=(X*X+Y*Y*Q(t/6+Q(D-X-Y))+Z*Z)/9-1+Q(X+t)*Q(Y-t),.1<d*N;)
N-=.1
As you can see, N
, the number of iterations, and D
the distance, start at 4. This is a safe distance considering the origin of the camera. At each step, the new position along the camera ray is computed in 3D space in X
, Y
and Z
, the distance to the object is estimated in d
and added to D
, and N
decreased by 0.1.
The loop stops when the condition .1<d*N
is true. This condition does two things:
- Ensure that we stop when
N
reaches 0 or we have reached the object. - The multiplication
d*N
introduces an approximation of the focal distance.
This means that the further we are from the origin of the camera, the bigger the margin of of error we can allow without any visual artefact. Of course this is not entirelly correct since N
does not represent the distance travelled but the number of iterations, but this is good enough.
Karma
TEA STORM won at the 256 bytes intro competition at Function 2013 in Budapest, Hungary on September 14th, 2013. As usual for my demoscene productions, TEA STORM is available on Pouet.net where any comments and gestures are appreciated.
Hope you like the little storm and write up.
Other recent experiments
There are many experiments and projects like TEA STORM to discover other here.
- FRONTFEST MOSCOW It was an honour to be invited to Fronfest Moscow 2017 with the little family to give my first workshop; implementing a Twin-stick shooter using ES6 and Canvas, and to continue my CODE🎙ART series of talks + live coding aiming to inspire new web developer artists. on November 18th, 2017
- VOLTRA VOLTRA: Grinding the Universe, a gritty JavaScript demo, winner of the 1024 bytes demo competition at the Assembly 2017. on August 6th, 2017
- BREATHING EARTH Another take on Nadieh Bremer mesmerizing Breathing Earth visualisation, running at 60fps on a 2D Canvas without libraries or frameworks. on June 26th, 2017
- DEMO REEL AND TINY JAVASCRIPT AT FRONT TRENDS I had the pleasure to speak about creating bite sized audio-visual demos, and LIVE code one at Front Trends 2016 in Warsaw, Poland. on May 19th, 2016
- JAVASCRIPT IS JARIG Javascript is 18 years old! Let's celebrate with a nice little tune. on September 14th, 2013
- MINI DISTRICT How to build a 3D City in 256 bytes with Canvas 2D on August 17th, 2013
- WOLF1K The idea of this entry for the JS1K contest was to do the impossible: a 1K remake of the famous WOLF5K that rocked the final edition of the5K. It does not feature guns, evil grins and violence for in WOLF1K there is no room for guns or any form of violence. on September 10th, 2010
- RUBBER IN SOLID # A crazy twister effect in 256bytes of DHTML on January 6th, 2008
Let's talk
Don't be shy; get in touch by mail, twitter, github, linkedin or pouet if you have any questions, feedback, speaking, workshop or performance opportunity.