Skip to content

277. Shader 入门(四):后期管线与阶段收束

Published:

前面几篇都在 同一个 shader-playground 工程里,用 ShaderMaterial 直接往屏幕 framebuffer 画东西。实际产品里还有另一条常见路径:先把场景画进离屏纹理,再经过一个全屏四边形跑片元,做暗角、调色、模糊、Bloom——这叫 后期 Post-processing。Three.js 用 EffectComposer 把多个 Pass 串起来:RenderPass 负责「场景 → 纹理」,后面的 ShaderPass 负责「纹理 → 纹理」,最后一站输出到屏幕。

本篇分两块:第一块仍在 shader-playground 目录,只替换 main.js,接入 EffectComposerRenderPassShaderPass,做一个最简单的 暗角 vignette,体会 tDiffuse 如何从上一 Pass 传到下一 Pass,以及 resize 时为什么要同时更新 composer.setSize。第二块不再引入新代码,而是把之前散落的点串成知识地图,给你 自测题与延伸方向,作为 Shader 入门段的句号。

后期这一段的场景故意换回 MeshStandardMaterial 的立方体加灯光,避免和前面 Shader 平面示例混淆:你要分清「场景里物体用什么材质」和「后期全屏用什么 Shader」是两层事。Composer 链路上若色彩发灰、发曝,往往和 outputColorSpace、OutputPass(随 Three 版本查阅官方示例)有关;本篇示例以 跑通管线 为主,升级引擎时请以当前版本文档为准。

学会后期之后,你会在 examples/jsm/postprocessing 里看到 Bloom、SSAO、SSR 等现成 Pass,本质都是 RenderPass + 若干图像处理 Pass,和本节暗角没有本质区别,只是 uniform 更多、采样更重。

在 shader-playground 中继续

index.html 仍可不变。将 main.js 整体替换为下面代码(覆盖前请先备份你上一版球体 Fresnel,若需要留档可复制为 main.fresnel.js):

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a22);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100);
camera.position.set(2, 1.6, 2.8);
camera.lookAt(0, 0, 0);

const box = new THREE.Mesh(
    new THREE.BoxGeometry(0.9, 0.9, 0.9),
    new THREE.MeshStandardMaterial({ color: 0x6688cc, roughness: 0.35, metalness: 0.1 }),
);
scene.add(box);

const light = new THREE.DirectionalLight(0xffffff, 1.2);
light.position.set(2, 4, 3);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.25));

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

const composer = new EffectComposer(renderer);
composer.setPixelRatio(window.devicePixelRatio);
composer.setSize(width, height);

const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const vignetteShader = {
    uniforms: {
        tDiffuse: { value: null },
        uStrength: { value: 0.55 },
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform float uStrength;
        varying vec2 vUv;
        void main() {
            vec4 tex = texture2D(tDiffuse, vUv);
            vec2 uv = (vUv - 0.5) * 2.0;
            float d = length(uv);
            float v = 1.0 - d * uStrength;
            v = clamp(v, 0.15, 1.0);
            gl_FragColor = vec4(tex.rgb * v, tex.a);
        }
    `,
};

const vignettePass = new ShaderPass(vignetteShader);
composer.addPass(vignettePass);

function tick() {
    requestAnimationFrame(tick);
    box.rotation.y += 0.008;
    controls.update();
    composer.render();
}
tick();

window.addEventListener('resize', () => {
    const w = window.innerWidth;
    const h = window.innerHeight;
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    renderer.setSize(w, h);
    composer.setSize(w, h);
});

主循环里务必调用 composer.render(),不要再调 renderer.render(scene, camera),否则后期 Pass 不会执行。resizerenderer 与 composer 同步改尺寸,否则窗口拉伸后会出现模糊或采样错位。

若你希望暗角边缘带一点冷色,可在 gl_FragColor 前对 tex.rgbvec3(0.95, 0.97, 1.0) 之类微调,只动片元即可,不必改场景。

2026-04-19 22.32.35.gif

阶段回顾:你现在已经掌握什么

Shader 入门段覆盖了:

  1. 数据流attribute(如 positionuv)、uniform(时间、贴图、参数)、varyingvUv、法线、世界坐标)。
  2. 片元:颜色、纹理采样、程序化噪声、discard、Fresnel。
  3. 顶点:位移、法线传递(世界空间)。
  4. 后期:Composer 串联、全屏 UV 采样上一张纹理。

性能直觉:顶点次数等于顶点数,片元次数等于像素量级;噪声循环、多重纹理采样、全屏 Pass 叠太多,都是常见瓶颈。优化时优先Profiler 再下结论。

案例代码上传了小册仓库

总结

到这里,Shader 入门系列在结构上可以 告一段落;实战中把本篇的片段组合进真实场景时,记得关注 色彩空间、透明排序、性能剖析 三件事。

评论