The Legend of IceCoon case study / Advanced webGL - First Part

Samsy
8 min readDec 26, 2016

--

For the end of the year, I enjoyed building a little THREE.JS game for this year’s Christmas Experiments.

http://christmasexperiments.com/2016/20/the-legend-of-the-icecoon/

Christmas experiments have been initiated by David Ronai in 2012, with the goal to deliver great experiments and highlight top web creatives as well as newcomers.
I used a lot of advanced techniques I’d like to share with everyone.
To let everyone know also, this is my first blog post ever !

I’ll try to be descriptive, please do not hesitate to ask any questions if there are any blurry parts.
If I’m wrong about a concept I’ll be more than glad to hear from you :)

( Thank you Alvin for correcting my french-glish )
#EXCUSEMYFRENCH

The technical self-brief

Video Reference I found on the internet from a 3D render

Constructing an infinite diamond tunnel you dive into, with lights, env and normal map / bloom postprocessing / the ability to switch graphics and travel through worlds + cross fade transitions, “trying” to keep it 60 fps on a retina display, even on mobile.

It sounds like a little challenge to me, I’ll go step by step.

And I’ll start with…..

Constructing an infinite tunnel

There’s no secret about the position distribution of the tunnel, it is based on a TubeGeometry. https://threejs.org/docs/#Reference/Geometries/TubeBufferGeometry
The idea is to create a tube geometry from a curve and extract the position of the vertices, place a rotated mesh on each of them, sit and look.

For the camera movement, this has been done before by the excellent zz85

https://threejs.org/examples/#webgl_geometry_extrude_splines

No sorcery there.

The problem is, rendering like this will cost you a lot of draw calls ( number of mesh visible = number of draw calls ), and draw calls is one of the bottlenecks in graphics rendering.

And you would want to save some processing and frame time to render cool fullscreen post processing effects and heavy materials with your cubes.

Geometry Instancing

To reduce the draw call count, we we’ll use a Geometry Instance.
This is not well documented on THREE.JS but you can find an example here.
https://threejs.org/examples/#webgl_interactive_instances_gpu

The idea is to render multiple copies of the same primitive ( or geometry ) in a scene at once, using one single draw call. Geometry Instancing can help us to do that. In our case, render hundreds of instances of the same geometry in a single mesh.

An instanced mesh got some attributes shared across all the geometry, and their own InstancedAttributes which are specific to the current instance.

Attributes
Each instance of the single mesh are all sharing the same set of attributes like, position ( describes the structure of the geometry ) , normals, uvs etc...
Nothing different than the usual buffers here.

InstancedAttributes
And you can have individuals proper attributes like offset ( to displace the geometry in space ), scale, color, ID, so each instance can have a different offset displacement, unique look etc..

In this case, each cube needs to be positioned on each of the tube vertices, using an InstancedBufferAttribute that describes the offset of each cubes.

And the usage in-shader.

An example of Instanced geometries + GPGPU

Now we can render the whole tunnel in a single draw call.

In my case there are 5320 instances ( number of vertices on the tube geometry ) * 12 vertices ( 60 indexed vertices ) = 63840 vertices and as many call to the vertex shader.

I asked myself if I could render only the visible cubes in front of the camera, to reduce the number of instances and not having to renderer all the vertices of the geometry on each frame and reduce the amount of vertex-shader calls.

So I’d want only 1024 mesh in my instanced geometry instead of 5320.

Geometry Texture

As a performance geek, I used a solution that allow me to render what is only visible in front of me in the tunnel using what we call a Geometry Texture.

A geometry texture is a texture that describes one or multiple components of geometry ( vertex positions, uvs ).

Using textures and RGBA components to write data that are not supposed to be displayed on a screen is a well-known technic.

The GPGPU ( General Purpose Graphical Processing Unit ) is a good example of what we can do with Data Textures. You can read more about on the excellent blog post of Nicolas Barradeau. http://barradeau.com/blog/?p=621

The idea is to transpose the vertices positions of the tube geometry into a Data Texture using the RGB component of a texture.

X = R
Y = G
Z = B

In my case, I need to store 5320 positions ( number of positions of the tube geometry ).

To support older GPUs ( not sure it is still the case today, probably not ), to reduce GPU memory consumption, and texture padding in gpu ( you can find some reading and discussion about this )….
… I’ll prefer to store the data in a power of 2 texture.

Disclaimer : I’ll be glad to hear about this if someone knows more..

We’ll determine the lowest size we can use to store theses positions.

128 * 128 = 16384

64 * 64 = 4096

128 * 64 = 8192

16384 is too much, 4096 is too low, 8192 is ok !

And because we are lucky, all the vertices of the geometry are generated inline as a Slinky…

…every positions will be stored as a single line in the dataTexture.

There will be a lot of empty pixels ( 8192 - 5320 = 2872) we’ll just set them to zero because we wont use them anyway.

This array contains the data of our DataTexture.
https://github.com/mrdoob/three.js/blob/master/src/textures/DataTexture.js

Once we got our geometry texture that describes the whole tube geometry, we’ll send this texture as a sampler2D in the shader.

Here how a dataTexture looks :

PS : You can have a look at the excellent FBOHelper and inspector made by Jaume Sanchez Elias
https://github.com/spite/THREE.FBOHelper

And how ours looks like :

Reading the Geometry Texture in shader

We want each instances to be unique, and let them know they are unique.
For that we’ll just set an unique ID to each instances in an InstancedAttribute. 0, 1, 2 …. , 1022, 1023.

So in the shader, each instance has now a unique ID that we can now access in this attribute.

We’ll need to send the DataTexture

We’ll need to set the data texture size as a uniform. [128, 64]

We’ll need the vertices length that has been transposed into the texture [ 5320 ].

The camera is moving, and looping inside the tube.
As a reminder :
https://threejs.org/examples/#webgl_geometry_extrude_splines
The shader needs to know whats the current progression of the camera inside the tube ( beginning is 0, end is 1 ).

The idea, is to displace the instances unique ID with the current progression of the camera in the tunnel.

Transform the 1 dimension displacedID coordinate into a 2D normalized row, col, based on the texture dimension.

Then read the dataTexture from this coordinate.

To show you how it works, here’s what I can see if I display the uvToRead as a color.
We can clearly see the red channel / X uv coordinate looping from 0 to 1, and the shade of the green channel / Y uv coordinate incrementing on the progression.

Because… The X value of the uvToRead is looping from 0 to 1 on a 128 indices basis, and every 128 index, the Y value is incrementing a bit.

Here’s a capture of how it looks when the progression loops !

I now have a geometry of 1024 instances that morphs and changes position on its own, one draw call, with a single material.
The only and single thing left to make it work now is to update the currentLoopDisplacement value from 0 to 1 according to the camera position.
The mesh is completely independent, theres no attributes update / cpu workload / no data or textures upload to the GPU = HUGE WIN.

This is incredibly performant, and runs really smoothly on a lot of computers / GPU / Mobile. Even a mac book air 13'’ with a cheap chipset / iPhone5. I just saved some processing and can concentrates on the look and feel on the experience !

To be continued… ( Post processing + cross fade ).

For the time being you can visit my portfolio :

http://samsy.ninja

Or / And / Follow me on twitter :)

https://twitter.com/Samsyyyy

I’m also free-lancer working on webGL projects for production studios or other independents. But can look for legendaries full-timer positions.

And big kudos for my bro, favorite designer, and one of my best friends :

http://stevenmengin.com/

https://twitter.com/steven_mengin

--

--

Samsy

Freelancer Technologist and Creative Developer. 3D / Webgl / WebXR / WebAR / Installations