@eigong/three-effekseer 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/README.md +63 -0
- package/dist/index.d.ts +307 -0
- package/dist/index.js +321 -0
- package/index.d.ts +3 -0
- package/package.json +42 -0
- package/three-webgpu-hook.d.ts +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @eigong/three-effekseer
|
|
2
|
+
|
|
3
|
+
`@eigong/three-effekseer` is a WebGPU add-on that integrates Effekseer into a patched Three.js WebGPU renderer.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install three@npm:@eigong/three@0.183.1-external-render-pass-hook.1 @eigong/three-effekseer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Version Baseline
|
|
12
|
+
|
|
13
|
+
This add-on is currently based on Effekseer `1.80 b2`.
|
|
14
|
+
The required Three.js baseline is `@eigong/three@0.183.1-external-render-pass-hook.1`.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- The local Three.js fork must expose the external render pass hook implemented in:
|
|
19
|
+
- [WebGPURenderer.js](../../node_modules/three/src/renderers/webgpu/WebGPURenderer.js)
|
|
20
|
+
- [WebGPUBackend.js](../../node_modules/three/src/renderers/webgpu/WebGPUBackend.js)
|
|
21
|
+
- The Effekseer WebGPU runtime must already be loaded in the page.
|
|
22
|
+
|
|
23
|
+
## Canonical Example
|
|
24
|
+
|
|
25
|
+
The canonical consumer of the add-on is the vanilla example:
|
|
26
|
+
|
|
27
|
+
- HTML entry: [vanilla/index.html](../../react-vite-ts/vanilla/index.html)
|
|
28
|
+
- Integration: [vanilla-main.ts](../../react-vite-ts/src/vanilla-main.ts)
|
|
29
|
+
|
|
30
|
+
The React example in [ThreeEffekseerCanvas.tsx](../../react-vite-ts/src/ThreeEffekseerCanvas.tsx) consumes the same add-on, but it is not the reference integration.
|
|
31
|
+
|
|
32
|
+
## Public API
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { EffekseerRenderPass } from '@eigong/three-effekseer'
|
|
36
|
+
|
|
37
|
+
const pass = new EffekseerRenderPass(
|
|
38
|
+
renderer,
|
|
39
|
+
scene,
|
|
40
|
+
camera,
|
|
41
|
+
ctx,
|
|
42
|
+
{ mode: 'composite' }
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const capabilities = pass.getCapabilities()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This mirrors the WebGL-side integration format by exposing a dedicated `EffekseerRenderPass` on the Three side while keeping the Effekseer context external.
|
|
49
|
+
|
|
50
|
+
`mode` defaults to `'basic'`.
|
|
51
|
+
|
|
52
|
+
## Modes
|
|
53
|
+
|
|
54
|
+
| Mode | Distortion | Depth Occlusion | Soft Particles | LOD | Collisions | Notes |
|
|
55
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
56
|
+
| `basic` | No | Yes | No | No | No | Effekseer is injected into the primary scene render pass. Lowest cost path. |
|
|
57
|
+
| `composite` | Yes | Yes | No | No | No | Uses scene capture for background refraction, renders the scene again into a composite target, then presents through `PostProcessing`. |
|
|
58
|
+
|
|
59
|
+
## Unsupported Features
|
|
60
|
+
|
|
61
|
+
- `Soft particles`: not supported yet. The add-on does not expose a sampleable scene depth texture to Effekseer.
|
|
62
|
+
- `LOD`: not supported. The add-on does not currently provide a Three-side feature contract for effect level-of-detail selection.
|
|
63
|
+
- `Collisions`: not supported. The add-on does not currently bridge Three scene collision data or collision callbacks into Effekseer.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import * as THREE4 from 'three/webgpu';
|
|
2
|
+
|
|
3
|
+
// common.ts
|
|
4
|
+
function updateCameraProjection(camera, width, height) {
|
|
5
|
+
const cameraWithProjection = camera;
|
|
6
|
+
if (cameraWithProjection.isPerspectiveCamera === true && typeof cameraWithProjection.aspect === "number") {
|
|
7
|
+
cameraWithProjection.aspect = width / height;
|
|
8
|
+
}
|
|
9
|
+
cameraWithProjection.updateProjectionMatrix?.();
|
|
10
|
+
}
|
|
11
|
+
function syncEffekseerCamera(camera, effekseer) {
|
|
12
|
+
camera.updateMatrixWorld();
|
|
13
|
+
effekseer.setProjectionMatrix(camera.projectionMatrix.elements);
|
|
14
|
+
effekseer.setCameraMatrix(camera.matrixWorldInverse.elements);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// createBasicPass.ts
|
|
18
|
+
function isPrimaryScenePass(camera, info) {
|
|
19
|
+
return info.camera === camera;
|
|
20
|
+
}
|
|
21
|
+
function createBasicPass(init) {
|
|
22
|
+
const { renderer, scene, camera, effekseer } = init;
|
|
23
|
+
const capabilities = {
|
|
24
|
+
mode: "basic",
|
|
25
|
+
supportsDistortion: false,
|
|
26
|
+
supportsDepthOcclusion: true,
|
|
27
|
+
supportsSoftParticles: false,
|
|
28
|
+
supportsLOD: false,
|
|
29
|
+
supportsCollisions: false
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
getCapabilities() {
|
|
33
|
+
return capabilities;
|
|
34
|
+
},
|
|
35
|
+
resize(width, height, pixelRatio) {
|
|
36
|
+
renderer.setPixelRatio(pixelRatio);
|
|
37
|
+
renderer.setSize(width, height, false);
|
|
38
|
+
updateCameraProjection(camera, width, height);
|
|
39
|
+
},
|
|
40
|
+
render(deltaFrames) {
|
|
41
|
+
const previousHook = renderer.getExternalRenderPassHook();
|
|
42
|
+
renderer.setExternalRenderPassHook((info) => {
|
|
43
|
+
previousHook?.(info);
|
|
44
|
+
if (!isPrimaryScenePass(camera, info)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
syncEffekseerCamera(camera, effekseer);
|
|
48
|
+
effekseer.drawExternal(info.renderPassEncoder, {
|
|
49
|
+
colorFormat: info.colorFormat,
|
|
50
|
+
depthFormat: info.depthStencilFormat,
|
|
51
|
+
sampleCount: info.sampleCount
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
syncEffekseerCamera(camera, effekseer);
|
|
56
|
+
effekseer.update(deltaFrames);
|
|
57
|
+
renderer.render(scene, camera);
|
|
58
|
+
} finally {
|
|
59
|
+
renderer.setExternalRenderPassHook(previousHook);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
dispose() {
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function isCompositePass(info, renderTarget, camera) {
|
|
67
|
+
return info.renderTarget === renderTarget && info.camera === camera;
|
|
68
|
+
}
|
|
69
|
+
function createCompositePassPresenter(init) {
|
|
70
|
+
const { renderer, scene, camera, renderTarget, resolveCompositePassState } = init;
|
|
71
|
+
return {
|
|
72
|
+
render(input) {
|
|
73
|
+
const previousHook = renderer.getExternalRenderPassHook();
|
|
74
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
|
75
|
+
const previousActiveCubeFace = renderer.getActiveCubeFace();
|
|
76
|
+
const previousActiveMipmapLevel = renderer.getActiveMipmapLevel();
|
|
77
|
+
renderer.setRenderTarget(renderTarget);
|
|
78
|
+
renderer.setExternalRenderPassHook((info) => {
|
|
79
|
+
previousHook?.(info);
|
|
80
|
+
if (!isCompositePass(info, renderTarget, camera)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const compositePassState = resolveCompositePassState(input, info);
|
|
84
|
+
if (!compositePassState) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
input.effekseer.drawExternal(info.renderPassEncoder, compositePassState.effekseerPassState);
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
renderer.render(scene, camera);
|
|
91
|
+
} finally {
|
|
92
|
+
renderer.setExternalRenderPassHook(previousHook);
|
|
93
|
+
renderer.setRenderTarget(
|
|
94
|
+
previousRenderTarget,
|
|
95
|
+
previousActiveCubeFace,
|
|
96
|
+
previousActiveMipmapLevel
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
resize() {
|
|
101
|
+
},
|
|
102
|
+
dispose() {
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function createFinalPassPresenter(init) {
|
|
107
|
+
const { renderer } = init;
|
|
108
|
+
const postProcessing = new THREE4.PostProcessing(renderer);
|
|
109
|
+
let sourceTexture = null;
|
|
110
|
+
const updateOutputNode = (texture) => {
|
|
111
|
+
if (sourceTexture === texture) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
sourceTexture = texture;
|
|
115
|
+
postProcessing.outputNode = THREE4.TSL.texture(texture);
|
|
116
|
+
postProcessing.needsUpdate = true;
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
render(texture) {
|
|
120
|
+
updateOutputNode(texture);
|
|
121
|
+
postProcessing.render();
|
|
122
|
+
},
|
|
123
|
+
resize() {
|
|
124
|
+
postProcessing.needsUpdate = true;
|
|
125
|
+
},
|
|
126
|
+
dispose() {
|
|
127
|
+
postProcessing.dispose();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function getNativeGPUTexture(renderer, texture) {
|
|
132
|
+
const nativeTexture = renderer.backend.get(texture).texture;
|
|
133
|
+
return nativeTexture ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// createCompositePass.ts
|
|
137
|
+
function createSceneCaptureTrigger(renderer, scenePass, samples) {
|
|
138
|
+
const postProcessing = new THREE4.PostProcessing(renderer);
|
|
139
|
+
const triggerTarget = new THREE4.RenderTarget(1, 1, {
|
|
140
|
+
type: renderer.getOutputBufferType(),
|
|
141
|
+
colorSpace: THREE4.LinearSRGBColorSpace,
|
|
142
|
+
depthBuffer: false,
|
|
143
|
+
samples
|
|
144
|
+
});
|
|
145
|
+
triggerTarget.texture.name = "ThreeEffekseer.captureTrigger";
|
|
146
|
+
postProcessing.outputColorTransform = false;
|
|
147
|
+
postProcessing.outputNode = scenePass.getTextureNode("output");
|
|
148
|
+
return {
|
|
149
|
+
render() {
|
|
150
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
|
151
|
+
const previousActiveCubeFace = renderer.getActiveCubeFace();
|
|
152
|
+
const previousActiveMipmapLevel = renderer.getActiveMipmapLevel();
|
|
153
|
+
renderer.setRenderTarget(triggerTarget);
|
|
154
|
+
try {
|
|
155
|
+
postProcessing.render();
|
|
156
|
+
} finally {
|
|
157
|
+
renderer.setRenderTarget(
|
|
158
|
+
previousRenderTarget,
|
|
159
|
+
previousActiveCubeFace,
|
|
160
|
+
previousActiveMipmapLevel
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
resize() {
|
|
165
|
+
},
|
|
166
|
+
dispose() {
|
|
167
|
+
postProcessing.dispose();
|
|
168
|
+
triggerTarget.dispose();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function syncScenePassResources(renderer, scenePassResources) {
|
|
173
|
+
const renderTarget = scenePassResources.scenePass?.renderTarget ?? null;
|
|
174
|
+
const colorTexture = renderTarget?.texture ?? null;
|
|
175
|
+
const depthTexture = renderTarget?.depthTexture ?? null;
|
|
176
|
+
scenePassResources.renderTarget = renderTarget;
|
|
177
|
+
scenePassResources.colorTexture = colorTexture;
|
|
178
|
+
scenePassResources.depthTexture = depthTexture;
|
|
179
|
+
scenePassResources.nativeColorTexture = colorTexture ? getNativeGPUTexture(renderer, colorTexture) : null;
|
|
180
|
+
scenePassResources.nativeColorTextureView = scenePassResources.nativeColorTexture?.createView() ?? null;
|
|
181
|
+
scenePassResources.nativeDepthTexture = depthTexture ? getNativeGPUTexture(renderer, depthTexture) : null;
|
|
182
|
+
scenePassResources.nativeDepthTextureView = null;
|
|
183
|
+
}
|
|
184
|
+
function createFinalPassState(renderer, scenePassResources, info) {
|
|
185
|
+
syncScenePassResources(renderer, scenePassResources);
|
|
186
|
+
const backgroundTextureView = scenePassResources.nativeColorTextureView;
|
|
187
|
+
return {
|
|
188
|
+
backgroundTextureView,
|
|
189
|
+
depthTextureView: null,
|
|
190
|
+
effekseerPassState: {
|
|
191
|
+
colorFormat: info.colorFormat,
|
|
192
|
+
depthFormat: info.depthStencilFormat,
|
|
193
|
+
sampleCount: info.sampleCount,
|
|
194
|
+
...backgroundTextureView ? { backgroundTextureView } : {}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function createCompositePass(init) {
|
|
199
|
+
const { renderer, scene, camera, effekseer } = init;
|
|
200
|
+
const samples = Math.max(0, renderer.samples || 0);
|
|
201
|
+
const capabilities = {
|
|
202
|
+
mode: "composite",
|
|
203
|
+
supportsDistortion: true,
|
|
204
|
+
supportsDepthOcclusion: true,
|
|
205
|
+
supportsSoftParticles: false,
|
|
206
|
+
supportsLOD: false,
|
|
207
|
+
supportsCollisions: false
|
|
208
|
+
};
|
|
209
|
+
const scenePass = THREE4.TSL.pass(scene, camera);
|
|
210
|
+
const sceneCaptureTrigger = createSceneCaptureTrigger(renderer, scenePass, samples);
|
|
211
|
+
const compositionTarget = new THREE4.RenderTarget(1, 1, {
|
|
212
|
+
type: renderer.getOutputBufferType(),
|
|
213
|
+
colorSpace: THREE4.LinearSRGBColorSpace,
|
|
214
|
+
depthBuffer: true,
|
|
215
|
+
samples
|
|
216
|
+
});
|
|
217
|
+
compositionTarget.texture.name = "ThreeEffekseer.composition";
|
|
218
|
+
const drawingBufferSize = new THREE4.Vector2();
|
|
219
|
+
const scenePassResources = {
|
|
220
|
+
scenePass,
|
|
221
|
+
renderTarget: scenePass.renderTarget,
|
|
222
|
+
colorTexture: scenePass.renderTarget.texture,
|
|
223
|
+
depthTexture: scenePass.renderTarget.depthTexture,
|
|
224
|
+
nativeColorTexture: null,
|
|
225
|
+
nativeColorTextureView: null,
|
|
226
|
+
nativeDepthTexture: null,
|
|
227
|
+
nativeDepthTextureView: null
|
|
228
|
+
};
|
|
229
|
+
const compositePresenter = createCompositePassPresenter({
|
|
230
|
+
renderer,
|
|
231
|
+
scene,
|
|
232
|
+
camera,
|
|
233
|
+
renderTarget: compositionTarget,
|
|
234
|
+
resolveCompositePassState: (_input, info) => createFinalPassState(renderer, scenePassResources, info)
|
|
235
|
+
});
|
|
236
|
+
const presenter = createFinalPassPresenter({ renderer });
|
|
237
|
+
return {
|
|
238
|
+
getCapabilities() {
|
|
239
|
+
return capabilities;
|
|
240
|
+
},
|
|
241
|
+
resize(width, height, pixelRatio) {
|
|
242
|
+
renderer.setPixelRatio(pixelRatio);
|
|
243
|
+
renderer.setSize(width, height, false);
|
|
244
|
+
renderer.getDrawingBufferSize(drawingBufferSize);
|
|
245
|
+
compositionTarget.setSize(drawingBufferSize.width, drawingBufferSize.height);
|
|
246
|
+
updateCameraProjection(camera, width, height);
|
|
247
|
+
presenter.resize(width, height, pixelRatio);
|
|
248
|
+
},
|
|
249
|
+
render(deltaFrames) {
|
|
250
|
+
syncEffekseerCamera(camera, effekseer);
|
|
251
|
+
effekseer.update(deltaFrames);
|
|
252
|
+
sceneCaptureTrigger.render();
|
|
253
|
+
scenePassResources.renderTarget = scenePass.renderTarget;
|
|
254
|
+
scenePassResources.colorTexture = scenePass.renderTarget.texture;
|
|
255
|
+
scenePassResources.depthTexture = scenePass.renderTarget.depthTexture;
|
|
256
|
+
compositePresenter.render({
|
|
257
|
+
scenePassResources,
|
|
258
|
+
effekseer
|
|
259
|
+
});
|
|
260
|
+
presenter.render(compositionTarget.texture);
|
|
261
|
+
},
|
|
262
|
+
dispose() {
|
|
263
|
+
sceneCaptureTrigger.dispose();
|
|
264
|
+
presenter.dispose();
|
|
265
|
+
compositionTarget.dispose();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ThreeEffekseerPass.ts
|
|
271
|
+
function createThreeEffekseerPass(init, options = {}) {
|
|
272
|
+
if (options.mode === "composite") {
|
|
273
|
+
return createCompositePass(init);
|
|
274
|
+
}
|
|
275
|
+
return createBasicPass(init);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// EffekseerRenderPass.ts
|
|
279
|
+
var EffekseerRenderPass = class {
|
|
280
|
+
pass;
|
|
281
|
+
constructor(renderer, scene, camera, effekseer, options = {}) {
|
|
282
|
+
this.pass = createThreeEffekseerPass(
|
|
283
|
+
{
|
|
284
|
+
renderer,
|
|
285
|
+
scene,
|
|
286
|
+
camera,
|
|
287
|
+
effekseer
|
|
288
|
+
},
|
|
289
|
+
options
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
getCapabilities() {
|
|
293
|
+
return this.pass.getCapabilities();
|
|
294
|
+
}
|
|
295
|
+
setSize(width, height, pixelRatio = 1) {
|
|
296
|
+
this.pass.resize(width, height, pixelRatio);
|
|
297
|
+
}
|
|
298
|
+
render(deltaFrames = 1) {
|
|
299
|
+
this.pass.render(deltaFrames);
|
|
300
|
+
}
|
|
301
|
+
dispose() {
|
|
302
|
+
this.pass.dispose();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var EffekseerRenderPass_default = EffekseerRenderPass;
|
|
306
|
+
|
|
307
|
+
export { EffekseerRenderPass_default as EffekseerRenderPass };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// common.ts
|
|
2
|
+
function updateCameraProjection(camera, width, height) {
|
|
3
|
+
const cameraWithProjection = camera;
|
|
4
|
+
if (cameraWithProjection.isPerspectiveCamera === true && typeof cameraWithProjection.aspect === "number") {
|
|
5
|
+
cameraWithProjection.aspect = width / height;
|
|
6
|
+
}
|
|
7
|
+
cameraWithProjection.updateProjectionMatrix?.();
|
|
8
|
+
}
|
|
9
|
+
function syncEffekseerCamera(camera, effekseer) {
|
|
10
|
+
camera.updateMatrixWorld();
|
|
11
|
+
effekseer.setProjectionMatrix(camera.projectionMatrix.elements);
|
|
12
|
+
effekseer.setCameraMatrix(camera.matrixWorldInverse.elements);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// createBasicPass.ts
|
|
16
|
+
function isPrimaryScenePass(camera, info) {
|
|
17
|
+
return info.camera === camera;
|
|
18
|
+
}
|
|
19
|
+
function createBasicPass(init) {
|
|
20
|
+
const { renderer, scene, camera, effekseer } = init;
|
|
21
|
+
const capabilities = {
|
|
22
|
+
mode: "basic",
|
|
23
|
+
supportsDistortion: false,
|
|
24
|
+
supportsDepthOcclusion: true,
|
|
25
|
+
supportsSoftParticles: false,
|
|
26
|
+
supportsLOD: false,
|
|
27
|
+
supportsCollisions: false
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
getCapabilities() {
|
|
31
|
+
return capabilities;
|
|
32
|
+
},
|
|
33
|
+
resize(width, height, pixelRatio) {
|
|
34
|
+
renderer.setPixelRatio(pixelRatio);
|
|
35
|
+
renderer.setSize(width, height, false);
|
|
36
|
+
updateCameraProjection(camera, width, height);
|
|
37
|
+
},
|
|
38
|
+
render(deltaFrames) {
|
|
39
|
+
const previousHook = renderer.getExternalRenderPassHook();
|
|
40
|
+
renderer.setExternalRenderPassHook((info) => {
|
|
41
|
+
previousHook?.(info);
|
|
42
|
+
if (!isPrimaryScenePass(camera, info)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
syncEffekseerCamera(camera, effekseer);
|
|
46
|
+
effekseer.drawExternal(info.renderPassEncoder, {
|
|
47
|
+
colorFormat: info.colorFormat,
|
|
48
|
+
depthFormat: info.depthStencilFormat,
|
|
49
|
+
sampleCount: info.sampleCount
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
syncEffekseerCamera(camera, effekseer);
|
|
54
|
+
effekseer.update(deltaFrames);
|
|
55
|
+
renderer.render(scene, camera);
|
|
56
|
+
} finally {
|
|
57
|
+
renderer.setExternalRenderPassHook(previousHook);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
dispose() {
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// createCompositePass.ts
|
|
66
|
+
import * as THREE4 from "three/webgpu";
|
|
67
|
+
|
|
68
|
+
// createCompositePassPresenter.ts
|
|
69
|
+
import "three/webgpu";
|
|
70
|
+
function isCompositePass(info, renderTarget, camera) {
|
|
71
|
+
return info.renderTarget === renderTarget && info.camera === camera;
|
|
72
|
+
}
|
|
73
|
+
function createCompositePassPresenter(init) {
|
|
74
|
+
const { renderer, scene, camera, renderTarget, resolveCompositePassState } = init;
|
|
75
|
+
return {
|
|
76
|
+
render(input) {
|
|
77
|
+
const previousHook = renderer.getExternalRenderPassHook();
|
|
78
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
|
79
|
+
const previousActiveCubeFace = renderer.getActiveCubeFace();
|
|
80
|
+
const previousActiveMipmapLevel = renderer.getActiveMipmapLevel();
|
|
81
|
+
renderer.setRenderTarget(renderTarget);
|
|
82
|
+
renderer.setExternalRenderPassHook((info) => {
|
|
83
|
+
previousHook?.(info);
|
|
84
|
+
if (!isCompositePass(info, renderTarget, camera)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const compositePassState = resolveCompositePassState(input, info);
|
|
88
|
+
if (!compositePassState) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
input.effekseer.drawExternal(info.renderPassEncoder, compositePassState.effekseerPassState);
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
renderer.render(scene, camera);
|
|
95
|
+
} finally {
|
|
96
|
+
renderer.setExternalRenderPassHook(previousHook);
|
|
97
|
+
renderer.setRenderTarget(
|
|
98
|
+
previousRenderTarget,
|
|
99
|
+
previousActiveCubeFace,
|
|
100
|
+
previousActiveMipmapLevel
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
resize() {
|
|
105
|
+
},
|
|
106
|
+
dispose() {
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// createFinalPassPresenter.ts
|
|
112
|
+
import * as THREE2 from "three/webgpu";
|
|
113
|
+
function createFinalPassPresenter(init) {
|
|
114
|
+
const { renderer } = init;
|
|
115
|
+
const postProcessing = new THREE2.PostProcessing(renderer);
|
|
116
|
+
let sourceTexture = null;
|
|
117
|
+
const updateOutputNode = (texture) => {
|
|
118
|
+
if (sourceTexture === texture) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
sourceTexture = texture;
|
|
122
|
+
postProcessing.outputNode = THREE2.TSL.texture(texture);
|
|
123
|
+
postProcessing.needsUpdate = true;
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
render(texture) {
|
|
127
|
+
updateOutputNode(texture);
|
|
128
|
+
postProcessing.render();
|
|
129
|
+
},
|
|
130
|
+
resize() {
|
|
131
|
+
postProcessing.needsUpdate = true;
|
|
132
|
+
},
|
|
133
|
+
dispose() {
|
|
134
|
+
postProcessing.dispose();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// webgpuInternals.ts
|
|
140
|
+
import "three/webgpu";
|
|
141
|
+
function getNativeGPUTexture(renderer, texture) {
|
|
142
|
+
const nativeTexture = renderer.backend.get(texture).texture;
|
|
143
|
+
return nativeTexture ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// createCompositePass.ts
|
|
147
|
+
function createSceneCaptureTrigger(renderer, scenePass, samples) {
|
|
148
|
+
const postProcessing = new THREE4.PostProcessing(renderer);
|
|
149
|
+
const triggerTarget = new THREE4.RenderTarget(1, 1, {
|
|
150
|
+
type: renderer.getOutputBufferType(),
|
|
151
|
+
colorSpace: THREE4.LinearSRGBColorSpace,
|
|
152
|
+
depthBuffer: false,
|
|
153
|
+
samples
|
|
154
|
+
});
|
|
155
|
+
triggerTarget.texture.name = "ThreeEffekseer.captureTrigger";
|
|
156
|
+
postProcessing.outputColorTransform = false;
|
|
157
|
+
postProcessing.outputNode = scenePass.getTextureNode("output");
|
|
158
|
+
return {
|
|
159
|
+
render() {
|
|
160
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
|
161
|
+
const previousActiveCubeFace = renderer.getActiveCubeFace();
|
|
162
|
+
const previousActiveMipmapLevel = renderer.getActiveMipmapLevel();
|
|
163
|
+
renderer.setRenderTarget(triggerTarget);
|
|
164
|
+
try {
|
|
165
|
+
postProcessing.render();
|
|
166
|
+
} finally {
|
|
167
|
+
renderer.setRenderTarget(
|
|
168
|
+
previousRenderTarget,
|
|
169
|
+
previousActiveCubeFace,
|
|
170
|
+
previousActiveMipmapLevel
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
resize() {
|
|
175
|
+
},
|
|
176
|
+
dispose() {
|
|
177
|
+
postProcessing.dispose();
|
|
178
|
+
triggerTarget.dispose();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function syncScenePassResources(renderer, scenePassResources) {
|
|
183
|
+
const renderTarget = scenePassResources.scenePass?.renderTarget ?? null;
|
|
184
|
+
const colorTexture = renderTarget?.texture ?? null;
|
|
185
|
+
const depthTexture = renderTarget?.depthTexture ?? null;
|
|
186
|
+
scenePassResources.renderTarget = renderTarget;
|
|
187
|
+
scenePassResources.colorTexture = colorTexture;
|
|
188
|
+
scenePassResources.depthTexture = depthTexture;
|
|
189
|
+
scenePassResources.nativeColorTexture = colorTexture ? getNativeGPUTexture(renderer, colorTexture) : null;
|
|
190
|
+
scenePassResources.nativeColorTextureView = scenePassResources.nativeColorTexture?.createView() ?? null;
|
|
191
|
+
scenePassResources.nativeDepthTexture = depthTexture ? getNativeGPUTexture(renderer, depthTexture) : null;
|
|
192
|
+
scenePassResources.nativeDepthTextureView = null;
|
|
193
|
+
}
|
|
194
|
+
function createFinalPassState(renderer, scenePassResources, info) {
|
|
195
|
+
syncScenePassResources(renderer, scenePassResources);
|
|
196
|
+
const backgroundTextureView = scenePassResources.nativeColorTextureView;
|
|
197
|
+
return {
|
|
198
|
+
backgroundTextureView,
|
|
199
|
+
depthTextureView: null,
|
|
200
|
+
effekseerPassState: {
|
|
201
|
+
colorFormat: info.colorFormat,
|
|
202
|
+
depthFormat: info.depthStencilFormat,
|
|
203
|
+
sampleCount: info.sampleCount,
|
|
204
|
+
...backgroundTextureView ? { backgroundTextureView } : {}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createCompositePass(init) {
|
|
209
|
+
const { renderer, scene, camera, effekseer } = init;
|
|
210
|
+
const samples = Math.max(0, renderer.samples || 0);
|
|
211
|
+
const capabilities = {
|
|
212
|
+
mode: "composite",
|
|
213
|
+
supportsDistortion: true,
|
|
214
|
+
supportsDepthOcclusion: true,
|
|
215
|
+
supportsSoftParticles: false,
|
|
216
|
+
supportsLOD: false,
|
|
217
|
+
supportsCollisions: false
|
|
218
|
+
};
|
|
219
|
+
const scenePass = THREE4.TSL.pass(scene, camera);
|
|
220
|
+
const sceneCaptureTrigger = createSceneCaptureTrigger(renderer, scenePass, samples);
|
|
221
|
+
const compositionTarget = new THREE4.RenderTarget(1, 1, {
|
|
222
|
+
type: renderer.getOutputBufferType(),
|
|
223
|
+
colorSpace: THREE4.LinearSRGBColorSpace,
|
|
224
|
+
depthBuffer: true,
|
|
225
|
+
samples
|
|
226
|
+
});
|
|
227
|
+
compositionTarget.texture.name = "ThreeEffekseer.composition";
|
|
228
|
+
const drawingBufferSize = new THREE4.Vector2();
|
|
229
|
+
const scenePassResources = {
|
|
230
|
+
scenePass,
|
|
231
|
+
renderTarget: scenePass.renderTarget,
|
|
232
|
+
colorTexture: scenePass.renderTarget.texture,
|
|
233
|
+
depthTexture: scenePass.renderTarget.depthTexture,
|
|
234
|
+
nativeColorTexture: null,
|
|
235
|
+
nativeColorTextureView: null,
|
|
236
|
+
nativeDepthTexture: null,
|
|
237
|
+
nativeDepthTextureView: null
|
|
238
|
+
};
|
|
239
|
+
const compositePresenter = createCompositePassPresenter({
|
|
240
|
+
renderer,
|
|
241
|
+
scene,
|
|
242
|
+
camera,
|
|
243
|
+
renderTarget: compositionTarget,
|
|
244
|
+
resolveCompositePassState: (_input, info) => createFinalPassState(renderer, scenePassResources, info)
|
|
245
|
+
});
|
|
246
|
+
const presenter = createFinalPassPresenter({ renderer });
|
|
247
|
+
return {
|
|
248
|
+
getCapabilities() {
|
|
249
|
+
return capabilities;
|
|
250
|
+
},
|
|
251
|
+
resize(width, height, pixelRatio) {
|
|
252
|
+
renderer.setPixelRatio(pixelRatio);
|
|
253
|
+
renderer.setSize(width, height, false);
|
|
254
|
+
renderer.getDrawingBufferSize(drawingBufferSize);
|
|
255
|
+
compositionTarget.setSize(drawingBufferSize.width, drawingBufferSize.height);
|
|
256
|
+
updateCameraProjection(camera, width, height);
|
|
257
|
+
sceneCaptureTrigger.resize(width, height, pixelRatio);
|
|
258
|
+
compositePresenter.resize(width, height, pixelRatio);
|
|
259
|
+
presenter.resize(width, height, pixelRatio);
|
|
260
|
+
},
|
|
261
|
+
render(deltaFrames) {
|
|
262
|
+
syncEffekseerCamera(camera, effekseer);
|
|
263
|
+
effekseer.update(deltaFrames);
|
|
264
|
+
sceneCaptureTrigger.render();
|
|
265
|
+
scenePassResources.renderTarget = scenePass.renderTarget;
|
|
266
|
+
scenePassResources.colorTexture = scenePass.renderTarget.texture;
|
|
267
|
+
scenePassResources.depthTexture = scenePass.renderTarget.depthTexture;
|
|
268
|
+
compositePresenter.render({
|
|
269
|
+
scenePassResources,
|
|
270
|
+
effekseer
|
|
271
|
+
});
|
|
272
|
+
presenter.render(compositionTarget.texture);
|
|
273
|
+
},
|
|
274
|
+
dispose() {
|
|
275
|
+
sceneCaptureTrigger.dispose();
|
|
276
|
+
compositePresenter.dispose();
|
|
277
|
+
presenter.dispose();
|
|
278
|
+
compositionTarget.dispose();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ThreeEffekseerPass.ts
|
|
284
|
+
function createThreeEffekseerPass(init, options = {}) {
|
|
285
|
+
if (options.mode === "composite") {
|
|
286
|
+
return createCompositePass(init);
|
|
287
|
+
}
|
|
288
|
+
return createBasicPass(init);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// EffekseerRenderPass.ts
|
|
292
|
+
var EffekseerRenderPass = class {
|
|
293
|
+
pass;
|
|
294
|
+
constructor(renderer, scene, camera, effekseer, options = {}) {
|
|
295
|
+
this.pass = createThreeEffekseerPass(
|
|
296
|
+
{
|
|
297
|
+
renderer,
|
|
298
|
+
scene,
|
|
299
|
+
camera,
|
|
300
|
+
effekseer
|
|
301
|
+
},
|
|
302
|
+
options
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
getCapabilities() {
|
|
306
|
+
return this.pass.getCapabilities();
|
|
307
|
+
}
|
|
308
|
+
setSize(width, height, pixelRatio = 1) {
|
|
309
|
+
this.pass.resize(width, height, pixelRatio);
|
|
310
|
+
}
|
|
311
|
+
render(deltaFrames = 1) {
|
|
312
|
+
this.pass.render(deltaFrames);
|
|
313
|
+
}
|
|
314
|
+
dispose() {
|
|
315
|
+
this.pass.dispose();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var EffekseerRenderPass_default = EffekseerRenderPass;
|
|
319
|
+
export {
|
|
320
|
+
EffekseerRenderPass_default as EffekseerRenderPass
|
|
321
|
+
};
|
package/index.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eigong/three-effekseer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebGPU add-on that integrates Effekseer into a patched Three.js renderer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"three-webgpu-hook.d.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup index.ts --format esm --dts --clean",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"effekseer",
|
|
28
|
+
"three",
|
|
29
|
+
"threejs",
|
|
30
|
+
"webgpu"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"three": ">=0.183.1 <0.184.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsup": "^8.5.0",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'three/webgpu'
|
|
2
|
+
import type { Camera, RenderTarget } from 'three'
|
|
3
|
+
|
|
4
|
+
declare module 'three/webgpu' {
|
|
5
|
+
export interface ExternalRenderPassHookInfo {
|
|
6
|
+
renderer: WebGPURenderer
|
|
7
|
+
renderPassEncoder: GPURenderPassEncoder
|
|
8
|
+
commandEncoder: GPUCommandEncoder
|
|
9
|
+
colorFormat: GPUTextureFormat
|
|
10
|
+
depthStencilFormat: GPUTextureFormat | null
|
|
11
|
+
sampleCount: number
|
|
12
|
+
isDefaultCanvasTarget: boolean
|
|
13
|
+
renderTarget: RenderTarget | null
|
|
14
|
+
camera: Camera | null
|
|
15
|
+
colorAttachmentView: GPUTextureView
|
|
16
|
+
resolveTargetView: GPUTextureView | null
|
|
17
|
+
depthStencilAttachmentView: GPUTextureView | null
|
|
18
|
+
sampleableColorTextureView: GPUTextureView | null
|
|
19
|
+
sampleableDepthTextureView: GPUTextureView | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ExternalRenderPassHook = (info: ExternalRenderPassHookInfo) => void
|
|
23
|
+
|
|
24
|
+
export interface WebGPURenderer {
|
|
25
|
+
setExternalRenderPassHook(callback: ExternalRenderPassHook | null): this
|
|
26
|
+
getExternalRenderPassHook(): ExternalRenderPassHook | null
|
|
27
|
+
}
|
|
28
|
+
}
|