When working with VR (Virtual Reality) in Three.js, achieving high performance is crucial to maintaining a smooth and immersive experience. This involves using shaders effectively and optimizing WebGL performance to avoid lag, reduce latency, and maintain a stable frame rate (90+ FPS for VR headsets).
In this article, we’ll explore:
- What are shaders in Three.js and WebGL?
- How shaders affect VR performance?
- WebGL optimizations for VR scenes.
1. What are Shaders in Three.js & WebGL?
Shaders: Custom GPU Programs
A shader is a small program that runs on the GPU (Graphics Processing Unit). Shaders are used to manipulate the appearance of objects, handle lighting, and create effects like reflections, shadows, and distortions.
In WebGL (which Three.js uses under the hood), shaders are written in GLSL (OpenGL Shading Language) and are of two types:
- Vertex Shader – Processes each vertex (position, transformations, etc.).
- Fragment Shader – Calculates the final pixel color (lighting, texture, etc.).
Example: Basic GLSL Shaders
Vertex Shader (Modifies Position of a Vertex)
glsl
Copy
Edit
varying vec3 vColor;
void main() {
vColor = position * 0.5 + 0.5; // Assign color based on position
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Fragment Shader (Controls Final Pixel Color)
glsl
Copy
Edit
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
Shaders are used in Three.js through ShaderMaterial or RawShaderMaterial.
2. How Shaders Affect VR Performance?
In VR, shaders can make or break performance.
- Complex shaders = Slow performance (due to high GPU load).
- Optimized shaders = Smooth VR experience (90+ FPS).
Problems with Heavy Shaders in VR
🚨 High Fragment Shader Complexity:
- Overdraw (rendering too many overlapping pixels).
- Expensive per-pixel computations (e.g., ray marching).
🚨 High Vertex Shader Load:
- Too many vertices (high-poly models).
- Heavy matrix calculations (deforming geometry).
Shader Optimization Tips for VR
✅ Use Fewer Textures & Samples
- Avoid multiple texture lookups.
- Use a single texture atlas instead of many textures.
✅ Limit Per-Pixel Computation
- Use baked lighting instead of dynamic lighting.
- Prefer
MeshBasicMaterial(no lighting calculations).
✅ Use Low-Precision Data
mediumpinstead ofhighpfor mobile VR.
✅ Use Efficient Math
- Replace expensive functions (
sin,cos,pow) with cheaper approximations.
3. WebGL Optimization for VR Performance
A. Reduce Draw Calls
Each object in the scene creates a draw call, which affects performance.
- Merge meshes using
BufferGeometryUtils.mergeBufferGeometries(). - Use Instanced Rendering (
InstancedMesh) for repeated objects.
Example: Instanced Rendering for VR
js
Copy
Edit
import { InstancedMesh, BoxGeometry, MeshStandardMaterial, Matrix4 } from "three";
const count = 1000;
const geometry = new BoxGeometry();
const material = new MeshStandardMaterial({ color: 0x00ff00 });
const mesh = new InstancedMesh(geometry, material, count);
for (let i = 0; i < count; i++) {
const matrix = new Matrix4();
matrix.setPosition(Math.random() * 10, Math.random() * 10, Math.random() * 10);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
B. Use Low-Poly & LOD (Level of Detail)
In VR, reducing polygon count improves FPS.
- Use simpler models (
.glbwith compression). - Implement LOD (Level of Detail) to load lower-poly models when objects are far away.
Example: LOD in Three.js
js
Copy
Edit
import { LOD, Mesh, SphereGeometry, MeshBasicMaterial } from "three";
const lod = new LOD();
lod.addLevel(new Mesh(new SphereGeometry(1, 32, 32), new MeshBasicMaterial({ color: 0xff0000 })), 0);
lod.addLevel(new Mesh(new SphereGeometry(1, 8, 8), new MeshBasicMaterial({ color: 0x00ff00 })), 10);
lod.addLevel(new Mesh(new SphereGeometry(1, 4, 4), new MeshBasicMaterial({ color: 0x0000ff })), 20);
scene.add(lod);
C. Optimize Materials for VR
- Avoid expensive materials (
MeshStandardMaterialwith high roughness). - Use PBR (Physically Based Rendering) only when needed.
- Prefer
MeshBasicMaterialfor static objects.
Example: Using Lightmaps Instead of Realtime Lighting
js
Copy
Edit
const textureLoader = new THREE.TextureLoader();
const bakedLightmap = textureLoader.load("lightmap.jpg");
const material = new THREE.MeshBasicMaterial({ map: bakedLightmap });
D. Use Efficient Shadows
Real-time shadows are expensive. Optimize by:
- Using shadow maps resolution wisely (
WebGLRenderer.shadowMapSize = 1024). - Using soft shadows instead of high-resolution shadows.
- Disabling shadows on non-important objects (
mesh.castShadow = false).
Example: Efficient Shadows in Three.js
js Copy Edit const light = new THREE.DirectionalLight(0xffffff, 1); light.castShadow = true; light.shadow.mapSize.width = 1024; // Lower resolution = better performance light.shadow.mapSize.height = 1024; scene.add(light);
E. Reduce Scene Complexity
- Use fog to hide distant objects (
scene.fog = new THREE.Fog(0x000000, 10, 50);). - Culling: Disable rendering objects outside the camera view.
- Use
frustumCulled = trueto prevent rendering unseen objects.