Materials

Material system with standard PBR materials, custom shaders, and specialized materials for water, foliage, and terrain.

Materials define how surfaces respond to light and appear when rendered. Web Engine supports Three.js materials, custom shaders, and includes specialized materials for common use cases like water, foliage, and terrain.

Standard Materials#

Web Engine supports all Three.js materials with automatic optimization and instancing:

MaterialUse CasePerformance
MeshStandardMaterialPBR materials with realistic lightingMedium
MeshBasicMaterialUnlit materials, UI, effectsFast
MeshPhongMaterialLegacy Phong lightingMedium
MeshPhysicalMaterialAdvanced PBR with clearcoat, transmissionSlow
MeshToonMaterialCartoon/cel-shaded lookFast
ShaderMaterialCustom GLSL shadersDepends on shader

PBR Materials (MeshStandardMaterial)#

The most commonly used material, providing physically-based rendering with metalness/roughness workflow:

import * as THREE from 'three';
// Create a PBR material
const material = new THREE.MeshStandardMaterial({
color: 0x888888, // Base color
metalness: 0.8, // 0 = dielectric, 1 = metal
roughness: 0.3, // 0 = smooth, 1 = rough
// Texture maps
map: diffuseTexture, // Base color texture
normalMap: normalTexture, // Normal map for detail
roughnessMap: roughnessTexture,
metalnessMap: metalnessTexture,
aoMap: aoTexture, // Ambient occlusion
// Additional properties
emissive: 0xff0000, // Self-illumination
emissiveIntensity: 0.5,
envMapIntensity: 1.0, // Reflection strength
});
// Apply to a mesh
mesh.material = material;

Unlit Materials (MeshBasicMaterial)#

Use MeshBasicMaterial for objects that should not be affected by lighting, such as UI elements or emissive effects:

const unlitMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff00,
map: texture,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide, // Render both sides
// Unlit materials ignore lights completely
// Perfect for UI, skyboxes, emissive objects
});

Material Properties#

Color and Texture#

PropertyTypeDescription
colorColorBase color tint (multiplied with map)
mapTextureDiffuse/albedo texture
emissiveColorSelf-illumination color
emissiveMapTextureEmission texture
emissiveIntensitynumberEmission strength multiplier

Surface Properties (PBR)#

PropertyRangeDescription
metalness0-1How metallic the surface is
roughness0-1Surface roughness (0 = mirror)
normalMapTextureNormal map for surface detail
normalScaleVector2Normal map intensity (x, y)
aoMapTextureAmbient occlusion map
aoMapIntensity0-1AO effect strength

Transparency and Blending#

// Transparent material (alpha blending)
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5, // Overall transparency
transmission: 0.9, // Light transmission (glass)
thickness: 0.5, // Thickness for refraction
roughness: 0.05,
metalness: 0.0,
});
// Alpha cutout (for foliage, chain-link fences)
const foliageMaterial = new THREE.MeshStandardMaterial({
map: leafTexture,
alphaMap: leafTexture, // Use RGB as alpha
transparent: true,
alphaTest: 0.5, // Discard pixels below 0.5
side: THREE.DoubleSide,
});

Custom Materials#

Web Engine includes specialized materials for common scenarios:

Water Material

Animated waves, reflection, refraction, and depth-based color

Foliage Material

Vertex wind animation for grass, trees, and vegetation

Terrain Material

Multi-layer blending with height-based texturing

Impostor Material

Billboard rendering for distant LODs

Water Material#

The WaterMaterial provides realistic water rendering with animated waves and Fresnel reflection:

import { WaterMaterial } from '@web-engine-dev/core/engine/materials';
const water = new WaterMaterial({
waterColor: new THREE.Color(0x0077be), // Shallow water
deepWaterColor: new THREE.Color(0x003355), // Deep water
reflectionStrength: 0.6,
waveSpeed: 1.0,
waveScale: 1.0,
waveHeight: 0.2,
normalMap: waterNormalTexture,
});
// Update time uniform each frame
function animate(time) {
water.uniforms.uTime.value = time * 0.001;
}

Water Reflections

For best results, combine WaterMaterial with a reflection probe or planar reflection to capture the environment. The material blends this with Fresnel-based reflection strength.

Foliage Material#

Add wind animation to vegetation using vertex displacement:

import {
createFoliageMaterial,
FoliageUniforms
} from '@web-engine-dev/core/engine/materials';
// Create foliage material from base material
const baseMaterial = new THREE.MeshStandardMaterial({
map: grassTexture,
alphaTest: 0.5,
side: THREE.DoubleSide,
});
const foliageMaterial = createFoliageMaterial(baseMaterial);
// Update wind uniforms
FoliageUniforms.uTime.value = time;
FoliageUniforms.uWind.value.set(1, 0, 0.5); // Wind direction
FoliageUniforms.uWindSpeed.value = 1.0;

