커스텀 셰이더
모든 BOX 메시는 Painter 패키지가 생성한 ShaderMaterial 하나를 사용합니다.
머티리얼을 교체하지 않고도 프래그먼트 셰이더의 세 지점에 직접 만든 GLSL을
끼워 넣을 수 있습니다.
ShaderHooks 객체
interface ShaderHooks {
uniforms?: Record<string, any>;
uvModifier?: string;
colorModifier?: string;
}요소에는 엄격한 JSON으로 선언합니다 (큰따옴표 필수).
<div
data-mirage-shader='{
"uniforms": { "uTime": 0, "uAmount": 0.05 },
"uvModifier": "resultUv.x += sin(resultUv.y * 10.0 + uTime) * uAmount;",
"colorModifier": "finalColor.rgb = mix(finalColor.rgb, vec3(1.0, 0.2, 0.4), 0.3);"
}'
></div>각 훅이 들어가는 위치
uniform vec2 uSize;
uniform vec4 uBgColor;
// …
#INJECT_DECLARATIONS // ← 유니폼 선언
void main() {
vec2 p = (vUv - 0.5) * uMeshSize;
// border-radius 클램핑…
#INJECT_UV_MODIFIER // ← uvModifier
vec4 baseColor = vec4(uBgColor.rgb, uBgColor.a);
// 그라디언트 레이어…
#INJECT_BASE_COLOR // ← 텍스처 샘플링
// SDF 박스, 테두리, 그림자…
#INJECT_COLOR_MODIFIER // ← colorModifier
float finalOpacity = finalColor.a * uOpacity;
if (finalOpacity < 0.001) discard;
gl_FragColor = vec4(finalColor.rgb, finalOpacity);
}uvModifier
텍스처 샘플링 전에 실행됩니다. resultUv를 수정하세요.
| 사용 가능 | 타입 | 의미 |
|---|---|---|
resultUv | vec2 | 샘플링에 쓰이는 UV — 여기에 쓰세요 |
screenUv | vec2 | 이 프래그먼트의 정규화된 화면 좌표 |
localUv | vec2 | 요소 로컬 UV (traveler가 아닐 때만) |
p | vec2 | 메시 중심 기준 픽셀 단위 프래그먼트 위치 |
vUv | vec2 | 원본 쿼드 UV, 0…1 |
uSize, uMeshSize | vec2 | 요소 크기, 메시 크기(그림자 여백 포함) |
traveler에서는 resultUv가 screenUv로 초기화됩니다. 일반 요소에서는
uTextureRepeat / uTextureOffset으로 스케일된 localUv에서 옵니다.
object-fit: cover 동작이 유지되는 이유입니다.
colorModifier
박스·테두리·그림자가 합성된 뒤 실행됩니다. finalColor를 수정하세요.
| 사용 가능 | 타입 | 의미 |
|---|---|---|
finalColor | vec4 | 합성된 RGBA — 여기에 쓰세요 |
p, vUv, resultUv | vec2 | 위와 동일 |
| 직접 선언한 유니폼 | — | uniforms에 선언한 것 |
⚠️
여기서 gl_FragColor에 대입하지 마세요. 훅이 실행된 뒤 finalColor로부터
gl_FragColor가 기록되므로, 직접 쓴 값은 조용히 버려집니다.
유니폼 타입
타입은 JSON 값에서 추론됩니다.
| JSON 값 | GLSL 타입 |
|---|---|
0.5 | float |
[0.5, 1.0] | vec2 |
[1, 0, 0] | vec3 |
[1, 0, 0, 1] | vec4 |
{ "type": "sampler2D", "value": null } | 명시한 타입 |
<div data-mirage-shader='{
"uniforms": {
"uTime": 0,
"uMouse": [0.5, 0.5],
"uTint": [1.0, 0.4, 0.8]
},
"colorModifier": "finalColor.rgb *= uTint;"
}'></div>매 프레임 유니폼 갱신
애니메이션하겠다고 속성을 다시 쓰지 마세요 — 속성이 바뀌면 셰이더 해시가
바뀌고, Mirage가 메시를 해제한 뒤 다시 만듭니다. updateUniforms()를 쓰세요.
const el = document.querySelector("#hero") as HTMLElement;
const start = performance.now();
mirage.getTracker().onRender.add(() => {
mirage.updateUniforms(el, {
uTime: (performance.now() - start) / 1000,
});
});
window.addEventListener("pointermove", (e) => {
mirage.updateUniforms(el, {
uMouse: [e.clientX / window.innerWidth, 1 - e.clientY / window.innerHeight],
});
});레시피
스캔라인
{
"uniforms": { "uDensity": 400 },
"colorModifier": "finalColor.rgb *= 0.85 + 0.15 * step(0.5, fract(vUv.y * uDensity));"
}움직이는 그라디언트 틴트
{
"uniforms": { "uTime": 0 },
"colorModifier": "finalColor.rgb = mix(finalColor.rgb, vec3(0.4 + 0.6 * sin(uTime), 0.3, 0.9), 0.35);"
}이미지 배럴 왜곡
{
"uniforms": { "uStrength": 0.15 },
"uvModifier": "vec2 c = resultUv - 0.5; resultUv = 0.5 + c * (1.0 + uStrength * dot(c, c));"
}디졸브
{
"uniforms": { "uProgress": 0 },
"colorModifier": "float n = fract(sin(dot(vUv, vec2(12.9898, 78.233))) * 43758.5453); if (n > uProgress) finalColor.a = 0.0;"
}함정
| 증상 | 원인 | 해결 |
|---|---|---|
| 셰이더가 무시됨 | 속성이 엄격한 JSON이 아님 | 모든 키와 문자열에 큰따옴표 사용 |
| 메시가 깜빡이거나 초기화됨 | 매 프레임 속성을 다시 씀 | updateUniforms() 사용 |
uTexture가 정의되지 않음 | 요소에 이미지도 훅도 없음 | 텍스처 청크는 텍스처나 훅이 있을 때만 주입됨 |
| 효과 위치가 어긋남 | vUv와 resultUv를 혼동 | vUv는 쿼드 로컬, resultUv는 실제 샘플링용 |
디버깅
머티리얼에서 컴파일된 셰이더를 읽어 볼 수 있습니다. private 필드를 뚫는 방식이라 API가 아닌 디버깅 요령으로만 쓰세요.
// start() 이후 — 내부 접근이며 버전에 따라 깨질 수 있음
const registry = (mirage as any)._engine.registry;
const mesh = registry.get(el);
console.log(mesh.material.fragmentShader);WebGL 컴파일 오류는 브라우저 콘솔에 줄 번호와 함께 표시되며, 이 생성된 소스에 대응됩니다.