Урок · Середній рівень · ~50 хв
Власний 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 покаже кінцевий
вихідний код шейдера, зокрема весь вставлений код, що полегшує
розуміння того, що саме компілюється.