Setting the file. One moment. Post · Frost Sequence Camera Orbit · heygen-com/hyperframes · Skills DocsThree Mesh BVH LICENSE
source/src/post/Post.ts
TypeScript·322 lines·12 KB
11 mrt,
12 output,
13 velocity,
14 vec2,
15 vec3,
16 vec4,
17 float,
18 uniform,
19 texture,
20 screenUV,
21 screenCoordinate,
22 screenSize,
23 mix,
24 luminance,
25 smoothstep,
26 length,
27 max,
28 clamp,
29 agxToneMapping,
30 acesFilmicToneMapping,
31 neutralToneMapping,
32 linearToneMapping,
33 floor,
34 step,
35} = tsl;
36import { traa } from "three/addons/tsl/display/TRAANode.js";
37import { taau } from "three/addons/tsl/display/TAAUNode.js";
38import { fsr1 } from "three/addons/tsl/display/FSR1Node.js";
39import { sharpen } from "three/addons/tsl/display/SharpenNode.js";
40import { dof } from "three/addons/tsl/display/DepthOfFieldNode.js";
41import { bloom } from "three/addons/tsl/display/BloomNode.js";
42import { sRGBTransferOETF } from "three/src/nodes/display/ColorSpaceFunctions.js";
43import type { PerformanceD, PostD } from "../dials/store";
44import { hash31 } from "../tsl/noise";
45
46type N = any;
47
48export interface PostGraphOptions {
49 scene: THREE.Scene;
50 camera: THREE.PerspectiveCamera;
51 renderer: THREE.WebGPURenderer;
52 blueNoise: THREE.Texture;
53 /** Optional additive haze node factory (gets the scene depth texture node and the viewZ node). */
54 haze?: (depthNode: N, viewZNode: N) => N;
55 /** Build the first graph with the live settings, avoiding a throwaway startup pipeline. */
56 initial?: PostD;
57 initialPerformance?: PerformanceD;
58 initialHaze?: boolean;
59}
60
61export function createPost(o: PostGraphOptions) {
62 const u = {
63 exposure: uniform(1.0),
64 contrast: uniform(1.08),
65 blackLift: uniform(0.02),
66 monochrome: uniform(1.0),
67 vignette: uniform(0.2),
68 vignetteSoft: uniform(0.7),
69 vignetteRadius: uniform(0.85),
70 grain: uniform(0.03),
71 grainSize: uniform(1),
72 grainSeed: uniform(new THREE.Vector2()),
73 grainShadow: uniform(0.8),
74 bgGrain: uniform(0),
75 bgGrainSize: uniform(2),
76 bgGrainSeed: uniform(new THREE.Vector2()),
77 dither: uniform(1.0),
78 focus: uniform(12.4),
79 focusRange: uniform(3.0),
80 aperture: uniform(0.9),
81 bloomStrength: uniform(0.15),
82 bloomRadius: uniform(0.15),
83 bloomThreshold: uniform(1.2),
84 sceneFade: uniform(1.0),
85 hazeIntensity: uniform(0.22),
86 upscaleSharpness: uniform(0.2),
87 };
88
89 const post = new ((THREE as any).RenderPipeline ?? THREE.PostProcessing)(o.renderer);
90 post.outputColorTransform = false;
91
92 let currentMode = { aa: "", tonemap: "", dof: true, haze: false, upscaler: "", sharpen: false };
93 let scenePass: any = null;
94 let temporal: any = null;
95 let lastRenderFrame = -1;
96 let hazeRTT: any = null;
97 let hazeScale = 1;
98 let settingsVersion = -1;
99 let disposableNodes: any[] = [];
100
101 function build(mode: {
102 aa: string;
103 tonemap: string;
104 dof: boolean;
105 haze: boolean;
106 upscaler: string;
107 sharpen: boolean;
108 }) {
109 currentMode = { ...mode };
110 const previousNodes = disposableNodes;
111 const nextNodes: any[] = [];
112 const own = (node: any) => {
113 if (node && typeof node.dispose === "function") nextNodes.push(node);
114 return node;
115 };
116 scenePass = own(pass(o.scene, o.camera, { type: THREE.HalfFloatType, samples: 0 } as any));
117 scenePass.setMRT(mrt({ output, velocity }));
118 const beauty = scenePass.getTextureNode("output");
119 const depthTex = scenePass.getTextureNode("depth");
120 const velTex = scenePass.getTextureNode("velocity");
121 const viewZ = scenePass.getViewZNode();
122
123 // TAAU reconstructs and anti-aliases the low-resolution MRT directly. FSR1 expects an
124 // anti-aliased input, so retain TRAA ahead of it. An RTT makes the bilinear path an explicit
125 // output-resolution pass; native is the original same-resolution pipeline.
126 let color: N;
127 temporal = null;
128 if (mode.upscaler === "taau") color = temporal = own(taau(beauty, depthTex, velTex, o.camera));
129 else {
130 color =
131 mode.aa === "traa" ? (temporal = own(traa(beauty, depthTex, velTex, o.camera))) : beauty;
132 if (mode.upscaler === "fsr1")
133 color = own(fsr1(color, float(2).sub(u.upscaleSharpness.mul(2)), true));
134 else if (mode.upscaler === "bilinear") color = own(rtt(color));
135 }
136 if (mode.sharpen && (mode.upscaler === "taau" || mode.upscaler === "bilinear")) {
137 color = own(sharpen(color, float(2).sub(u.upscaleSharpness.mul(2)), true));
138 }
139
140 hazeRTT = null;
141 if (o.haze && mode.haze) {
142 // the haze is a smooth volumetric term: it can be rendered at reduced resolution and upsampled
143 hazeRTT = own(rtt(o.haze(depthTex, viewZ)));
144 hazeRTT.setResolutionScale(hazeScale);
145 color = color.add(hazeRTT.mul(u.hazeIntensity));
146 }
147
148 if (mode.dof) color = own(dof(color, viewZ, u.focus, u.focusRange, u.aperture));
149
150 const bl = own(bloom(color, u.bloomStrength, u.bloomRadius, u.bloomThreshold));
151 (bl as any).smoothWidth.value = 0.35;
152 color = color.add(bl);
153
154 const graded = Fn(() => {
155 const hdr = vec3(color).mul(u.sceneFade).toVar();
156 const luma = luminance(hdr);
157 hdr.assign(mix(hdr, vec3(luma), u.monochrome));
158 const tm =
159 mode.tonemap === "aces"
160 ? acesFilmicToneMapping(hdr, u.exposure)
161 : mode.tonemap === "neutral"
162 ? neutralToneMapping(hdr, u.exposure)
163 : mode.tonemap === "linear"
164 ? linearToneMapping(hdr, u.exposure)
165 : agxToneMapping(hdr, u.exposure);
166 const t = clamp(vec3(tm), 0.0, 1.0).toVar();
167 const mid = float(0.18);
168 t.assign(clamp(mid.add(t.sub(mid).mul(u.contrast)), 0.0, 1.0));
169 const sCurve = t.mul(t).mul(float(3).sub(t.mul(2)));
170 t.assign(mix(t, sCurve, clamp(u.contrast.sub(1.0).mul(1.5), 0.0, 0.6)));
171 t.assign(t.mul(u.blackLift.oneMinus()).add(u.blackLift));
172 const aspect = screenSize.x.div(screenSize.y);
173 const dv = vec2(screenUV.x.sub(0.5).mul(aspect), screenUV.y.sub(0.5));
174 const r = length(dv).div(u.vignetteRadius);
175 const vig = smoothstep(float(1.0).sub(u.vignetteSoft), 1.0001, r).mul(u.vignette);
176 t.assign(t.mul(vig.oneMinus()));
177 const srgb = vec3(sRGBTransferOETF(t)).toVar();
178 const px = floor(screenCoordinate.xy.div(u.grainSize));
179 const bnUV = px.div(64.0).add(u.grainSeed);
180 const n = texture(o.blueNoise, bnUV).r.sub(0.5);
181 const n2 = texture(o.blueNoise, bnUV.add(vec2(0.37, 0.61))).r.sub(0.5);
182 const tri = n.add(n2);
183 const lum = luminance(srgb);
184 const shadowW = mix(float(1.0), smoothstep(1.0, 0.05, lum), u.grainShadow);
185 const grainAmt = u.grain.mul(shadowW).mul(smoothstep(0.0, 0.08, lum).mul(0.85).add(0.15));
186 srgb.addAssign(vec3(tri.mul(grainAmt)));
187 // background-only grain for recorded video: coarse blocks of white noise, several grey levels deep, so
188 // the codec keeps a texture where the plain gradient would band (object pixels are left alone)
189 const isBg = step(0.9999995, depthTex.r);
190 const pb = floor(screenCoordinate.xy.div(max(u.bgGrainSize, 1.0)));
191 const b1 = hash31(vec3(pb, u.bgGrainSeed.x.mul(97.0)));
192 const b2 = hash31(vec3(pb.add(vec2(17.3, 5.1)), u.bgGrainSeed.y.mul(53.0)));
193 srgb.addAssign(vec3(b1.add(b2).sub(1.0).mul(u.bgGrain).mul(isBg)));
194 const dn = texture(o.blueNoise, screenCoordinate.xy.div(64.0).add(u.grainSeed.yx)).r.sub(0.5);
195 const dn2 = texture(
196 o.blueNoise,
197 screenCoordinate.xy.div(64.0).add(vec2(0.5, 0.25)).add(u.grainSeed),
198 ).r.sub(0.5);
199 srgb.addAssign(vec3(dn.add(dn2).mul(u.dither).div(255.0)));
200 return vec4(max(srgb, 0.0), 1.0);
201 })();
202
203 post.outputNode = graded;
204 post.needsUpdate = true;
205 disposableNodes = nextNodes;
206 for (const node of previousNodes) node.dispose();
207 }
208
209 build({
210 aa: o.initial?.aaMode ?? "traa",
211 tonemap: o.initial?.tonemap ?? "agx",
212 dof: o.initial?.dof.enabled ?? true,
213 haze: !!o.haze && (o.initialHaze ?? true),
214 upscaler: o.initialPerformance?.nativePostEffects ? o.initialPerformance.upscaler : "native",
215 sharpen:
216 !!o.initialPerformance?.nativePostEffects &&
217 !!o.initialPerformance.upscaleSharpness &&
218 (o.initialPerformance.upscaler === "taau" || o.initialPerformance.upscaler === "bilinear"),
219 });
220
221 function update(
222 p: PostD,
223 perf: PerformanceD,
224 focusDistance: number,
225 sceneFade: number,
226 hazeIntensity: number,
227 t: number,
228 version: number,
229 ) {
230 const haze = !!o.haze && hazeIntensity !== 0;
231 const upscaler = perf.nativePostEffects ? perf.upscaler : "native";
232 const useSharpen =
233 perf.nativePostEffects &&
234 perf.upscaleSharpness > 0 &&
235 (upscaler === "taau" || upscaler === "bilinear");
236 if (version !== settingsVersion || haze !== currentMode.haze) {
237 settingsVersion = version;
238 if (
239 p.aaMode !== currentMode.aa ||
240 p.tonemap !== currentMode.tonemap ||
241 p.dof.enabled !== currentMode.dof ||
242 haze !== currentMode.haze ||
243 upscaler !== currentMode.upscaler ||
244 useSharpen !== currentMode.sharpen
245 ) {
246 build({
247 aa: p.aaMode,
248 tonemap: p.tonemap,
249 dof: p.dof.enabled,
250 haze,
251 upscaler,
252 sharpen: useSharpen,
253 });
254 }
255 u.exposure.value = p.exposure;
256 u.contrast.value = p.contrast;
257 u.blackLift.value = p.blackLift;
258 u.monochrome.value = p.monochrome;
259 u.vignette.value = p.vignette.strength;
260 u.vignetteSoft.value = p.vignette.softness;
261 u.vignetteRadius.value = p.vignette.radius;
262 u.grain.value = p.grain.strength;
263 u.grainSize.value = p.grain.size;
264 u.grainShadow.value = p.grain.shadowWeight;
265 u.bgGrain.value = p.grain.backgroundStrength;
266 u.bgGrainSize.value = p.grain.backgroundSize;
267 u.dither.value = p.dither ? 1 : 0;
268 u.focusRange.value = p.dof.range;
269 u.aperture.value = p.dof.aperture;
270 u.bloomStrength.value = p.bloom.intensity;
271 u.bloomRadius.value = p.bloom.radius;
272 u.bloomThreshold.value = p.bloom.threshold;
273 u.upscaleSharpness.value = perf.upscaleSharpness;
274 }
275 const bs = p.grain.backgroundSpeed > 0 ? Math.floor(t * 24 * p.grain.backgroundSpeed) : 0;
276 u.bgGrainSeed.value.set(((bs * 0.7548776662) % 1) + 0.1, ((bs * 0.5698402909) % 1) + 0.1);
277 const gs = p.grain.speed > 0 ? Math.floor(t * 24 * p.grain.speed) : 0;
278 u.grainSeed.value.set((gs * 0.7548776662) % 1, (gs * 0.5698402909) % 1);
279 u.focus.value = focusDistance + p.dof.focusBias;
280 u.sceneFade.value = sceneFade;
281 u.hazeIntensity.value = hazeIntensity;
282 }
283
284 function setHazeResolution(scale: number) {
285 if (scale === hazeScale) return;
286 hazeScale = scale;
287 if (hazeRTT) hazeRTT.setResolutionScale(scale);
288 }
289
290 function setSceneResolution(scale: number) {
291 if (scenePass) scenePass.setResolutionScale(scale);
292 }
293
294 function setFrame(index: number) {
295 if (temporal) {
296 // Pinned three 0.185.1 temporal AA uses a 31-entry jitter cycle and seeds history on resize.
297 // Tie jitter to composition time, discard stale seek history.
298 temporal._jitterIndex = ((index % 31) + 31) % 31;
299 if (index !== lastRenderFrame + 1) temporal._historyRenderTarget.setSize(1, 1);
300 }
301 lastRenderFrame = index;
302 }
303
304 function dispose() {
305 for (const node of disposableNodes) node.dispose();
306 disposableNodes = [];
307 post.dispose();
308 }
309
310 return {
311 post,
312 uniforms: u,
313 update,
314 setHazeResolution,
315 setSceneResolution,
316 setFrame,
317 dispose,
318 get scenePass() {
319 return scenePass;
320 },
321 };
322}