tmpvar.com wrench logo

POC / Shell Texturing

Inspiration

acerola's youtube video How Are Games Rendering Fur?

How it works

schematic sketch of shells

The basic idea behind shell texturing is:

  1. Render multiple mesh instances, offsetting each vertex by the normal based on the instance count and shell spacing. For example:
    vec3 actualPosition = vertexPosition + normal * instanceIndex * shellSpacing
    
  2. Output color where the current pixel is inside of the hair, otherwise discard the pixel. For example:
    const float divisions = 10.0;
    vec2 localUV = fract(uv * divisions) * 2.0 - 1.0;
    float p = float(instanceIndex) / float(instanceCount)
    // compute distance to cone
    float d = length(localUV - 0.5) - (1.0 - p);
    
    // test if inside the cone
    if (d < 0.0) {
      gl_FragColor = vec4(1.0);
    } else {
      discard;
    }
    
    which ends up looking like this:
    example cone output

Demo

shell count
shell spacing
shell subdivisions
mesh

This demo requires WebGPU - in other words, you should open this page in Chrome or Edge.

source: shell-texturing.js

Downsides

  • Viewing the slices at the wrong angles cause major artifacting. This can be avoided on geometry likes planes, but complex meshes might be hard to hide the artifacts without further effort.

  • Mesh UVs can cause discontinuities at the seams for programatically generated textures. One potential solve for this is using the normal and computing the UV from that, depending on the mesh this might also contain artifacts: see the sphere mesh above when viewed from the top.

    vec3 basisUp = vec3(0.0, 1.0, 0.0);
    // swap the up vector if our normal is (0, 1, 0) - assuming y-up
    if (abs(dot(normal, basisUp)) >= 0.999) {
      basisUp = vec3f(1.0, 0.0, 0.0);
    }
    
    vec3 right = cross(normal, basisUp);
    vec3 up = cross(forward, right);
    
    vec2 uv = vec2(
      dot(right, uvw),
      dot(up, uvw),
    );
    

Further Reading