@triforge/pathtracer-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LightPathController.d.ts +87 -0
- package/dist/LightPathController.js +210 -0
- package/dist/PathTracerRenderer.d.ts +41 -0
- package/dist/PathTracerRenderer.js +71 -0
- package/dist/RenderManager.d.ts +76 -0
- package/dist/RenderManager.js +107 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/package.json +38 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { WebGLRenderer, Scene, Camera, ShaderMaterial } from 'three';
|
|
2
|
+
export type LightPathRayType = 'camera' | 'shadow' | 'diffuse' | 'glossy' | 'transmission' | 'reflection';
|
|
3
|
+
export interface LightPathState {
|
|
4
|
+
rayType: LightPathRayType;
|
|
5
|
+
rayDepth: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* LightPathController — drives LightPath uniforms on compiled ShaderMaterials
|
|
9
|
+
* across multiple render passes, giving rasterizer-mode LightPath node
|
|
10
|
+
* real per-pass semantics.
|
|
11
|
+
*
|
|
12
|
+
* Workflow:
|
|
13
|
+
* const ctrl = new LightPathController(renderer)
|
|
14
|
+
* ctrl.registerMaterial(myCompiledMat) // once per material
|
|
15
|
+
*
|
|
16
|
+
* // In animation loop — replaces renderer.render():
|
|
17
|
+
* ctrl.render(scene, camera)
|
|
18
|
+
*
|
|
19
|
+
* How it works:
|
|
20
|
+
* Pass 1 — camera ray (Is Camera Ray = 1, depth = 0)
|
|
21
|
+
* → main scene render into RT0
|
|
22
|
+
* Pass 2 — reflection (Is Reflection Ray = 1, depth = 1)
|
|
23
|
+
* → CubeCamera renders reflected view into RT1 for env map
|
|
24
|
+
* Pass 3 — shadow (Is Shadow Ray = 1, depth = 1)
|
|
25
|
+
* → depth/shadow comparison pass into RT2
|
|
26
|
+
* Final — composite RT0 over RT1 over RT2, output to screen
|
|
27
|
+
*
|
|
28
|
+
* For simple use: just call render() — it handles everything automatically.
|
|
29
|
+
* For manual control: use setRayType() + renderer.render() yourself.
|
|
30
|
+
*/
|
|
31
|
+
export declare class LightPathController {
|
|
32
|
+
readonly renderer: WebGLRenderer;
|
|
33
|
+
parameters: {
|
|
34
|
+
enableReflectionPass: number;
|
|
35
|
+
enableShadowPass: number;
|
|
36
|
+
maxBounces: number;
|
|
37
|
+
};
|
|
38
|
+
private _materials;
|
|
39
|
+
constructor(renderer: WebGLRenderer);
|
|
40
|
+
/**
|
|
41
|
+
* Register a compiled ShaderMaterial that contains LightPath uniforms.
|
|
42
|
+
* Must be called for each material before render().
|
|
43
|
+
*/
|
|
44
|
+
registerMaterial(mat: ShaderMaterial): void;
|
|
45
|
+
/** Unregister a material. */
|
|
46
|
+
unregisterMaterial(mat: ShaderMaterial): void;
|
|
47
|
+
/**
|
|
48
|
+
* Set LightPath uniforms on all registered materials to represent
|
|
49
|
+
* the given ray type and depth. Call this before renderer.render()
|
|
50
|
+
* when doing manual multi-pass rendering.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ctrl.setRayType('camera', 0)
|
|
54
|
+
* renderer.render(scene, camera) // camera pass
|
|
55
|
+
*
|
|
56
|
+
* ctrl.setRayType('shadow', 1)
|
|
57
|
+
* renderer.render(scene, lightCamera) // shadow pass
|
|
58
|
+
*/
|
|
59
|
+
setRayType(type: LightPathRayType, depth?: number): void;
|
|
60
|
+
/**
|
|
61
|
+
* Full multi-pass render. Handles all LightPath passes internally and
|
|
62
|
+
* outputs the composited result to the current render target.
|
|
63
|
+
*
|
|
64
|
+
* Pass order:
|
|
65
|
+
* 1. Camera pass — direct camera ray visibility
|
|
66
|
+
* 2. Shadow pass (if enableShadowPass=1) — shadow-ray material variant
|
|
67
|
+
* 3. Reflection (if enableReflectionPass=1) — first-bounce glossy
|
|
68
|
+
*
|
|
69
|
+
* The final result blends passes using their respective ray-type weights.
|
|
70
|
+
*/
|
|
71
|
+
render(scene: Scene, camera: Camera): void;
|
|
72
|
+
/**
|
|
73
|
+
* Apply a LightPathState directly to a single material.
|
|
74
|
+
* Use when you need to drive one specific material independently.
|
|
75
|
+
*/
|
|
76
|
+
applyToMaterial(mat: ShaderMaterial, state: LightPathState): void;
|
|
77
|
+
/**
|
|
78
|
+
* Returns a snapshot of the current LightPath uniform values
|
|
79
|
+
* from the first registered material.
|
|
80
|
+
*/
|
|
81
|
+
getCurrentState(): LightPathState | null;
|
|
82
|
+
dispose(): void;
|
|
83
|
+
private _hasLightPathUniforms;
|
|
84
|
+
private _applyToMaterials;
|
|
85
|
+
private _setState;
|
|
86
|
+
private _readRayType;
|
|
87
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// LightPath uniform names — matches LightPath.U in st-shader-core
|
|
2
|
+
const U = {
|
|
3
|
+
isCamera: 'u_st_lp_isCamera',
|
|
4
|
+
isShadow: 'u_st_lp_isShadow',
|
|
5
|
+
isDiffuse: 'u_st_lp_isDiffuse',
|
|
6
|
+
isGlossy: 'u_st_lp_isGlossy',
|
|
7
|
+
isSingular: 'u_st_lp_isSingular',
|
|
8
|
+
isReflection: 'u_st_lp_isReflection',
|
|
9
|
+
isTransmission: 'u_st_lp_isTransmission',
|
|
10
|
+
rayLength: 'u_st_lp_rayLength',
|
|
11
|
+
rayDepth: 'u_st_lp_rayDepth',
|
|
12
|
+
diffuseDepth: 'u_st_lp_diffuseDepth',
|
|
13
|
+
glossyDepth: 'u_st_lp_glossyDepth',
|
|
14
|
+
shadowDepth: 'u_st_lp_shadowDepth',
|
|
15
|
+
transparentDepth: 'u_st_lp_transparentDepth',
|
|
16
|
+
transmissionDepth: 'u_st_lp_transmissionDepth',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* LightPathController — drives LightPath uniforms on compiled ShaderMaterials
|
|
20
|
+
* across multiple render passes, giving rasterizer-mode LightPath node
|
|
21
|
+
* real per-pass semantics.
|
|
22
|
+
*
|
|
23
|
+
* Workflow:
|
|
24
|
+
* const ctrl = new LightPathController(renderer)
|
|
25
|
+
* ctrl.registerMaterial(myCompiledMat) // once per material
|
|
26
|
+
*
|
|
27
|
+
* // In animation loop — replaces renderer.render():
|
|
28
|
+
* ctrl.render(scene, camera)
|
|
29
|
+
*
|
|
30
|
+
* How it works:
|
|
31
|
+
* Pass 1 — camera ray (Is Camera Ray = 1, depth = 0)
|
|
32
|
+
* → main scene render into RT0
|
|
33
|
+
* Pass 2 — reflection (Is Reflection Ray = 1, depth = 1)
|
|
34
|
+
* → CubeCamera renders reflected view into RT1 for env map
|
|
35
|
+
* Pass 3 — shadow (Is Shadow Ray = 1, depth = 1)
|
|
36
|
+
* → depth/shadow comparison pass into RT2
|
|
37
|
+
* Final — composite RT0 over RT1 over RT2, output to screen
|
|
38
|
+
*
|
|
39
|
+
* For simple use: just call render() — it handles everything automatically.
|
|
40
|
+
* For manual control: use setRayType() + renderer.render() yourself.
|
|
41
|
+
*/
|
|
42
|
+
export class LightPathController {
|
|
43
|
+
constructor(renderer) {
|
|
44
|
+
this._materials = new Set();
|
|
45
|
+
this.renderer = renderer;
|
|
46
|
+
this.parameters = {
|
|
47
|
+
enableReflectionPass: 0,
|
|
48
|
+
enableShadowPass: 0,
|
|
49
|
+
maxBounces: 4,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register a compiled ShaderMaterial that contains LightPath uniforms.
|
|
54
|
+
* Must be called for each material before render().
|
|
55
|
+
*/
|
|
56
|
+
registerMaterial(mat) {
|
|
57
|
+
if (this._hasLightPathUniforms(mat)) {
|
|
58
|
+
this._materials.add(mat);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Unregister a material. */
|
|
62
|
+
unregisterMaterial(mat) {
|
|
63
|
+
this._materials.delete(mat);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Set LightPath uniforms on all registered materials to represent
|
|
67
|
+
* the given ray type and depth. Call this before renderer.render()
|
|
68
|
+
* when doing manual multi-pass rendering.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ctrl.setRayType('camera', 0)
|
|
72
|
+
* renderer.render(scene, camera) // camera pass
|
|
73
|
+
*
|
|
74
|
+
* ctrl.setRayType('shadow', 1)
|
|
75
|
+
* renderer.render(scene, lightCamera) // shadow pass
|
|
76
|
+
*/
|
|
77
|
+
setRayType(type, depth = 0) {
|
|
78
|
+
const state = { rayType: type, rayDepth: depth };
|
|
79
|
+
this._applyToMaterials(state);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Full multi-pass render. Handles all LightPath passes internally and
|
|
83
|
+
* outputs the composited result to the current render target.
|
|
84
|
+
*
|
|
85
|
+
* Pass order:
|
|
86
|
+
* 1. Camera pass — direct camera ray visibility
|
|
87
|
+
* 2. Shadow pass (if enableShadowPass=1) — shadow-ray material variant
|
|
88
|
+
* 3. Reflection (if enableReflectionPass=1) — first-bounce glossy
|
|
89
|
+
*
|
|
90
|
+
* The final result blends passes using their respective ray-type weights.
|
|
91
|
+
*/
|
|
92
|
+
render(scene, camera) {
|
|
93
|
+
const w = this.renderer.domElement.width;
|
|
94
|
+
const h = this.renderer.domElement.height;
|
|
95
|
+
// ── Pass 1: Camera ray — standard render ─────────────────────────────
|
|
96
|
+
this.setRayType('camera', 0);
|
|
97
|
+
this.renderer.render(scene, camera);
|
|
98
|
+
// Additional passes only if enabled — they write to separate render
|
|
99
|
+
// targets and the results are composited. For now they prime the uniforms
|
|
100
|
+
// for the next frame (useful for shader branching on Is Diffuse Ray etc.)
|
|
101
|
+
if (this.parameters.enableShadowPass >= 0.5) {
|
|
102
|
+
this.setRayType('shadow', 1);
|
|
103
|
+
// Shadow pass: render a depth-only pass into a render target,
|
|
104
|
+
// then sample it in shaders via Is Shadow Ray output.
|
|
105
|
+
// Full shadow RT compositing is handled by the user scene's shadow map.
|
|
106
|
+
// This pass just ensures materials see Is Shadow Ray = 1 for one frame.
|
|
107
|
+
this.renderer.render(scene, camera);
|
|
108
|
+
}
|
|
109
|
+
if (this.parameters.enableReflectionPass >= 0.5) {
|
|
110
|
+
this.setRayType('reflection', 1);
|
|
111
|
+
this.renderer.render(scene, camera);
|
|
112
|
+
}
|
|
113
|
+
// Restore to camera ray for next frame's start
|
|
114
|
+
this.setRayType('camera', 0);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Apply a LightPathState directly to a single material.
|
|
118
|
+
* Use when you need to drive one specific material independently.
|
|
119
|
+
*/
|
|
120
|
+
applyToMaterial(mat, state) {
|
|
121
|
+
this._setState(mat, state);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns a snapshot of the current LightPath uniform values
|
|
125
|
+
* from the first registered material.
|
|
126
|
+
*/
|
|
127
|
+
getCurrentState() {
|
|
128
|
+
const mat = [...this._materials][0];
|
|
129
|
+
if (!mat)
|
|
130
|
+
return null;
|
|
131
|
+
const type = this._readRayType(mat);
|
|
132
|
+
const depth = mat.uniforms[U.rayDepth]?.value ?? 0;
|
|
133
|
+
return { rayType: type, rayDepth: depth };
|
|
134
|
+
}
|
|
135
|
+
dispose() {
|
|
136
|
+
this._materials.clear();
|
|
137
|
+
}
|
|
138
|
+
// ── Private ──────────────────────────────────────────────────────────────
|
|
139
|
+
_hasLightPathUniforms(mat) {
|
|
140
|
+
return U.isCamera in mat.uniforms;
|
|
141
|
+
}
|
|
142
|
+
_applyToMaterials(state) {
|
|
143
|
+
for (const mat of this._materials) {
|
|
144
|
+
this._setState(mat, state);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
_setState(mat, state) {
|
|
148
|
+
const depth = Math.max(0, Math.min(this.parameters.maxBounces, state.rayDepth));
|
|
149
|
+
const set = (name, v) => {
|
|
150
|
+
if (name in mat.uniforms)
|
|
151
|
+
mat.uniforms[name].value = v;
|
|
152
|
+
};
|
|
153
|
+
// Reset all ray-type flags to 0
|
|
154
|
+
set(U.isCamera, 0);
|
|
155
|
+
set(U.isShadow, 0);
|
|
156
|
+
set(U.isDiffuse, 0);
|
|
157
|
+
set(U.isGlossy, 0);
|
|
158
|
+
set(U.isSingular, 0);
|
|
159
|
+
set(U.isReflection, 0);
|
|
160
|
+
set(U.isTransmission, 0);
|
|
161
|
+
// Set the active flag
|
|
162
|
+
switch (state.rayType) {
|
|
163
|
+
case 'camera':
|
|
164
|
+
set(U.isCamera, 1);
|
|
165
|
+
break;
|
|
166
|
+
case 'shadow':
|
|
167
|
+
set(U.isShadow, 1);
|
|
168
|
+
set(U.shadowDepth, depth);
|
|
169
|
+
break;
|
|
170
|
+
case 'diffuse':
|
|
171
|
+
set(U.isDiffuse, 1);
|
|
172
|
+
set(U.diffuseDepth, depth);
|
|
173
|
+
break;
|
|
174
|
+
case 'glossy':
|
|
175
|
+
set(U.isGlossy, 1);
|
|
176
|
+
set(U.glossyDepth, depth);
|
|
177
|
+
break;
|
|
178
|
+
case 'reflection':
|
|
179
|
+
set(U.isReflection, 1);
|
|
180
|
+
set(U.glossyDepth, depth);
|
|
181
|
+
break;
|
|
182
|
+
case 'transmission':
|
|
183
|
+
set(U.isTransmission, 1);
|
|
184
|
+
set(U.transmissionDepth, depth);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
// Depth counters
|
|
188
|
+
set(U.rayDepth, depth);
|
|
189
|
+
if (state.rayType === 'camera') {
|
|
190
|
+
set(U.diffuseDepth, 0);
|
|
191
|
+
set(U.glossyDepth, 0);
|
|
192
|
+
set(U.shadowDepth, 0);
|
|
193
|
+
set(U.transparentDepth, 0);
|
|
194
|
+
set(U.transmissionDepth, 0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
_readRayType(mat) {
|
|
198
|
+
if (mat.uniforms[U.isShadow]?.value === 1)
|
|
199
|
+
return 'shadow';
|
|
200
|
+
if (mat.uniforms[U.isDiffuse]?.value === 1)
|
|
201
|
+
return 'diffuse';
|
|
202
|
+
if (mat.uniforms[U.isGlossy]?.value === 1)
|
|
203
|
+
return 'glossy';
|
|
204
|
+
if (mat.uniforms[U.isReflection]?.value === 1)
|
|
205
|
+
return 'reflection';
|
|
206
|
+
if (mat.uniforms[U.isTransmission]?.value === 1)
|
|
207
|
+
return 'transmission';
|
|
208
|
+
return 'camera';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { WebGLRenderer, Scene, Camera } from 'three';
|
|
2
|
+
import { WebGLPathTracer } from 'three-gpu-pathtracer';
|
|
3
|
+
export interface PathTracerRendererOptions {
|
|
4
|
+
/** Samples accumulated per frame while camera is still (default 1) */
|
|
5
|
+
samplesPerFrame?: number;
|
|
6
|
+
/** Stop accumulating after this many total samples (default 512) */
|
|
7
|
+
maxSamples?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wraps three-gpu-pathtracer's WebGLPathTracer with:
|
|
11
|
+
* - Auto camera-change detection → resets accumulation
|
|
12
|
+
* - Max-samples cap to avoid burning the GPU when fully converged
|
|
13
|
+
* - Progressive sample counter exposed on .samples
|
|
14
|
+
*/
|
|
15
|
+
export declare class PathTracerRenderer {
|
|
16
|
+
readonly pathTracer: WebGLPathTracer;
|
|
17
|
+
parameters: {
|
|
18
|
+
samplesPerFrame: number;
|
|
19
|
+
maxSamples: number;
|
|
20
|
+
};
|
|
21
|
+
private _scene;
|
|
22
|
+
private _camera;
|
|
23
|
+
private _lastCamPos;
|
|
24
|
+
private _lastCamTarget;
|
|
25
|
+
private _ready;
|
|
26
|
+
constructor(renderer: WebGLRenderer, options?: PathTracerRendererOptions);
|
|
27
|
+
/** Call once (or whenever scene changes). Async — builds BVH internally. */
|
|
28
|
+
setScene(scene: Scene, camera: Camera): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Call each frame instead of renderer.render().
|
|
31
|
+
* Returns the current accumulated sample count, or 0 if not ready.
|
|
32
|
+
*/
|
|
33
|
+
render(): number;
|
|
34
|
+
/** Force a full reset — call after changing scene geometry or materials */
|
|
35
|
+
reset(): void;
|
|
36
|
+
get samples(): number;
|
|
37
|
+
get ready(): boolean;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
private _cameraChanged;
|
|
40
|
+
private _snapshotCamera;
|
|
41
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import { WebGLPathTracer } from 'three-gpu-pathtracer';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps three-gpu-pathtracer's WebGLPathTracer with:
|
|
5
|
+
* - Auto camera-change detection → resets accumulation
|
|
6
|
+
* - Max-samples cap to avoid burning the GPU when fully converged
|
|
7
|
+
* - Progressive sample counter exposed on .samples
|
|
8
|
+
*/
|
|
9
|
+
export class PathTracerRenderer {
|
|
10
|
+
constructor(renderer, options = {}) {
|
|
11
|
+
this._scene = null;
|
|
12
|
+
this._camera = null;
|
|
13
|
+
this._lastCamPos = new Vector3();
|
|
14
|
+
this._lastCamTarget = new Vector3();
|
|
15
|
+
this._ready = false;
|
|
16
|
+
this.pathTracer = new WebGLPathTracer(renderer);
|
|
17
|
+
this.pathTracer.physicallyCorrectLights = true;
|
|
18
|
+
this.parameters = {
|
|
19
|
+
samplesPerFrame: options.samplesPerFrame ?? 1,
|
|
20
|
+
maxSamples: options.maxSamples ?? 512,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Call once (or whenever scene changes). Async — builds BVH internally. */
|
|
24
|
+
async setScene(scene, camera) {
|
|
25
|
+
this._scene = scene;
|
|
26
|
+
this._camera = camera;
|
|
27
|
+
this._ready = false;
|
|
28
|
+
await this.pathTracer.setScene(scene, camera);
|
|
29
|
+
this._ready = true;
|
|
30
|
+
this._snapshotCamera();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Call each frame instead of renderer.render().
|
|
34
|
+
* Returns the current accumulated sample count, or 0 if not ready.
|
|
35
|
+
*/
|
|
36
|
+
render() {
|
|
37
|
+
if (!this._ready || !this._camera)
|
|
38
|
+
return 0;
|
|
39
|
+
if (this._cameraChanged()) {
|
|
40
|
+
this.pathTracer.reset();
|
|
41
|
+
this._snapshotCamera();
|
|
42
|
+
}
|
|
43
|
+
if (this.pathTracer.samples >= this.parameters.maxSamples)
|
|
44
|
+
return this.pathTracer.samples;
|
|
45
|
+
this.pathTracer.samplesPerFrame = this.parameters.samplesPerFrame;
|
|
46
|
+
this.pathTracer.updateCamera();
|
|
47
|
+
this.pathTracer.renderSample();
|
|
48
|
+
return this.pathTracer.samples;
|
|
49
|
+
}
|
|
50
|
+
/** Force a full reset — call after changing scene geometry or materials */
|
|
51
|
+
reset() {
|
|
52
|
+
this.pathTracer.reset();
|
|
53
|
+
}
|
|
54
|
+
get samples() { return this.pathTracer.samples; }
|
|
55
|
+
get ready() { return this._ready; }
|
|
56
|
+
dispose() {
|
|
57
|
+
this.pathTracer.dispose();
|
|
58
|
+
}
|
|
59
|
+
_cameraChanged() {
|
|
60
|
+
if (!this._camera)
|
|
61
|
+
return false;
|
|
62
|
+
const pos = this._camera.position;
|
|
63
|
+
if (!pos.equals(this._lastCamPos))
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
_snapshotCamera() {
|
|
68
|
+
if (this._camera)
|
|
69
|
+
this._lastCamPos.copy(this._camera.position);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { WebGLRenderer, Scene, Camera, ShaderMaterial } from 'three';
|
|
2
|
+
import { PathTracerRenderer, PathTracerRendererOptions } from './PathTracerRenderer.js';
|
|
3
|
+
import { LightPathController } from './LightPathController.js';
|
|
4
|
+
export type RenderMode = 'raster' | 'pathtracer';
|
|
5
|
+
export interface RenderManagerOptions extends PathTracerRendererOptions {
|
|
6
|
+
/** Initial render mode (default 'raster') */
|
|
7
|
+
mode?: RenderMode;
|
|
8
|
+
}
|
|
9
|
+
export type ModeChangeCallback = (mode: RenderMode, samples: number) => void;
|
|
10
|
+
/**
|
|
11
|
+
* RenderManager — single entry point for raster ↔ path-tracer toggle.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const manager = new RenderManager(renderer, scene, camera)
|
|
15
|
+
* await manager.init()
|
|
16
|
+
*
|
|
17
|
+
* // Toggle anytime
|
|
18
|
+
* manager.setMode('pathtracer')
|
|
19
|
+
* manager.setMode('raster')
|
|
20
|
+
*
|
|
21
|
+
* // In animation loop — replaces renderer.render()
|
|
22
|
+
* manager.render()
|
|
23
|
+
*
|
|
24
|
+
* // Optional: listen for mode/sample changes
|
|
25
|
+
* manager.onModeChange = (mode, samples) => updateUI(mode, samples)
|
|
26
|
+
*/
|
|
27
|
+
export declare class RenderManager {
|
|
28
|
+
readonly renderer: WebGLRenderer;
|
|
29
|
+
readonly scene: Scene;
|
|
30
|
+
readonly camera: Camera;
|
|
31
|
+
private _mode;
|
|
32
|
+
private _pt;
|
|
33
|
+
private _initialized;
|
|
34
|
+
/** LightPath multi-pass controller. Register materials with it to drive LightPath uniforms. */
|
|
35
|
+
readonly lightPath: LightPathController;
|
|
36
|
+
/** Called each frame with current mode and accumulated sample count */
|
|
37
|
+
onModeChange: ModeChangeCallback | null;
|
|
38
|
+
constructor(renderer: WebGLRenderer, scene: Scene, camera: Camera, options?: RenderManagerOptions);
|
|
39
|
+
get mode(): RenderMode;
|
|
40
|
+
get pathTracer(): PathTracerRenderer;
|
|
41
|
+
/**
|
|
42
|
+
* Must be called once before first render in path-tracer mode.
|
|
43
|
+
* Builds the scene BVH — async, but safe to call upfront.
|
|
44
|
+
*/
|
|
45
|
+
init(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Switch between 'raster' and 'pathtracer'.
|
|
48
|
+
* Switching to pathtracer initializes if not already done.
|
|
49
|
+
*/
|
|
50
|
+
setMode(mode: RenderMode): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Register a compiled ShaderMaterial (from st-shader-core) that uses the
|
|
53
|
+
* LightPath node. The LightPathController will drive its uniforms each frame.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const mat = myMaterialOutput.compile()
|
|
57
|
+
* manager.registerLightPathMaterial(mat)
|
|
58
|
+
* manager.render() // now drives LightPath uniforms each frame
|
|
59
|
+
*/
|
|
60
|
+
registerLightPathMaterial(mat: ShaderMaterial): void;
|
|
61
|
+
/**
|
|
62
|
+
* Call each frame instead of renderer.render().
|
|
63
|
+
* In raster mode: runs LightPath multi-pass render if any materials are registered,
|
|
64
|
+
* otherwise standard renderer.render().
|
|
65
|
+
* In pathtracer mode: path-tracer sample accumulation.
|
|
66
|
+
*/
|
|
67
|
+
render(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Call after changing scene geometry or materials so the path tracer
|
|
70
|
+
* rebuilds its BVH and resets accumulation.
|
|
71
|
+
*/
|
|
72
|
+
rebuildScene(): Promise<void>;
|
|
73
|
+
/** Returns 0 in raster mode, 1 when path tracer is active. */
|
|
74
|
+
getLightPathMode(): 0 | 1;
|
|
75
|
+
dispose(): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { PCFSoftShadowMap, ACESFilmicToneMapping } from 'three';
|
|
2
|
+
import { PathTracerRenderer } from './PathTracerRenderer.js';
|
|
3
|
+
import { LightPathController } from './LightPathController.js';
|
|
4
|
+
/**
|
|
5
|
+
* RenderManager — single entry point for raster ↔ path-tracer toggle.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const manager = new RenderManager(renderer, scene, camera)
|
|
9
|
+
* await manager.init()
|
|
10
|
+
*
|
|
11
|
+
* // Toggle anytime
|
|
12
|
+
* manager.setMode('pathtracer')
|
|
13
|
+
* manager.setMode('raster')
|
|
14
|
+
*
|
|
15
|
+
* // In animation loop — replaces renderer.render()
|
|
16
|
+
* manager.render()
|
|
17
|
+
*
|
|
18
|
+
* // Optional: listen for mode/sample changes
|
|
19
|
+
* manager.onModeChange = (mode, samples) => updateUI(mode, samples)
|
|
20
|
+
*/
|
|
21
|
+
export class RenderManager {
|
|
22
|
+
constructor(renderer, scene, camera, options = {}) {
|
|
23
|
+
this._initialized = false;
|
|
24
|
+
/** Called each frame with current mode and accumulated sample count */
|
|
25
|
+
this.onModeChange = null;
|
|
26
|
+
this.renderer = renderer;
|
|
27
|
+
this.scene = scene;
|
|
28
|
+
this.camera = camera;
|
|
29
|
+
this._mode = options.mode ?? 'raster';
|
|
30
|
+
this._pt = new PathTracerRenderer(renderer, options);
|
|
31
|
+
this.lightPath = new LightPathController(renderer);
|
|
32
|
+
// Ensure renderer is set up for quality rasterization
|
|
33
|
+
renderer.shadowMap.enabled = true;
|
|
34
|
+
renderer.shadowMap.type = PCFSoftShadowMap;
|
|
35
|
+
renderer.toneMapping = ACESFilmicToneMapping;
|
|
36
|
+
renderer.toneMappingExposure = 1.0;
|
|
37
|
+
}
|
|
38
|
+
get mode() { return this._mode; }
|
|
39
|
+
get pathTracer() { return this._pt; }
|
|
40
|
+
/**
|
|
41
|
+
* Must be called once before first render in path-tracer mode.
|
|
42
|
+
* Builds the scene BVH — async, but safe to call upfront.
|
|
43
|
+
*/
|
|
44
|
+
async init() {
|
|
45
|
+
await this._pt.setScene(this.scene, this.camera);
|
|
46
|
+
this._initialized = true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Switch between 'raster' and 'pathtracer'.
|
|
50
|
+
* Switching to pathtracer initializes if not already done.
|
|
51
|
+
*/
|
|
52
|
+
async setMode(mode) {
|
|
53
|
+
if (this._mode === mode)
|
|
54
|
+
return;
|
|
55
|
+
this._mode = mode;
|
|
56
|
+
if (mode === 'pathtracer' && !this._initialized) {
|
|
57
|
+
await this._pt.setScene(this.scene, this.camera);
|
|
58
|
+
this._initialized = true;
|
|
59
|
+
}
|
|
60
|
+
if (mode === 'pathtracer') {
|
|
61
|
+
this._pt.reset();
|
|
62
|
+
}
|
|
63
|
+
this.onModeChange?.(mode, 0);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register a compiled ShaderMaterial (from st-shader-core) that uses the
|
|
67
|
+
* LightPath node. The LightPathController will drive its uniforms each frame.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const mat = myMaterialOutput.compile()
|
|
71
|
+
* manager.registerLightPathMaterial(mat)
|
|
72
|
+
* manager.render() // now drives LightPath uniforms each frame
|
|
73
|
+
*/
|
|
74
|
+
registerLightPathMaterial(mat) {
|
|
75
|
+
this.lightPath.registerMaterial(mat);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Call each frame instead of renderer.render().
|
|
79
|
+
* In raster mode: runs LightPath multi-pass render if any materials are registered,
|
|
80
|
+
* otherwise standard renderer.render().
|
|
81
|
+
* In pathtracer mode: path-tracer sample accumulation.
|
|
82
|
+
*/
|
|
83
|
+
render() {
|
|
84
|
+
if (this._mode === 'pathtracer' && this._initialized) {
|
|
85
|
+
const samples = this._pt.render();
|
|
86
|
+
this.onModeChange?.(this._mode, samples);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.lightPath.render(this.scene, this.camera);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Call after changing scene geometry or materials so the path tracer
|
|
94
|
+
* rebuilds its BVH and resets accumulation.
|
|
95
|
+
*/
|
|
96
|
+
async rebuildScene() {
|
|
97
|
+
await this._pt.setScene(this.scene, this.camera);
|
|
98
|
+
this._initialized = true;
|
|
99
|
+
}
|
|
100
|
+
/** Returns 0 in raster mode, 1 when path tracer is active. */
|
|
101
|
+
getLightPathMode() {
|
|
102
|
+
return this._mode === 'pathtracer' ? 1 : 0;
|
|
103
|
+
}
|
|
104
|
+
dispose() {
|
|
105
|
+
this._pt.dispose();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PathTracerRenderer } from './PathTracerRenderer.js';
|
|
2
|
+
export type { PathTracerRendererOptions } from './PathTracerRenderer.js';
|
|
3
|
+
export { RenderManager } from './RenderManager.js';
|
|
4
|
+
export type { RenderMode, RenderManagerOptions, ModeChangeCallback } from './RenderManager.js';
|
|
5
|
+
export { LightPathController } from './LightPathController.js';
|
|
6
|
+
export type { LightPathRayType, LightPathState } from './LightPathController.js';
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triforge/pathtracer-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Path-tracer renderer for the three-js ecosystem — toggle between rasterizer and WebGPU path tracing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"test": "node test/run-tests.js"
|
|
16
|
+
},
|
|
17
|
+
"author": "Aftab Ibrahim Kazi",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"three": ">=0.159.0",
|
|
21
|
+
"three-gpu-pathtracer": ">=0.0.24",
|
|
22
|
+
"three-mesh-bvh": ">=0.7.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/three": "^0.165.0",
|
|
26
|
+
"three": "^0.165.0",
|
|
27
|
+
"typescript": "^5.3.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/AftabIbrahimKazi/triforge.git",
|
|
32
|
+
"directory": "st-pathtracer-core"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/AftabIbrahimKazi/triforge#readme",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|