Terrain Material#

Multi-layer terrain material with automatic blending based on height and slope:

import { createTerrainMaterial } from '@web-engine-dev/core/engine/materials';
const terrainMaterial = createTerrainMaterial({
layers: [
{
name: 'grass',
diffuse: grassTexture,
normal: grassNormalMap,
heightRange: [0, 50], // 0-50m elevation
slopeRange: [0, 0.3], // Flat to 30% slope
scale: 10, // Texture tiling
},
{
name: 'rock',
diffuse: rockTexture,
normal: rockNormalMap,
heightRange: [30, 100],
slopeRange: [0.3, 1.0], // Steep slopes
scale: 15,
},
{
name: 'snow',
diffuse: snowTexture,
normal: snowNormalMap,
heightRange: [80, 200], // High elevation
slopeRange: [0, 0.4],
scale: 8,
},
],
blendSharpness: 0.5, // Blend transition
});

Material Instancing#

The engine automatically shares material instances to reduce memory and state changes:

import {
getSharedMaterial,
getMaterialStats
} from '@web-engine-dev/core/engine/rendering';
// Automatically share materials with identical properties
const mat1 = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mat2 = new THREE.MeshStandardMaterial({ color: 0xff0000 });
// Get shared instance (reuses mat1 or mat2)
const sharedMat = getSharedMaterial(mat1);
// Check material sharing stats
const stats = getMaterialStats();
console.log(`Sharing ${stats.sharedMaterials} materials`);
console.log(`Reduced to ${stats.poolCount} unique instances`);

Automatic Optimization

Material instancing happens automatically in the rendering pipeline. You don't need to manually call getSharedMaterial unless you're optimizing specific scenarios.

Custom Shader Materials#

Create fully custom materials using GLSL shaders:

const customMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xff0000) },
uTexture: { value: texture },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform float uTime;
uniform vec3 uColor;
uniform sampler2D uTexture;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
// Sample texture
vec4 texColor = texture2D(uTexture, vUv);
// Animated effect
float pulse = sin(uTime + vPosition.y * 2.0) * 0.5 + 0.5;
// Combine
vec3 color = texColor.rgb * uColor * pulse;
gl_FragColor = vec4(color, texColor.a);
}
`,
transparent: true,
side: THREE.DoubleSide,
});

Using Three.js Shader Chunks#

Extend existing materials by injecting custom shader code:

const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
material.onBeforeCompile = (shader) => {
// Add custom uniforms
shader.uniforms.uCustom = { value: 1.0 };
// Inject custom code into vertex shader
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
// Custom vertex displacement
transformed.y += sin(position.x * 0.5 + uCustom) * 0.5;
`
);
// Inject custom code into fragment shader
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`
#include <color_fragment>
// Custom color modification
diffuseColor.rgb *= uCustom;
`
);
};
// Trigger recompilation
material.needsUpdate = true;

Material Variants#

Create material variants for different quality levels or LODs:

import { MaterialVariants } from '@web-engine-dev/core/engine/materials';
// Define material variants
const variants = new MaterialVariants({
high: new THREE.MeshPhysicalMaterial({
map: diffuse4k,
normalMap: normal4k,
roughnessMap: roughness4k,
clearcoat: 1.0,
transmission: 0.5,
}),
medium: new THREE.MeshStandardMaterial({
map: diffuse2k,
normalMap: normal2k,
roughnessMap: roughness2k,
}),
low: new THREE.MeshBasicMaterial({
map: diffuse512,
}),
});
// Switch based on distance or quality settings
const distance = camera.position.distanceTo(mesh.position);
mesh.material = variants.get(distance > 100 ? 'low' : 'medium');

Best Practices#

Performance#

  • Reuse materials across multiple meshes to reduce state changes
  • Use MeshBasicMaterial for unlit objects to skip lighting calculations
  • Disable features you don't need (transparent: false, depthWrite: true)
  • Use texture atlases instead of many small textures
  • Compress textures using KTX2 or Basis Universal format

Visual Quality#

  • Use normal maps to add surface detail without geometry
  • Combine AO maps with lighting for better depth perception
  • Use roughness/metalness maps for realistic material variation
  • Apply proper texture filtering (mipmaps, anisotropic)
  • Match material properties to real-world values for realism

Memory Management#

  • Dispose unused materials: material.dispose()
  • Release textures when not needed: texture.dispose()
  • Use texture compression to reduce GPU memory
  • Share materials between objects instead of cloning
  • Monitor memory with renderer.info.memory

Transparency Sorting

Transparent materials require back-to-front sorting, which can be expensive. Use alphaTest (cutout) instead of full transparency when possible, and minimize overlapping transparent objects.

Rendering | Web Engine Docs | Web Engine Docs