히어로와 파이프라인이 같은 씬을 쓰던 것을 나눈다
게이트 씬을 파이프라인으로 옮기면서 히어로에도 남겨뒀더니 같은 그림이
페이지에 두 번 나왔다. 역할이 다르면 씬도 달라야 한다.
히어로 shader-plane 흐름 필드 노이즈를 두 번 접어 빛의 결을 만든다
공간을 만드는 재질. 아무것도 반응 안 한다
파이프라인 gate-scene 유리판 여섯 통과하면 그 단계가 켜진다. 주인공
재료 shader-plane 스테이지 필터 타일이 앉는 무대
히어로에는 무거운 씬을 임포트하지 않는다 — three.md §0 대로 셰이더 플레인 하나다.
lib/hero-scene.ts -> lib/gate-scene.ts 로 리네임했다. 이름이 용도와 다르면
다음 사람이 히어로 것인 줄 알고 고친다.
대비 실측: 캔버스 배경 최악 8.46:1(가장 밝은 중앙 여백), h1 자리 14.83:1.
readPixels 로 재려다 전부 0 이 나왔다 — preserveDrawingBuffer: false 면
버퍼가 비어 있다. 스크린샷 픽셀로 쟀고, 글자 없는 영역만 골랐다.
스킬 three.md
씬을 두 자리에 재사용하지 마라. 역할(주인공/재질/무대)로 나눠라
파일 이름을 용도와 맞춰라
WebGL 위 대비는 readPixels 가 아니라 스크린샷 픽셀로 잰다.
0 을 검정으로 착각하면 대비가 통과한 것처럼 보인다
This commit is contained in:
parent
af3a08397e
commit
91bb4ae9dc
5 changed files with 124 additions and 29 deletions
|
|
@ -246,6 +246,27 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다
|
|||
| 사례 | `drift` 0.4 | 두 결과물이 흐르는 자리라 배경도 같이 흐른다 |
|
||||
| 조판 | `drift` 0.25 | **종이는 손에 닿는 거리다.** 원경으로 두면 거짓말이 된다 |
|
||||
|
||||
### three.js 씬 셋은 서로 다른 것이어야 한다
|
||||
|
||||
게이트 씬을 파이프라인으로 옮기면서 히어로에도 그대로 남겨뒀더니
|
||||
**같은 그림이 페이지에 두 번** 나왔다. 히어로를 다른 것으로 바꿨다.
|
||||
|
||||
| 자리 | 씬 | 무엇 |
|
||||
|---|---|---|
|
||||
| 히어로 | `shader-plane` 흐름 필드 | 노이즈를 두 번 접어 빛이 유리를 지난 결을 만든다. 공간을 만드는 **재질** |
|
||||
| 파이프라인 | `gate-scene` 유리판 여섯 장 | 스크롤하면 통과하고 그 단계가 켜진다. 의미를 가진 **주인공** |
|
||||
| 재료 | `shader-plane` 스테이지 | SVG 필터 타일이 앉는 무대 |
|
||||
|
||||
히어로에는 무거운 씬을 임포트하지 않는다 — `three.md` §0 이 권하는 대로
|
||||
**셰이더 플레인 하나**다. 파일 이름도 `hero-scene.ts` → `gate-scene.ts` 로 고쳤다.
|
||||
이름이 실제 용도와 다르면 다음 사람이 히어로 것인 줄 알고 고친다.
|
||||
|
||||
캔버스 배경 위 본문 대비 실측(스크린샷 픽셀): 최악 **8.46:1**(가장 밝은 중앙 여백),
|
||||
h1 자리 14.83:1.
|
||||
|
||||
> `readPixels` 로 재려다 전부 0 이 나왔다. `preserveDrawingBuffer: false` 면 버퍼가
|
||||
> 비워져 있다. **WebGL 위 대비는 스크린샷 픽셀로 재라.**
|
||||
|
||||
### 게이트 씬을 파이프라인으로 옮겼다
|
||||
|
||||
히어로에서 뺐던 유리판 여섯 장을 여기로 되살렸다. **같은 씬인데 자막이 필요 없어졌다.**
|
||||
|
|
|
|||
|
|
@ -162,12 +162,70 @@ const { c, ui } = Astro.props;
|
|||
|
||||
|
||||
<script>
|
||||
// 유리판이 느리게 흐르며 공간을 만든다. 배경이므로 스스로 설명하지 않는다.
|
||||
// 굴절은 렌더 타겟 한 장으로 만든다 — transmission 재질보다 훨씬 싸다.
|
||||
import { mountHeroScene } from "../lib/hero-scene";
|
||||
/*
|
||||
히어로 배경은 셰이더 플레인 하나다. 무거운 씬을 임포트하지 않는다 —
|
||||
references/three.md §0 이 권하는 그 구조다.
|
||||
|
||||
한때 여기에도 게이트 씬(유리판 여섯 장)을 깔았는데, 그러면 같은 그림이
|
||||
파이프라인 섹션과 페이지에 두 번 나온다. 게이트 은유는 옆에 여섯 단계가
|
||||
적힌 그 섹션의 것이고, 여기는 공간을 만드는 재질이면 된다.
|
||||
*/
|
||||
import { mountShaderPlane } from "../lib/shader-plane";
|
||||
|
||||
const canvas = document.getElementById("hero-canvas") as HTMLCanvasElement | null;
|
||||
|
||||
if (canvas) {
|
||||
mountHeroScene({ canvas, accentVar: "--accent", glowVar: "--glow-2" });
|
||||
mountShaderPlane({
|
||||
canvas,
|
||||
pointer: true,
|
||||
speed: 0.5,
|
||||
colors: { uA: "--accent", uB: "--glow-2" },
|
||||
fragment: `
|
||||
precision mediump float;
|
||||
varying vec2 vUv;
|
||||
uniform float uTime; uniform float uAspect;
|
||||
uniform vec2 uPointer;
|
||||
uniform vec3 uA; uniform vec3 uB;
|
||||
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
||||
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), 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 += a * noise(p); p *= 2.03; a *= 0.5; }
|
||||
return v;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 p = vUv; p.x *= uAspect;
|
||||
float t = uTime * 0.06;
|
||||
vec2 m = uPointer; m.x *= uAspect;
|
||||
|
||||
// 흐름 필드. 두 번 접어서 빛이 유리를 지난 것처럼 늘어지게 만든다.
|
||||
vec2 q = vec2(fbm(p * 1.5 + t), fbm(p * 1.5 - t * 0.8 + 4.2));
|
||||
vec2 r = vec2(fbm(p * 2.1 + q * 1.6 + t * 0.5),
|
||||
fbm(p * 2.1 + q * 1.6 - t * 0.4 + 1.7));
|
||||
float f = fbm(p * 1.2 + r * 1.9);
|
||||
|
||||
// 광원 둘. 포인터를 아주 조금만 따라간다.
|
||||
vec2 c1 = mix(vec2(0.30 * uAspect, 0.58), m, 0.14);
|
||||
vec2 c2 = vec2(0.80 * uAspect, 0.34);
|
||||
|
||||
float a = smoothstep(1.00, 0.06, distance(p, c1) + f * 0.42);
|
||||
float b = smoothstep(0.92, 0.05, distance(p, c2) + r.y * 0.38);
|
||||
|
||||
// 결이 지나간 자리에 얇은 선이 선다. 유리 가장자리의 굴절 대신이다.
|
||||
float streak = smoothstep(0.48, 0.52, f) - smoothstep(0.52, 0.58, f);
|
||||
|
||||
vec3 col = uA * a + uB * b * 0.9 + (uA + uB) * 0.5 * streak * 0.16;
|
||||
float alpha = clamp(a * 1.05 + b * 0.95 + streak * 0.2, 0.0, 1.0) * 0.8;
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ const { c } = Astro.props;
|
|||
|
||||
<script>
|
||||
// 게이트를 통과할 때 해당 단계를 켠다. 값이 바뀔 때만 DOM 을 쓴다.
|
||||
import { mountHeroScene } from "../lib/hero-scene";
|
||||
import { mountGateScene } from "../lib/gate-scene";
|
||||
|
||||
const canvas = document.getElementById("pipeline-canvas") as HTMLCanvasElement | null;
|
||||
const section = document.getElementById("pipeline");
|
||||
|
|
@ -230,7 +230,7 @@ const { c } = Astro.props;
|
|||
|
||||
if (canvas && section && stops.length) {
|
||||
let last = -1;
|
||||
mountHeroScene({
|
||||
mountGateScene({
|
||||
canvas,
|
||||
accentVar: "--accent",
|
||||
glowVar: "--glow-2",
|
||||
|
|
|
|||
|
|
@ -1,33 +1,20 @@
|
|||
/**
|
||||
* 히어로 3D 씬 — 파이프라인 게이트.
|
||||
* 게이트 씬 — 유리판 여섯 장이 깊이 축에 서고, 스크롤하면 카메라가 통과한다.
|
||||
*
|
||||
* 유리 판 여섯 장이 깊이 축에 차례로 서 있다. 이 스킬의 여섯 단계다.
|
||||
* 스크롤하면 카메라가 판을 하나씩 통과하고, 지나온 판은 뒤에 남아 겹친다.
|
||||
* 파이프라인 섹션 전용이다. 한때 히어로 배경으로도 썼는데, 그러면 같은 그림이
|
||||
* 페이지에 두 번 나온다. 그리고 히어로에는 옆에서 반응할 콘텐츠가 없어서
|
||||
* 의미를 판독기가 자막으로 설명해야 했다 — 그림이 스스로 말하지 못한다는 뜻이다.
|
||||
* 히어로 배경은 `shader-plane.ts` 의 흐름 필드가 맡는다.
|
||||
*
|
||||
* 형태에 이유가 있다. 이 제품이 파는 것은 순서이고("순서를 바꾸지 마라"가
|
||||
* 절대 규칙이다), 순서를 깊이로 옮기는 것은 은유가 아니라 직역이다.
|
||||
* 판이 여섯인 이유도 여기서 나온다. 아래 Pipeline 섹션의 여섯 단계와 같은 수다.
|
||||
*
|
||||
* 조사 결론(research/three/08-proposal.md): 우리가 파는 것은 무형이라
|
||||
* 구체 오브젝트가 성립하지 않는다. GLB 는 쓰지 않는다. 지오메트리는 코드다.
|
||||
*
|
||||
* 구현 방식과 그 이유:
|
||||
* 배경 그라디언트를 렌더 타겟에 한 번 그리고, 유리 판이 그 텍스처를
|
||||
* UV 를 밀어서 샘플링한다. MeshPhysicalMaterial 의 transmission 은
|
||||
* 매 프레임 씬을 다시 렌더하므로 판 한 장만 있어도 비싸다.
|
||||
* references/three.md 가 그 비용을 경고한다. 렌더 타겟 한 장이면
|
||||
* 같은 인상을 훨씬 싸게 얻는다.
|
||||
*
|
||||
* 예산 규칙은 그대로 지킨다:
|
||||
* 성능 게이트는 references/three.md 그대로다.
|
||||
* - three 는 초기 번들에 없다. 화면에 가까워질 때만 받는다
|
||||
* - DPR 클램프 1.5
|
||||
* - DPR 클램프 1.5. 저사양이면 판을 여섯에서 셋으로 줄인다
|
||||
* - 화면 밖이거나 탭이 숨으면 그리지 않는다
|
||||
* - 저사양이면 판 수를 줄인다
|
||||
* - WebGL 이 없거나 모션을 줄이는 설정이면 아예 시작하지 않는다.
|
||||
* 그때 보이는 CSS 그라디언트가 폴백이 아니라 결과물이다
|
||||
*/
|
||||
|
||||
export interface HeroSceneOptions {
|
||||
export interface GateSceneOptions {
|
||||
canvas: HTMLCanvasElement;
|
||||
/** CSS 커스텀 프로퍼티 이름 */
|
||||
accentVar: string;
|
||||
|
|
@ -61,7 +48,7 @@ const NOISE = `
|
|||
}
|
||||
`;
|
||||
|
||||
export function mountHeroScene(opts: HeroSceneOptions): void {
|
||||
export function mountGateScene(opts: GateSceneOptions): void {
|
||||
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
const { canvas } = opts;
|
||||
|
||||
|
|
@ -76,7 +63,7 @@ export function mountHeroScene(opts: HeroSceneOptions): void {
|
|||
io.observe(canvas);
|
||||
}
|
||||
|
||||
async function boot({ canvas, accentVar, glowVar, onGate, progressFrom }: HeroSceneOptions): Promise<void> {
|
||||
async function boot({ canvas, accentVar, glowVar, onGate, progressFrom }: GateSceneOptions): Promise<void> {
|
||||
let THREE: typeof import("three");
|
||||
try {
|
||||
THREE = await import("three");
|
||||
|
|
@ -59,6 +59,35 @@ h1 이 36px 이어도 히어로가 압도적이다.
|
|||
|
||||
캔버스 위에 글자를 올려야 한다면 **하나만** 올려라. 그리고 거기엔 바탕을 깔아라.
|
||||
|
||||
### 씬을 두 자리에 재사용하지 마라
|
||||
|
||||
의미를 가진 씬을 만들었다면 그 자리는 하나다. 배경이 허전하다고 같은 씬을 다른 섹션에
|
||||
또 깔면 **같은 그림이 두 번** 나오고, 첫 번째 자리의 의미까지 희석된다.
|
||||
|
||||
한 페이지에 씬이 여럿이면 **역할을 나눠라.**
|
||||
|
||||
| 역할 | 구현 | 판단 기준 |
|
||||
|---|---|---|
|
||||
| 주인공 | 씬(메시·카메라·이동) | 옆의 콘텐츠가 반응한다 |
|
||||
| 재질 | 셰이더 플레인 하나 | 공간만 만든다. 아무것도 반응하지 않는다 |
|
||||
| 무대 | 셰이더 플레인 하나 | 다른 요소가 그 위에 앉는다 |
|
||||
|
||||
**파일 이름을 용도와 맞춰라.** `hero-scene.ts` 를 파이프라인에서 쓰기 시작한 순간
|
||||
다음 사람은 그것을 히어로 것인 줄 알고 고친다. `gate-scene.ts` 로 바꿔라.
|
||||
|
||||
### WebGL 위의 대비는 `readPixels` 로 못 잰다
|
||||
|
||||
`preserveDrawingBuffer: false`(기본값)면 프레임을 그린 뒤 버퍼가 비워진다.
|
||||
`gl.readPixels` 는 **전부 0 을 돌려주고, 그 0 을 검정으로 착각하면 대비가 통과한 것처럼 보인다.**
|
||||
|
||||
**스크린샷을 찍어 그 픽셀을 재라.** 그리고 글자가 없는 영역만 골라라 —
|
||||
글자를 포함해 재면 최고 밝기가 글자 자신이라 대비 1.00:1 이 나온다.
|
||||
|
||||
```js
|
||||
// 캔버스 배경만 있는 영역에서 가장 밝은 픽셀 = 최악의 경우
|
||||
sharp(shot).extract({ left, top, width, height }).raw()
|
||||
```
|
||||
|
||||
### 씬의 의미는 옆의 콘텐츠가 반응할 때 성립한다
|
||||
|
||||
3D 에 은유를 담았는데 **그 의미를 자막으로 설명하고 있다면, 그림이 스스로 말하지 못한다는 뜻**이다.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue