OnBeforeCompile fires only after first render

Hey Everyone!
I create my material before running render, but for some reason I can’t update it in the render function directly, the userData.shader is undefined at first render call, and I need to use an if statement to avoid this issue.

import * as THREE from "three";
import * as dat from "dat.gui";
import Stats from 'three/examples/jsm/libs/stats.module.js'

import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const sizes = {
	width: window.innerWidth,
	height: window.innerHeight
};

const vertShaderSource = document.getElementById("vertexshader").textContent;
const fragShaderSource = document.getElementById("fragmentshader").textContent;

class App {
	constructor() {
		this.width = sizes.width;
		this.height = sizes.height;
		this.container = document.getElementById("app");
		this.scene = new THREE.Scene();
		this.renderer = new THREE.WebGLRenderer();
		this.container.appendChild(this.renderer.domElement);
		this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
		this.renderer.setClearAlpha(0xeeeeee, 1);
		this.renderer.setSize(this.width, this.height);
		this.stats = Stats()
  document.body.appendChild(this.stats.dom)

		this.camera = new THREE.PerspectiveCamera(
			70,
			this.width / this.height,
			0.01,
			1000
		);
		this.camera.position.set(0, 0, 2);
		this.controls = new OrbitControls(this.camera, this.renderer.domElement);
		this.debug = true;

		this.time = 0;
		this.clock = new THREE.Clock();
	
		this.init();
	}

	init() {
		this.addObjects();
		this.addLights();
		this.resize();
		this.render();
		this.setupResize();
		this.setDebug();
	}

	setupResize() {
		window.addEventListener("resize", this.resize.bind(this));
	}

	resize() {
		this.width = this.container.offsetWidth;
		this.height = this.container.offsetHeight;
		this.renderer.setSize(this.width, this.height);
		this.camera.aspect = this.width / this.height;
		this.camera.updateProjectionMatrix();
	}

	addObjects() {
		const that = this
		this.material = new THREE.MeshStandardMaterial({
			onBeforeCompile:(shader) => {
				 		console.log("onBeforeCompile triggered");
						shader.uniforms.time = { value: 0 };
            that.material.userData.shader = shader;
            console.log(this.material); // This should now log properly
        },
		//	side: THREE.DoubleSide,
			// uniforms: {
			// 	time: { value: 0 },
			// 	resolution: { value: new THREE.Vector4() },
			// 	progress: { value: 0 }
			// },
			// vertexShader: vertShaderSource,
			// fragmentShader: fragShaderSource,
			wireframe: true
		});
		
		this.geometry = new THREE.PlaneGeometry(1, 1, 10, 10);

		this.plane = new THREE.Mesh(this.geometry, this.material);
		this.scene.add(this.plane);
	}

	addLights() {
		const light1 = new THREE.AmbientLight(0xffffff, 0.5);
		const light2 = new THREE.DirectionalLight(0xffffff, 0.5);
		light2.position.set(0.5, 0, 0.866); // ~60º

		this.scene.add(light1);
		this.scene.add(light2);
	}

	render() {
	//	console.log(this.material.userData);
		this.time = this.clock.getDelta(); 
		//if(this.material.userData.shader){
			this.material.userData.shader.uniforms.time.value += this.time;
	//	}
		this.renderer.render(this.scene, this.camera);
		this.stats.update();
		window.requestAnimationFrame(this.render.bind(this));
	}
	setDebug() {
		if (this.debug) {
			this.debug = new dat.GUI({ width: 420 });
			const main = this.debug.addFolder("main");
		main.add(this.material.userData.shader.uniforms.progress, "value", 0, 1);
		}
	}
}

new App();

Materials and their respective shaders are created and pushed onto the GPU on demand- so they won’t be compiled unless you render them (mind, you can render them onto a hidden canvas / render target, this will not have any visible effect but the compilation will happen.)

2 Likes

Indeed, silly mistake, thx for helping out!

1 Like

ez mistake. happens to me all the time.

You don’t need the entire shader object. Consider creating an instance of the uniform and then passing that once to the shader uniforms.