const axisAngleRotation = `
mat4 axisAngleRotation(vec3 axis, float angle) {
return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,
const shaderSource = () => {
attribute vec3 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
vec3 noDisplacementNormal
// Find the rotation induced by the displacement
float angle = acos(dot(displacementNormal, noDisplacementNormal));
vec3 rawAxis = cross(displacementNormal, noDisplacementNormal);
if (length(rawAxis) < 0.01) {
vec3 axis = normalize(rawAxis);
mat4 rotation = axisAngleRotation(axis, angle);
// Apply the rotation to the original normal
vec3 normal = (rotation * vec4(origNormal, 0.)).xyz;
vec4 objSpacePosition = vec4(aPosition, 1.0);
const position = ad.vec3Param('objSpacePosition.xyz')
const position3 = ad.vec3(x, y, z)
const time = ad.param('time')
const smoothstep = (edge0, edge1, x) => {
const t = x.sub(edge0).div(edge1.sub(edge0)).clamp(0, 1)
return t.mult(t).mult(t.mult(-2).add(3))
const rotateY = (pos, angle) => {
pos.x().mult(ca).add(pos.z().mult(sa)),
pos.x().mult(sa.neg()).add(pos.z().mult(ca)),
const rotateX = (pos, angle) => {
pos.y().mult(ca).sub(pos.z().mult(sa)),
pos.y().mult(sa).add(pos.z().mult(ca)),
const twistProgressY = smoothstep(
time.mult(0.001).sub(y.mult(1.8))
ad.sin(time.mult(0.00025).add(Math.PI * 1.2))
const twistProgressX = smoothstep(
time.mult(0.001).sub(y.mult(1.8))
ad.sin(time.mult(0.0005).add(Math.PI * 1.8))
twistProgressY.mult(Math.PI * 2)
const xOff = ad.vec3(0, ad.sin(time.mult(0.0001)).mult(0.1), -0.1)
twistProgressX.mult(Math.PI)
const twistOffset = twisted.sub(position)
const offset = twistOffset
offset.outputDeriv('dodx', x)
offset.outputDeriv('dody', y)
offset.outputDeriv('dodz', z)
objSpacePosition.xyz += offset;
vec3 slopeX = dodx + dody + vec3(1.0, 1.0, 0.0);
vec3 slopeYZ = dodz + vec3(0.0, 0.0, 1.0);
vec3 displacementNormal = normalize(cross(slopeX, slopeYZ));
vec3 noDisplacementNormal = normalize(vec3(1.,-1.,0.));
vec3 normal = adjustNormal(
vec4 worldSpacePosition = uModelViewMatrix * objSpacePosition;
gl_Position = uProjectionMatrix * worldSpacePosition;
vPosition = worldSpacePosition.xyz;
vNormal = uNormalMatrix * normal;
uniform sampler2D sphereMap;
uniform sampler2D roughSphereMap;
uniform sampler2D texture;
const float PI = ${Math.PI.toFixed(10)};
float map(float val, float inA, float inB, float outA, float outB) {
return (val - inA) / (inB - inA) * (outB - outA) + outA;
vec4 sampleBackground(vec3 normal, sampler2D bg) {
// x = rho sin(phi) cos(theta)
// z = rho sin(phi) sin(theta)
// rho = 1 after normalization
float phi = acos(normal.y);
? acos(normal.x / sinPhi)
map(theta, 0., PI, 0., 1.),
return texture2D(bg, coord);
vec4 remapShadows(vec4 color) {
float fresnel(vec3 direction, vec3 normal, bool invert) {
vec3 halfDirection = normalize( normal + direction );
float cosine = dot( halfDirection, direction );
float product = max( cosine, 0.0 );
float factor = invert ? 1.0 - pow( product, 5.0 ) : pow( product, 5.0 );
vec3 normal = normalize(vNormal);
vec3 toSurface = normalize(vPosition);
vec3 reflectedDir = normalize(reflect(toSurface, normal));
vec4 baseColor = vec4(0.8, 0.88, 0.95, 1.);
vec4 diffuseColor = sampleBackground(normal, roughSphereMap);
vec4 reflectionColor = remapShadows(sampleBackground(reflectedDir, sphereMap));
float fresnelStrength = 0.1;
float reflectionStrength = 0.5;
float reflectionAmount = reflectionStrength + fresnelStrength * fresnel(toSurface, normal, false);
vec4 mixedColor = baseColor * diffuseColor + reflectionAmount * reflectionColor;
mat4 rotation = axisAngleRotation(normalize(vec3(0., 0.5, 0.25)), 0.5);
vec3 hueShifted = (rotation * vec4(mixedColor.rgb, 0.)).xyz;
gl_FragColor = vec4(abs(hueShifted), 1.);
sphereMap = loadImage('office_spheremap.jpg')
createCanvas(600, 600, WEBGL);
irradianceMap = createGraphics(smallWidth, Math.floor(smallWidth * (sphereMap.height / sphereMap.width)), WEBGL)
irradiance = irradianceMap.createShader(irradianceVert, irradianceFrag)
irradianceMap.shader(irradiance)
irradiance.setUniform('environmentMap', sphereMap)
irradianceMap.rectMode(irradianceMap.CENTER)
irradianceMap.rect(0, 0, irradianceMap.width, irradianceMap.height)
reflection = createShader(...shaderSource())
subdivPlane = makePlane(60);
reflection.setUniform('sphereMap', sphereMap)
reflection.setUniform('roughSphereMap', irradianceMap)
reflection.setUniform('time', millis())