A Material is a plain object with three fields: shader sources, a draw mode and a uniforms object.
const material = {
glsl: { vertex: vertexSource, fragment: fragmentSource },
drawMode: DrawMode.TRIANGLES,
uniforms: { color: Color.fromHex('#ff00ff'), time: 0 },
};
createShaderMaterial() builds one from sources and uniforms;
createDefaultMaterial(), createBasicMaterial(color) and
createNormalMaterial() are ready-made ones. There is no material class and
no lighting model built in: a material is its shader. In three.js terms
everything is a RawShaderMaterial.
glsl is optional and keyed by language on purpose. A WebGL2 renderer reads
material.glsl and throws if it is missing; a WebGPU renderer would read
material.wgsl. One material can carry both and work with either renderer.
The built-in shaders are GLSL ES 3.00 (#version 300 es, in/out, an
explicit out vec4 fragColor). User shaders may still be 1.00
(attribute/varying, gl_FragColor); WebGL2 accepts both.
The renderer compiles and links one program per material and caches it in a
map keyed by the material object. Every mesh that uses the same material
object shares the program. If glsl.vertex or glsl.fragment is replaced
by a different string, the cache notices on the next draw and recompiles.
This makes live-editing a fragment shader a one-line change.
After linking, the renderer asks the program for its active uniforms: the uniforms the shader declares and actually uses (GLSL drops unused ones). For each it stores the location, the declared type and, for arrays, the length. This table drives everything below.
On every draw the renderer walks material.uniforms and, for each name the
program knows, converts the value into a flat number array and uploads it.
Two details make this robust:
uniform3fv for a vec3,
uniform3iv for an ivec3 and uniform3iv again for a bvec3. Choosing by
JavaScript type instead would send a Vector of floats through an integer
setter as soon as the shader declares an ivec3, truncating them
silently; the table in src/webgl/uniforms.ts maps every GLSL type to
the right call.uniformNfv calls are
not free, and a scene with many meshes sharing a material would otherwise
re-upload the same colour hundreds of times per frame. This is also why
mutating a Vector in place works: the comparison looks at the numbers,
not the object identity.Accepted values: number, arrays of numbers (nested arrays are flattened,
so [[1, 2], [3, 4]] fills a vec2[2]), Vector, Color
(uploaded as vec4), Matrix, Mat2/ Mat3/ Mat4,
bigint, and Texture. Array uniforms are addressed by their base
name: a shader's uniform vec3 lights[4] is uniforms.lights = [...] with
twelve numbers.
A Texture value is not a number; a sampler uniform holds a texture
unit index. The renderer assigns units per draw in the order it meets
textures in the uniforms object: the first texture is bound to unit 0 and
the sampler gets 0, the next unit 1, and so on. The texture is uploaded
the first time it is seen (see Textures).
If a shader declares any of modelMatrix, viewMatrix,
projectionMatrix, modelViewMatrix (all mat4) or normalMatrix
(mat3), the renderer sets them per mesh from the scene graph and the
camera, before the material's own uniforms. A material that defines a uniform
of the same name wins, so a shader can still take a hand-built matrix. The
built-in vertex shader uses modelViewMatrix, projectionMatrix and
normalMatrix and passes vPosition (view space), vNormal (view space,
normalised) and vUv to the fragment shader.
The lights of the frame are built-in uniforms too, filled once per frame
from the visible Lights, in view space, with colours premultiplied
by intensity: ambientLightColor (vec3), directionalLightDirections[]
and directionalLightColors[] (vec3 arrays) with
directionalLightCount (int), and pointLightPositions[],
pointLightColors[] (vec3 arrays), pointLightRanges[] (float array)
with pointLightCount. The shader picks the array sizes; the renderer fits
the lights to them and clamps the counts. Lights
explains what the values mean and shows a fragment shader that uses them.
Besides shaders and uniforms, a material carries the GPU state a shader cannot express itself, all optional:
transparent (default false): blend with what is already drawn
(SRC_ALPHA, ONE_MINUS_SRC_ALPHA) instead of overwriting it, and turn
depth writes off unless depthWrite says otherwise. It also decides draw
order - see prepareScene.side (default Side.DOUBLE, no culling): restrict drawing to
Side.FRONT or Side.BACK faces, for closed meshes where the unseen
side is a wasted fragment, or for a single-sided plane meant to be seen
from one direction only.depthTest / depthWrite (default true): whether a fragment is
discarded by what is nearer, and whether it records its own depth.The theory behind blending and culling, and why glTF needs them, is in Material render state.
drawMode is one of the DrawMode strings and says how the GPU
groups vertices into primitives: 'triangles', 'lines', 'points',
the strip and fan variants. It lives on the material rather than the
geometry because it is a property of how something is drawn, and a
wireframe material with 'lines' can be swapped onto any geometry.
in/out.getActiveUniform and the uniform[1234]{f,i,ui}v
family.resolution and time
uniform.