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?
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);