Search code examples
typescriptthree.jsglslshadervertex-shader

Updating a GLSL uniform variable in an animation within three.js with typescript not working


I have a three js, typescript application, within it I use my own custom GLSL shaders. Throught designing the shaders I used the THREE.ShaderMaterial() class but now I want to refactor my code so that I can use the THREE.MeshStandardMaterial class instead.

This is my first time doing something like this but eitherway, everything in the refactor went well, apart from updating the uTime uniform in the animation loop like this:

material.userData.shader.uniforms.uTime.value = timeDivided;

since now I am getting this error:

index.ts:71 Uncaught TypeError: Cannot read properties of undefined (reading 'uniforms')
    at animate (stackOverflowQuestion.ts:71:28)
    at onAnimationFrame (three.module.js:28991:1)
    at onAnimationFrame (three.module.js:13332:1)
animate @ stackOverflowQuestion.ts:71

Here is the full app code simplified:

index.ts:


    import * as THREE from "three";
    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
    import vertexShaderParse from "./shaders/vertexParse.glsl";
    import vertexMain from "./shaders/vertexMain.glsl";
    import textureImage from "./assets/images/test-image-1.jpeg";
    
    // renderer
    const renderer = new THREE.WebGLRenderer();
    renderer.shadowMap.enabled = true;
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    // scene
    const scene = new THREE.Scene();
    scene.background = new THREE.Color("#AADEF0");
    
    // camera
    const camera = new THREE.PerspectiveCamera(
      60,
      window.innerWidth / window.innerHeight,
      0.1
    );
    camera.position.set(0, 5, 5);
    scene.add(camera);
    
    // ambient light
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
    scene.add(ambientLight);
    
    // directional light
    const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
    dirLight.shadow.camera.lookAt(0, 0, 0);
    scene.add(dirLight);
    
    // orbit controls
    const orbitControls = new OrbitControls(camera, renderer.domElement);
    orbitControls.target.set(0, 0, 0);
    
    // main object using the shaders
    const geometry = new THREE.IcosahedronGeometry(1, 250);
    const material = new THREE.MeshStandardMaterial();
    material.onBeforeCompile = (shader) => {
      // storing a reference to the shader object in userData
      material.userData.shader = shader;
      // setting uniforms
      shader.uniforms.uTime = { value: 0 };
      shader.uniforms.uRadius = { value: 0.8 };
      shader.uniforms.uTexture = {
        value: new THREE.TextureLoader().load(textureImage),
      };
    
      const parseVertexString = `#include <displacementmap_pars_vertex>`;
      shader.vertexShader = shader.vertexShader.replace(
        parseVertexString,
        parseVertexString + vertexShaderParse
      );
    
      const mainVertexString = `#include <displacementmap_vertex>`;
      shader.vertexShader = shader.vertexShader.replace(
        mainVertexString,
        mainVertexString + vertexMain
      );
    };
    
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    
    const animate = (time: number) => {
      const timeDivided = time / 10000;
      // this is the part that doesn't work ↓, uncommenting this causes the application to break
      material.userData.shader.uniforms.uTime.value = timeDivided;
      renderer.render(scene, camera);
    };
    renderer.setAnimationLoop(animate);

vertexMain.glsl:


    uniform float uTime;
    uniform sampler2D uTexture;
    
    varying float vDisplacement;
    
    float getWave(vec3 position) {
      return clamp(smoothstep(-0.1, 0.40, abs(mod(position.y * 5., 1.) - 0.5)), 0.4, 1.0);
    }

vertexMain.glsl:


    vec3 image = texture2D(uTexture, uv).xyz;
    
    vec3 coords = normal;
    coords.y += uTime;
    coords += desaturatedImage / 5.;
    float wavePattern = getWave(coords);
    vDisplacement = wavePattern;
    
    // varyings
    float displacement = vDisplacement / 5.;
    vec3 displacedNormal = normalize(objectNormal) * displacement;
    
    transformed += displacedNormal;

How should I fix this so that I can animate the uTime uniform?


Solution

  • You access material.userData.shader in animate() before the first render. At that point, the onBeforeCompile() has never been executed and hence material.userData.shader is undefined. You should be able to fix the issue like so:

    const shader = material.userData.shader;
    
    if ( shader !== undefined ) {
    
       shader.uniforms.uTime.value = timeDivided;
    
    } 
    
    renderer.render(scene, camera);