2. Geometrías, materiales, luces y sombras
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: la página de la lección src/02-materials-lights/main.ts, los recuentos de las geometrías y los valores por defecto de los materiales en scripts/l02-geometries.ts, y los ejercicios en scripts/l02-exercises.ts.
La escena
Sección titulada «La escena»La lección 1 dibujó un cubo con un material que no necesita luz. Esta lección construye un trozo de mástil de guitarra sobre una mesa: un diapasón de palisandro, cinco trastes de níquel, un punto de nácar, una luz de cielo y un sol que proyecta sombras. Cada pieza es una primitiva de three.js.

Página en vivo: ejecutarla aquí
Abrir a pantalla completa. Pruébalo: desmarca neck casts a shadow para la escena del ejercicio 3 (?neckshadow=off), o shadows para ver lo que aporta el shadow map. La escena del estudio de la lección 7 pone una guitarra entera bajo un foco con sombras suaves.
La captura de pantalla viene de node scripts/probe.mjs 02-materials-lights --shot out/neck.png, en Chromium headless en la máquina del autor, con WebGPU.
| WPF 3D | MonoGame | three.js | |
|---|---|---|---|
| Geometría | MeshGeometry3D: Positions, Normals, TextureCoordinates, TriangleIndices |
VertexBuffer e IndexBuffer |
BufferGeometry: atributos con nombre y un índice |
| Formas integradas | ninguna | ninguna | caja, plano, esfera, cilindro, toro y más |
| Material | DiffuseMaterial, SpecularMaterial, EmissiveMaterial |
BasicEffect: difuso, especular, tres luces direccionales |
MeshStandardMaterial, MeshPhysicalMaterial: metalness y roughness |
| Luces | AmbientLight, DirectionalLight, PointLight, SpotLight |
lo que calcula el efecto | las mismas cuatro, más HemisphereLight, en unidades físicas |
| Sombras | ninguna | tu propio shadow map | shadow maps, activados por renderer, por luz y por objeto |
Qué contiene una geometría
Sección titulada «Qué contiene una geometría»Una BufferGeometry es un conjunto de arrays tipados que el renderer sube a la GPU tal cual: un atributo por propiedad de vértice, position, normal y uv, y un índice opcional que enumera los triángulos por número de vértice. Las primitivas solo rellenan esos arrays. scripts/l02-geometries.ts imprime lo que contienen las geometrías de la escena, sin renderer:
$ node scripts/l02-geometries.tsBoxGeometry(1, 1, 1) position 24×3, normal 24×3, uv 24×2; index 36; triangles 12; groups 6PlaneGeometry(8, 8) position 4×3, normal 4×3, uv 4×2; index 6; triangles 2; groups 0CircleGeometry(0.06, 32) position 34×3, normal 34×3, uv 34×2; index 96; triangles 32; groups 0CylinderGeometry(0.012, 0.012, 0.55, 12) position 76×3, normal 76×3, uv 76×2; index 144; triangles 48; groups 3SphereGeometry(1, 32, 16) position 561×3, normal 561×3, uv 561×2; index 2880; triangles 960; groups 0normals at the corner (0.5, 0.5, 0.5): (1, 0, 0) (0, 1, 0) (0, 0, 1)MeshStandardMaterial: { color: 'ffffff', roughness: 1, metalness: 0 }MeshPhysicalMaterial: { ior: 1.5, reflectivity: 0.5, clearcoat: 0, transmission: 0 }PointLight with power 800 lm: { intensity: 63.66, decay: 2, distance: 0 }- Un cubo tiene 24 vértices, no 8. Un vértice es una posición y una normal y una UV. Cada esquina pertenece a tres caras que apuntan en tres direcciones, así que la caja la guarda tres veces, como muestran las últimas líneas para la esquina (0.5, 0.5, 0.5). El
MeshGeometry3Dde WPF funciona igual: compartir una posición entre caras comparte su normal, y las aristas se ven redondeadas. - Los grupos dividen una geometría en partes que pueden llevar materiales distintos: una caja tiene uno por cara, un cilindro uno para su lateral y uno para cada tapa. Pasar un array de materiales a un
Meshlos utiliza. - Los parámetros que siguen al tamaño son segmentos.
CylinderGeometry(…, 12)tiene 12 lados, suficientes para un traste así de fino visto a distancia; los 32 segmentos de la incrustación son para su borde redondo. Los segmentos cuestan triángulos, y los triángulos son lo que cuentarenderer.info. SphereGeometry(1, 32, 16)tiene 32 × 16 cuadriláteros, lo que daría 1 024 triángulos, pero informa de 960: en cada polo, una fila de cuadriláteros se reduce a triángulos, y las mitades degeneradas se omiten (ejercicio 1).- Un
PlaneGeometryestá en el plano XY, orientado hacia +Z. La mesa se gira −90° alrededor de X para mirar hacia arriba; una cara frontal vista desde detrás no se dibuja, salvo que el material fijeside: THREE.DoubleSide.
Materiales basados en la física
Sección titulada «Materiales basados en la física»El mástil, los trastes y el punto usan dos materiales (main.ts, líneas 31-63):
// El mástil: el palisandro es un dieléctrico (metalness 0) con una superficie rugosaconst neck = new THREE.Mesh( new THREE.BoxGeometry(4, 0.12, 0.55), new THREE.MeshStandardMaterial({ color: 0x4a2c1d, metalness: 0, roughness: 0.75 }),);neck.position.y = 0.3;// ?neckshadow=off: el mástil recibe sombras pero no proyecta ninguna (ejercicio 3)neck.castShadow = params.get('neckshadow') !== 'off';neck.receiveShadow = true;scene.add(neck);
// Trastes: alpaca, un metal, así que metalness 1 y el color tiñe los reflejosconst fretMaterial = new THREE.MeshStandardMaterial({ color: 0xc9c5bd, metalness: 1, roughness: 0.3 });const fretGeometry = new THREE.CylinderGeometry(0.012, 0.012, 0.55, 12);for (let fret = 1; fret <= 5; fret++) { // Cada traste está a tiro de escala × (1 − 2^(−n/12)) de la cejuela, aquí con un tiro de escala de 6.5 unidades const x = -2 + 6.5 * (1 - 2 ** (-fret / 12)); const mesh = new THREE.Mesh(fretGeometry, fretMaterial); mesh.rotation.x = Math.PI / 2; mesh.position.set(x, 0.37, 0); mesh.castShadow = true; scene.add(mesh);}
// Una incrustación: nácar, con una capa transparente que MeshPhysicalMaterial añade sobre el modelo estándarconst inlay = new THREE.Mesh( new THREE.CircleGeometry(0.06, 32), new THREE.MeshPhysicalMaterial({ color: 0xf2efe6, roughness: 0.4, clearcoat: 1, clearcoatRoughness: 0.05, iridescence: 0.6 }),);inlay.rotation.x = -Math.PI / 2;// Entre el segundo y el tercer traste, donde una guitarra tiene su primer puntoinlay.position.set(-1.13, 0.361, 0);scene.add(inlay);El DiffuseMaterial de WPF y el BasicEffect de MonoGame usan el modelo de iluminación clásico: un color difuso, y un color especular con una potencia que fija el tamaño del brillo. Los valores se ven bien bajo una luz y mal bajo otra. MeshStandardMaterial está basado en la física: describe la superficie, y el renderer deduce cómo se refleja la luz en ella. El modelo es el modelo metallic-roughness de glTF 2.0 (lección 4), así que un material exportado desde Blender llega con los mismos valores.
metalnessvale 0 para un dieléctrico, como la madera, el plástico, la piel o la piedra, y 1 para un metal. Un dieléctrico muestra su color con luz difusa y refleja un poco de luz blanca en ángulos rasantes; un metal no tiene parte difusa, y su color tiñe el reflejo. Los valores intermedios son para transiciones, como el óxido sobre el metal, no para materiales “ligeramente metálicos”.roughnessva de 0, un espejo, a 1, una superficie mate. Difumina el reflejo y ensancha el brillo a la vez, en lugar de la potencia especular separada del modelo clásico.colores el albedo, el color de la superficie sin iluminación: los valores hexadecimales son sRGB, y el renderer los convierte a valores lineales, como mide la lección 3.- Los valores por defecto impresos arriba son un dieléctrico blanco y totalmente rugoso:
color: 'ffffff', roughness: 1, metalness: 0.
MeshPhysicalMaterial amplía el material estándar con capas para las superficies que el estándar no puede describir: clearcoat para el barniz o la pintura de coche, iridescence para el arcoíris del nácar o de las pompas de jabón, sheen para los tejidos, transmission para el vidrio. Cada capa cuesta trabajo de shader, así que úsala donde se note.
Con WebGPURenderer, ambos materiales se convierten internamente en materiales de nodos, MeshStandardNodeMaterial y MeshPhysicalNodeMaterial, que la lección 6 amplía con TSL.
Luces en unidades físicas
Sección titulada «Luces en unidades físicas»// Una luz de cielo y suelo rellena las sombras; una luz direccional, como el sol, las proyectascene.add(new THREE.HemisphereLight(0xdde6ff, 0x302820, 1.2));const sun = new THREE.DirectionalLight(0xffffff, 3);sun.position.set(2, 4, 3);- Una
DirectionalLightenvía rayos paralelos desde su posición hacia su objetivo, el origen por defecto, como el sol. Solo cuenta la dirección, no la distancia. - Una
HemisphereLightmezcla un color de cielo desde arriba con un color de suelo desde abajo, según la orientación de cada superficie. Es un sustituto barato de la luz que devuelve una habitación real; la lección 3 la reemplaza por un mapa de entorno. - Una
PointLightilumina en todas las direcciones desde un punto, y unaSpotLighten un cono. Su luz disminuye con el cuadrado de la distancia (decay: 2), ydistance: 0significa sin corte.
Desde r155, three.js usa unidades físicas por defecto (guía de migración), y el código fuente de r186 ya no tiene el interruptor useLegacyLights que recuperaba el comportamiento antiguo. La intensidad de una luz puntual o de un foco está en candelas, y su propiedad power convierte desde lúmenes: 800 lm, una bombilla doméstica corriente, son 800 / 4π ≈ 63.66 cd, como imprimió el script. Las luces direccionales y hemisféricas no tienen una posición desde la que disminuir; su intensidad se aplica igual en todas partes. Las unidades de la escena importan entonces: la atenuación con el cuadrado de la distancia trata una unidad como un metro, así que una escena modelada en centímetros necesita luces 10 000 veces más fuertes para verse igual (por verificar en una escena propia).
Sombras
Sección titulada «Sombras»Las sombras se activan explícitamente en tres niveles, y hacen falta los tres:
renderer.shadowMap.enabled = shadows;renderer.shadowMap.type = THREE.PCFShadowMap;sun.castShadow = true;sun.shadow.mapSize.set(1024, 1024);// La cámara de sombras es ortográfica: la caja que cubre es el único lugar donde pueden caer sombrasObject.assign(sun.shadow.camera, { left: -3, right: 3, top: 3, bottom: -3, near: 0.5, far: 12 });sun.shadow.camera.updateProjectionMatrix();y cada malla indica si proyecta sombras, si las recibe, o ambas cosas (castShadow, receiveShadow).
Un shadow map es una imagen de profundidad renderizada desde la luz: antes de dibujar la escena, el renderer dibuja los objetos que proyectan sombra en una textura, aquí de 1024 por 1024 píxeles, desde el punto de vista del sol, guardando solo su distancia. Después, para cada píxel de una superficie receptora, el material compara su propia distancia a la luz con la guardada en el mapa: si algo estaba más cerca, el píxel está en sombra. Para una luz direccional, la “cámara” de ese render es ortográfica, y la caja left, right, top, bottom, near, far es todo lo que ve: una sombra fuera de la caja no se dibuja, y una caja más grande que la escena reparte los 1024 píxeles sobre espacio vacío y hace la sombra pixelada. La caja por defecto mide 10 unidades de ancho, (-5, 5, 5, -5, 0.5, 500) en DirectionalLightShadow.js.
WebGPURenderer actualiza la proyección de la cámara de sombras una sola vez, cuando construye la sombra (ShadowNode.js, línea 379); un cambio hecho después del primer frame necesita updateProjectionMatrix(), y el curso lo llama siempre.
Lo que cuestan las sombras
Sección titulada «Lo que cuestan las sombras»La página informa de sus contadores con y sin sombras (?noshadows):
$ node scripts/probe.mjs 02-materials-lights{ "backend": "WebGPU", "gpu": "nvidia, blackwell", "frame": 4, "render": { "calls": 9, "drawCalls": 15, "triangles": 539 }, "memory": { "geometries": 5, "textures": 5, "programs": 10 }, "shadows": true, "objects": { "Scene": 1, "Mesh": 8, "HemisphereLight": 1, "DirectionalLight": 1 }}$ node scripts/probe.mjs 02-materials-lights --query noshadows{ ... "render": { "calls": 6, "drawCalls": 9, "triangles": 287 }, "memory": { "geometries": 5, "textures": 3, "programs": 6 }, "shadows": false, ...}Sin sombras: 8 mallas y el pase de salida, 9 draw calls, y 2 + 12 + 5 × 48 + 32 + 1 = 287 triángulos. Con sombras, los seis objetos que proyectan sombra se dibujan una segunda vez en el shadow map: 6 draw calls más y 12 + 240 = 252 triángulos más. calls, que cuenta los renders desde el inicio (lección 1), pasa de 2 por frame a 3: el shadow map es un render en sí mismo. El shadow map también añade texturas y sus propios programas de shader. La mesa y la incrustación no proyectan sombra y no se dibujan en el mapa. Cada luz que proyecta sombras añade un pase como este, y una luz puntual añade seis, uno por cara de un cubo, por eso las escenas en tiempo real se limitan a una o dos luces con sombras.
PCFShadowMap filtra el borde de la sombra comparando varias muestras del mapa. r186 eliminó PCFSoftShadowMap de WebGPURenderer: la constante sigue existiendo, marcada como obsoleta en constants.js, y un renderer que la recibe avisa WebGPURenderer: PCFSoftShadowMap has been removed. Using PCFShadowMap instead. (Renderer.js, líneas 902-906). VSMShadowMap sigue disponible para sombras más suaves.
En GuitarAlchemist/ga
Sección titulada «En GuitarAlchemist/ga»El diapasón de GA en 05c8eda ilumina el mástil con una luz ambiental, cinco luces direccionales y una luz puntual. La luz principal proyecta sombras, con una caja de sombra de 60 por 60 unidades, donde una unidad son 10 mm (scaleLength / 10, un tiro de escala de 65 unidades), un mapa de 1024 por 1024, y un bias contra el shadow acne, las rayas que aparecen en una superficie cuando se sombrea a sí misma:
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.3); directionalLight.position.set(15, 30, 15); directionalLight.castShadow = true; directionalLight.shadow.camera.near = 0.1; directionalLight.shadow.camera.far = 100; directionalLight.shadow.camera.left = -30; directionalLight.shadow.camera.right = 30; directionalLight.shadow.camera.top = 30; directionalLight.shadow.camera.bottom = -30; directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.mapSize.height = 1024; directionalLight.shadow.radius = 4; directionalLight.shadow.bias = -0.0005;Lo que esta lección añade a ese código:
- Las sombras son solo para
WebGLRenderer. La lección 1 mostró que el diapasón las activa, comoVSMShadowMap, solo cuando el renderer es unWebGLRenderer(líneas 207-211). ConWebGPURenderer,renderer.shadowMap.enabledse queda en false, y los ajustes de sombra de arriba no hacen nada. - La luz puntual apenas cuenta.
new THREE.PointLight(0xffffff, 0.6, 20)en (0, 3, 0) son 0.6 cd: 3 unidades por debajo, sobre el mástil, 0.6 / 3² ≈ 0.07, frente a 1.3 de la luz principal, alrededor del 5 %. Condistance: 20, las partes del mástil a más de 20 unidades no reciben nada. Los valores parecen escritos antes de r155, cuando las intensidades no eran físicas. - Una textura cumple cuatro papeles. El
MeshStandardMaterialdel diapasón (líneas 805-819) usa la mismawoodTexturecomomap,roughnessMap,normalMapybumpMap. Un mapa de rugosidad lee su canal verde como rugosidad, y un mapa de normales lee sus canales rojo, verde y azul como una dirección: una imagen de color de madera no da ni lo uno ni lo otro. La lección 3 explica por qué una imagen de color y una imagen de datos también deben diferir en espacio de color. - Las cuerdas entorchadas usan
MeshPhysicalMaterialconmetalness: 1,clearcoat,sheen, un mapa de normales y un mapa de normales para la capa transparente (líneas 761-774): tres capas de shading en cada cuerda, cada una pagada en cada píxel que cubren las cuerdas.
Experimento GA
Sección titulada «Experimento GA»El experimento 8 de la lección 14 pone sombras bajo las cuerdas de un mástil entero: un shadow map de 4096 cuesta más o menos lo mismo que uno de 1024, un mástil inmóvil puede renderizar su sombra una sola vez con shadow.autoUpdate = false, y una textura precalculada elimina incluso eso.
Puntos clave
Sección titulada «Puntos clave»- Una
BufferGeometryson arrays tipados: un vértice es una posición, una normal y una UV juntas, así que las aristas vivas duplican vértices. MeshStandardMaterialdescribe la superficie conmetalness(0 o 1) yroughness;MeshPhysicalMaterialañade capa transparente, sheen, iridiscencia y transmisión.- Las intensidades de las luces son físicas desde r155: candelas para las luces puntuales y los focos, con
poweren lúmenes, y la atenuación trata una unidad como un metro. - Las sombras necesitan el renderer, la luz y cada malla; cada luz con sombras dibuja de nuevo los objetos que las proyectan, y la caja de la cámara de sombras decide dónde pueden aparecer.
PCFSoftShadowMapya no existe enWebGPURendererdesde r186.
Ejercicios
Sección titulada «Ejercicios»- Sin ejecutar nada, ¿cuántos vértices y triángulos tiene
SphereGeometry(1, 8, 4)? Luego compruébalo conscripts/l02-exercises.ts. - Una
SpotLightrecibepower = 800. ¿Cuál es su intensidad en candelas, y cambia cuando estrechas el cono conangle = Math.PI / 12? LeeSpotLight.jsantes de comprobarlo con el script. - Con
?neckshadow=off, el mástil recibe sombras pero no proyecta ninguna. PredicedrawCallsytriangles, y lo que aparece sobre la mesa, luego compruébalo connode scripts/probe.mjs 02-materials-lights --query neckshadow=off --shot out/neck.png.
Solución 1
$ node scripts/l02-exercises.tsSphereGeometry(1, 8, 4): { vertices: 45, triangles: 48 }Los vértices forman una cuadrícula de (8 + 1) × (4 + 1) = 45: la costura de la esfera se guarda dos veces, para que las coordenadas de textura puedan ir de 0 a 1 a su alrededor. Hay 8 × 4 = 32 cuadriláteros, 64 triángulos, menos un triángulo por cuadrilátero en las filas superior e inferior, donde el cuadrilátero se ha reducido a un triángulo en el polo: 64 − 8 − 8 = 48. Para SphereGeometry(1, 32, 16): 33 × 17 = 561 vértices y 2 × 32 × 16 − 2 × 32 = 960 triángulos, como se imprimió arriba.
Solución 2
SpotLight with power 800 lm: { intensity: 254.65, angle: 1.0472 }after angle = π/12: { intensity: 254.65, power: 800 }La potencia de SpotLight es intensidad × π, “por convención para un foco”, dice su código fuente, así que 800 lm dan 800 / π ≈ 254.65 cd, cuatro veces los 63.66 de la luz puntual. Estrechar el cono no cambia ni la intensidad ni la potencia: three.js no concentra los lúmenes en un cono más pequeño, como haría un reflector real. El ángulo por defecto es π/3 (1.0472 radianes).
Solución 3
$ node scripts/probe.mjs 02-materials-lights --query neckshadow=off{ ... "render": { "calls": 9, "drawCalls": 14, "triangles": 527 }, ...}Un draw call y 12 triángulos menos: el mástil ya no se dibuja en el shadow map. Los trastes sí, así que sus sombras finas llegan ahora a la mesa a través del mástil, que ya no las tapa:

Página en vivo: ejecutarla aquí
El mástil sigue recibiendo las sombras de los trastes en su cara superior. Una malla que no proyecta sombra pero debería tapar otra es una causa frecuente de luz que se cuela donde no debería.
Fuentes
Sección titulada «Fuentes»- Manual de three.js: Primitives, Materials, Lights, Shadows
- Documentación de three.js:
BufferGeometry,MeshStandardMaterial,MeshPhysicalMaterial,DirectionalLight,LightShadow - Código fuente de three.js en r186:
PointLight.js,SpotLight.js,DirectionalLightShadow.js,ShadowNode.js - La guía de migración para las luces físicas de r155 y los tipos de sombra de r186
- Khronos: glTF 2.0, materiales
- Microsoft: Información general sobre gráficos 3D, materiales y luces