Custom shader material

커스텀 셰이더 머티리얼

Writing GLSL by hand instead of using a built-in material, to control color and shape at the vertex and pixel level.

Also known as: GLSLShaderMaterialVertex/Fragment shader
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x08060f);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 0, 4.6);

const vertexShader = [
  'varying vec3 vPos;',
  'varying vec3 vNormal;',
  'void main() {',
  '  vPos = position;',
  '  vNormal = normalize(normalMatrix * normal);',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
  '}',
].join('\n');

const fragmentShader = [
  'uniform float uTime;',
  'varying vec3 vPos;',
  'varying vec3 vNormal;',
  'void main() {',
  '  float bands = sin((vPos.y * 4.0 + uTime * 1.4)) * 0.5 + 0.5;',
  '  float swirl = sin((vPos.x * 5.0 - vPos.z * 5.0 + uTime * 2.0)) * 0.5 + 0.5;',
  '  vec3 colorA = vec3(0.36, 0.36, 0.97);',
  '  vec3 colorB = vec3(0.95, 0.36, 0.54);',
  '  vec3 base = mix(colorA, colorB, bands * 0.6 + swirl * 0.4);',
  '  float rim = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 2.0);',
  '  gl_FragColor = vec4(base + rim * 0.25, 1.0);',
  '}',
].join('\n');

const mat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader,
  fragmentShader,
});
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1.5, 96, 96), mat);
scene.add(mesh);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  mat.uniforms.uTime.value = t * 0.001;
  mesh.rotation.y = t * 0.00018;
  renderer.render(scene, camera);
});

THREE.ShaderMaterial takes two strings: vertexShader and fragmentShader. The vertex shader runs once per vertex to decide the screen position (gl_Position); the fragment shader runs once per rasterized pixel to decide its color (gl_FragColor). Pass values between the two with varying (or in/out) variables.

The channel for JS to hand values into GLSL is a uniform — think of it as a global that every vertex and pixel reads the same value from. The demo drives flowing stripes and a swirl purely from one uniform float uTime, computed with sin() inside the fragment shader.

Attributes like position, normal, uv, and matrices like modelViewMatrix and projectionMatrix are injected automatically by three.js — you don’t declare them yourself. Declaring them again actually causes a duplicate-declaration error.

When to use

For a distinctive look no built-in material can produce, procedural patterns, or data-visualization shaders.