Урок · Середній рівень · ~50 хв
Three.js · GLSL · ShaderMaterial · Uniform-змінні

Власний ShaderMaterial у Three.js

ShaderMaterial дає повний контроль над вершинними та фрагментними шейдерами GLSL, зберігаючи при цьому граф сцени, матриці та обробку геометрії Three.js. Цей урок охоплює весь робочий процес: uniform-змінні, varying-змінні, семплювання текстур, процедурні візерунки та анімовані ефекти.

1Мінімальний каркас ShaderMaterial

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const mat = new THREE.ShaderMaterial({ vertexShader: /* glsl */ ` void main() { // modelViewMatrix, projectionMatrix, position вставляє Three.js gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` void main() { gl_FragColor = vec4(0.13, 0.77, 0.37, 1.0); // суцільний зелений } `, }); const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 32), mat); scene.add(mesh);
Three.js автоматично додає на початок шейдерів ShaderMaterial оголошення projectionMatrix, modelViewMatrix, normalMatrix, position, normal, uv та uv2. RawShaderMaterial цього не робить (усе оголошуєте ви самі).

2Uniform-змінні та їх оновлення

const mat = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0 }, u_color: { value: new THREE.Color(0x22c55e) }, u_scale: { value: 2.5 }, }, vertexShader: /* glsl */ ` uniform float u_time; uniform float u_scale; void main() { vec3 pos = position; pos.y += sin(pos.x * u_scale + u_time) * 0.2; // деформація хвилею gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } `, fragmentShader: /* glsl */ ` uniform vec3 u_color; void main() { gl_FragColor = vec4(u_color, 1.0); } `, }); // Оновлюємо в циклі рендерингу — пряме записування властивості, метод не потрібен renderer.setAnimationLoop(t => { mat.uniforms.u_time.value = t * 0.001; renderer.render(scene, camera); });

3Varying-змінні: передача даних вершина → фрагмент

const mat = new THREE.ShaderMaterial({ vertexShader: /* glsl */ ` varying vec2 vUv; varying vec3 vNormal; varying vec3 vWorldPos; void main() { vUv = uv; vNormal = normalize(normalMatrix * normal); vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` varying vec2 vUv; varying vec3 vNormal; varying vec3 vWorldPos; void main() { // Просте ламбертове освітлення vec3 lightDir = normalize(vec3(1.0, 2.0, 1.5)); float diff = max(dot(vNormal, lightDir), 0.0); // колір на основі UV з освітленням vec3 col = vec3(vUv, 0.5); // R=u, G=v, B=0.5 gl_FragColor = vec4(col * (0.2 + 0.8 * diff), 1.0); } `, });

4Семплювання текстур

const loader = new THREE.TextureLoader(); const tex = loader.load('/shared/textures/noise.png'); const mat = new THREE.ShaderMaterial({ uniforms: { u_map: { value: tex }, u_time: { value: 0 }, }, vertexShader: /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` uniform sampler2D u_map; uniform float u_time; varying vec2 vUv; void main() { // Прокручуємо UV із часом vec2 scrolledUv = vUv + vec2(u_time * 0.1, 0.0); vec4 texel = texture2D(u_map, scrolledUv); gl_FragColor = texel; } `, });

5Процедурний візерунок із шуму

// Підключаємо допоміжні функції GLSL hash + value noise; зовнішня текстура не потрібна const noiseFrag = /* glsl */ ` precision highp float; varying vec2 vUv; uniform float u_time; float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); } float noise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); return mix(mix(hash(i + vec2(0,0)), hash(i + vec2(1,0)), u.x), mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); } float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += noise(p) * a; p *= 2.1; a *= 0.5; } return v; } void main() { float n = fbm(vUv * 4.0 + u_time * 0.3); vec3 col = mix(vec3(0.05, 0.1, 0.2), vec3(0.1, 0.8, 0.5), n); gl_FragColor = vec4(col, 1.0); } `;

6ShaderMaterial проти RawShaderMaterial

// ShaderMaterial автоматично вставляє: // - Вбудовані uniform-змінні: modelMatrix, viewMatrix, projectionMatrix, // modelViewMatrix, normalMatrix, cameraPosition // - Вбудовані атрибути: position, normal, uv, tangent, color // - директиви #define для світла, якщо lights:true const mat = new THREE.ShaderMaterial({ lights: true, // вставляє define THREE_MAX_LIGHTS та uniform-змінні світла fog: true, // вставляє uniform-змінні туману u_fogColor, u_fogNear, u_fogFar depthWrite: true, transparent: false, // ShaderMaterial може поєднувати фрагменти Three.js через #include <common> тощо }); // RawShaderMaterial: БЕЗ автоматичних вставок // Ви маєте самі оголосити precision, усі uniform-змінні та всі атрибути. // Використовуйте, коли потрібен найменший можливий шейдер без накладних витрат. const rawMat = new THREE.RawShaderMaterial({ vertexShader: ` precision highp float; attribute vec3 position; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` precision highp float; void main() { gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); } `, });
Налагоджуючи шейдер, додайте console.log(mat.vertexShader) — Three.js покаже кінцевий вихідний код шейдера, зокрема весь вставлений код, що полегшує розуміння того, що саме компілюється.