Materials and textures

A material decides how a surface responds to light. The shorthand fields cover most cases. Mat builds a full descriptor when you need one.

import { Mat, Tex } from 'motion-script';

const brushed = Mat.standard({
  color: '#c9ccd4',
  roughness: 0.35,
  metalness: 0.9,
  normalMap: Tex.image('brushed-normal.png', { repeat: [4, 4] }),
});

g3.mesh(Geo.cylinder({ radius: 1, height: 0.2 }), brushed);

Hoist and share materials. One material across many meshes compiles one shader program instead of one per mesh.

Choosing a material

BuilderLitUse for
Mat.standardyesThe default. Physically based, roughness and metalness
Mat.physicalyesClearcoat, transmission, sheen, iridescence. Costs more per pixel
Mat.basicnoFlat colour. Screens, overlays, anything that is its own light source
Mat.lambertyesCheap diffuse, no specular highlight
Mat.phongyesClassic specular highlight via shininess and specular
Mat.toonyesBanded cel shading
Mat.normalnoColours surfaces by their normal. A debugging view
Mat.depthnoColours by distance from the camera
Mat.matcapnoLighting baked into a lookup texture. Cheap and stylised
Mat.pointsnoPoint clouds
Mat.lineBasic / Mat.lineDashednoLines
Mat.spritenoCamera-facing quads
Mat.shadowCatches shadows on an otherwise invisible surface
Mat.shaderRaw GLSL

Reach for Mat.standard by default. Use Mat.physical only when a surface genuinely needs glass, car paint or fabric.

Common fields

Every material takes these:

FieldTypeDescription
opacitynumberAlpha. Needs transparent: true to have an effect
transparentbooleanEnable alpha blending
alphaTestnumberDiscard fragments below this alpha
side'front' | 'back' | 'double'Which faces are drawn
wireframebooleanDraw edges only
blending'normal' | 'additive' | 'subtractive' | 'multiply' | 'none'How the colour combines with the framebuffer
vertexColorsbooleanRead per-vertex colour from the geometry
toneMappedbooleanInclude this material in tone mapping
depthWrite / depthTestbooleanDepth buffer behaviour
visiblebooleanDraw at all

side: 'back' with depthWrite: false is the usual recipe for a translucent shell that should not occlude what is inside it.

Maps

Most lit materials accept the same texture slots:

SlotWhat it does
mapBase colour
normalMapSurface detail, with normalScale
roughnessMap / metalnessMapPer-pixel roughness and metalness
aoMapBaked ambient occlusion, with aoMapIntensity
alphaMapPer-pixel alpha
emissiveMapWhere the surface emits, with emissive and emissiveIntensity
displacementMapMoves vertices, with displacementScale and displacementBias
envMapReflections, with envMapIntensity
lightMapBaked lighting, with lightMapIntensity

Textures

A bare string works anywhere a texture is expected, so Tex.image is only needed when you want sampler options:

.plane({ width: 4, height: 4, map: 'floor.jpg' })
.plane({ width: 4, height: 4, map: Tex.image('floor.jpg', { repeat: [4, 4], wrapS: 'repeat', wrapT: 'repeat' }) })
OptionTypeDescription
wrapS / wrapT'clamp' | 'repeat' | 'mirror'Behaviour past the edge
repeatVector2Tiling
offsetVector2Pan
rotationnumberRotation in degrees
centerVector2Pivot for the rotation
magFilter / minFilter'nearest' | 'linear'Sampling. 'nearest' for pixel art
anisotropynumberSharpness at grazing angles
flipYbooleanFlip vertically
colorSpace'srgb' | 'linear'Use 'linear' for data maps like normals

Images go through the ordinary asset pipeline, so the pixels are resident before the frame that needs them draws.

Tex.data builds a texture from raw RGBA bytes, which is how you make gradient ramps, noise and lookup tables:

Tex.data(bytes, 256, 1, { magFilter: 'linear' })

Tex.surface renders 2D MotionScript content into a texture. See 2D on 3D.

Metals need something to reflect

A standard or physical material with high metalness renders black unless the scene has an environment to reflect. The fastest fix needs no asset:

new Graphics3D()
  .environment({ type: 'room' })
  .sphere({ radius: 1, color: '#c9ccd4', metalness: 1, roughness: 0.15 })

See Environment.

Physical extras

Mat.physical adds the fields that make glass, coated and fabric surfaces:

FieldFor
clearcoat, clearcoatRoughnessCar paint, lacquer
transmission, thickness, iorGlass and liquids
attenuationColor, attenuationDistanceTinting through a volume
sheen, sheenColor, sheenRoughnessCloth
iridescence, iridescenceIORSoap film, oil slick
specularIntensity, specularColorNon-metal specular tint
anisotropy, anisotropyRotationBrushed metal, hair

Raw GLSL

Mat.shader is the full escape hatch:

Mat.shader({
  vertex: vertexSource,
  fragment: fragmentSource,
  uniforms: { uTime: phase(), uColor: [1, 0.3, 0.2] },
})

Uniform values are free to animate. Changing the shader source recompiles the program, so keep the source constant and drive it through uniforms.

Escape hatches, in order

When a descriptor does not expose what you need, work down this list:

  1. The full parameter surface on the named descriptor.
  2. Arbitrary vertex data with Geo.buffer or Geo.parametric.
  3. Raw GLSL with Mat.shader.
  4. A params object, passed straight through to the underlying renderer object.

params is available on geometries, materials, lights, cameras and post effects. It is the last resort, since what it accepts is renderer-specific.