Skip to content
Development
Skill

/threejs-materials-lighting

Light and shade a three.js scene: choose materials (MeshStandardMaterial PBR vs unlit MeshBasicMaterial), add ambient/hemisphere/directional/point/spot lights, turn on shadow maps, and use an environment map (IBL) for realistic reflections. Use when a three.js model looks black,

From plugin
awesome-gamedev-agent-skills
6.3k77 skills

Claude Code

$ npx -y skills add gamedev-skills/awesome-gamedev-agent-skills --skill threejs-materials-lighting --agent claude-code

SKILL.md

threejs-materials-lighting.SKILL.md
name: threejs-materials-lighting
description: >
  Lights and shades three.js r187 scenes: standard, physical and unlit materials, lights, shadows,
  HDR environment lighting and tone mapping. Use when a mesh renders black or washed out, shadows
  are missing or full of acne, or EffectComposer darkens the image. Not for renderer setup
  (`threejs-scene-setup`), model loading (`threejs-gltf-loading`) or custom shaders
  (`shader-programming`).

three.js materials and lighting

Makes three.js surfaces read correctly: the right material, a light rig, working shadows and image-based lighting (IBL). Patterns target **r187**. Lighting has been physically based since r155, so intensities from older tutorials look far too dark.

When to use

  • A mesh renders black or flat, a material choice is needed, lights or shadows are being

added, or environment reflections (IBL) are being set up.

  • Code constructs `MeshStandardMaterial`, `DirectionalLight` and similar, or sets

`renderer.shadowMap.enabled`, `scene.environment` or `renderer.toneMapping`.

  • Not for the renderer, camera and loop (`threejs-scene-setup`), loading models

(`threejs-gltf-loading`), or custom GLSL and node shaders (`shader-programming` for the technique).

Workflow

1. **Rule out geometry first.** Swap the material for `MeshNormalMaterial`. If the mesh is still missing, the problem is the camera, scale or loading, not lighting. 2. **Default to `MeshStandardMaterial`.** It reacts to lights and `scene.environment` and matches glTF's PBR model. Use `MeshPhysicalMaterial` only for clearcoat, transmission or sheen, and `MeshBasicMaterial` for unlit UI, sprites and flat colour. 3. **Light the scene**: an environment map or a hemisphere fill, plus one directional key light. Start the key at about 2 to 3. 4. **Turn on shadows in three places**: the renderer, the light, and each mesh's `castShadow` / `receiveShadow`. Then fit the light's shadow camera to the play area. 5. **Tag colour textures sRGB** and leave data maps (normal, roughness, metalness) linear. 6. **Choose a tone mapping curve** once, and finish any post-processing chain with `OutputPass`. 7. **Verify** with [Verify](#verify).

Patterns

1. PBR material under a key and fill

import * as THREE from 'three';

const material = new THREE.MeshStandardMaterial({
  color: 0xcc4444,
  roughness: 0.5, // 0 = mirror, 1 = matte
  metalness: 0.0, // 0 = plastic, wood, skin; 1 = bare metal
});
scene.add(new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), material));

scene.add(new THREE.HemisphereLight(0xbbddff, 0x443322, 1.0)); // sky, ground, intensity
const key = new THREE.DirectionalLight(0xffffff, 2.5);
key.position.set(5, 10, 7);
scene.add(key);

2. Unlit material with a colour texture

const tex = new THREE.TextureLoader().load('assets/logo.png');
tex.colorSpace = THREE.SRGBColorSpace; // colour maps are sRGB
const logo = new THREE.MeshBasicMaterial({ map: tex, transparent: true }); // ignores lights

3. Shadows with a fitted shadow camera

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;      // soft since r182

const sun = new THREE.DirectionalLight(0xffffff, 3);
sun.position.set(8, 12, 6);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);                // default 512
const cam = sun.shadow.camera;                     // orthographic for a DirectionalLight
cam.near = 1; cam.far = 40;
cam.left = -15; cam.right = 15; cam.top = 15; cam.bottom = -15;
scene.add(sun);
// scene.add(new THREE.CameraHelper(cam));         // draw the frustum while tuning

mesh.castShadow = true;
ground.receiveShadow = true;

For a level larger than the shadow frustum, move the light and its `target` with the player each frame instead of growing the frustum, which spreads the same map over more ground. Add the target to the scene (`scene.add(sun.target)`) so its world matrix updates.

4. PBR texture set

const loader = new THREE.TextureLoader();
const colorMap = loader.load('assets/brick_color.jpg');
colorMap.colorSpace = THREE.SRGBColorSpace;
const brick = new THREE.MeshStandardMaterial({
  map: colorMap,
  normalMap: loader.load('assets/brick_normal.jpg'),   // data maps stay linear
  roughnessMap: loader.load('assets/brick_rough.jpg'),
  metalness: 0,
});

5. Image-based lighting from an HDR file

import { HDRLoader } from 'three/addons/loaders/HDRLoader.js'; // RGBELoader before r180

new HDRLoader().load('assets/studio.hdr', (hdr) => {
  hdr.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = hdr; // lights and reflects every PBR material
  scene.background = hdr;  // optional backdrop
});
renderer.toneMapping = THREE.ACESFilmicToneMapping; // default is NoToneMapping
renderer.toneMappingExposure = 1.0;

No HDR file? The reference shows `RoomEnvironment` through `PMREMGenerator`.

Verify

1. `npm run build` (or `npx tsc --noEmit`) passes. 2. Probe the lighting state in the running page. Expose `renderer` and `scene` on `window` (`Object.assign(window, { renderer, scene })`). Then pass this expression to Playwright's `page.evaluate` as a string, so it runs in the page and not in Node (the smoke script in `threejs-scene-setup` works), or ask the user to paste it into the browser console and report the output:

(() => {
  const lights = [], casters = [], srgbMissing = [];
  window.scene.traverse((o) => {
    if (o.isLight) lights.push(`${o.type}:${o.intensity}${o.castShadow ? ':shadow' : ''}`);
    if (o.isMesh && o.castShadow) casters.push(o.name || o.type);
    for (const m of [].concat(o.material ?? [])) {
      if (m.map && m.map.colorSpace !== 'srgb') srgbMissing.push(m.name || m.type);
    }
  });
  return {
    lights,
    environment: !!window.scene.environment,
    shadowMap: window.renderer.shadowMap.enabled,
    shadowCasters: casters.length,
    toneMapping: window.renderer.toneMapping, // 0 = NoToneMa
Read more
@gamedev-skills
Ships withawesome-gamedev-agent-skills

<img src="docs/assets/awesome-gamedev-agent-skills-banner.png" width="820" alt="awesome-gamedev-agent-skills — game-dev skills for AI coding agents.

Stars on GitHub

Momentum

stars in total
6.3k
Star history of gamedev-skills/awesome-gamedev-agent-skills on GitHub, a chart by star-history.com

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc13d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about threejs-materials-lighting.

Which coding agents can use threejs-materials-lighting?
threejs-materials-lighting installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.