-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathGPUCameraRenderer.ts
288 lines (257 loc) · 9.09 KB
/
GPUCameraRenderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import { GPURenderer, GPURendererParams, ProjectedMesh, RenderedMesh, SceneObject } from './GPURenderer'
import { Camera, CameraBasePerspectiveOptions } from '../camera/Camera'
import { BufferBinding } from '../bindings/BufferBinding'
import { BindGroup } from '../bindGroups/BindGroup'
import { Vec3 } from '../../math/Vec3'
import { AllowedBindGroups } from '../../types/BindGroups'
/**
* Parameters used to create a {@link GPUCameraRenderer}
*/
export interface GPUCameraRendererParams extends GPURendererParams {
/** An object defining {@link CameraBasePerspectiveOptions | camera perspective parameters} */
camera: CameraBasePerspectiveOptions
}
/**
* This renderer also creates a {@link Camera} and its associated {@link cameraBufferBinding | binding} and {@link cameraBindGroup | bind group}.<br>
* Can be safely used to render compute passes and meshes if they do not need to be tied to the DOM.
*
* @example
* ```javascript
* // first, we need a WebGPU device, that's what GPUDeviceManager is for
* const gpuDeviceManager = new GPUDeviceManager({
* label: 'Custom device manager',
* })
*
* // we need to wait for the WebGPU device to be created
* await gpuDeviceManager.init()
*
* // then we can create a camera renderer
* const gpuCameraRenderer = new GPUCameraRenderer({
* deviceManager: gpuDeviceManager, // we need the WebGPU device to create the renderer context
* container: document.querySelector('#canvas'),
* })
* ```
*/
export class GPUCameraRenderer extends GPURenderer {
/** {@link Camera} used by this {@link GPUCameraRenderer} */
camera: Camera
/** {@link BufferBinding | binding} handling the {@link camera} matrices */
cameraBufferBinding: BufferBinding
/** {@link BindGroup | bind group} handling the {@link cameraBufferBinding | camera buffer binding} */
cameraBindGroup: BindGroup
/** Options used to create this {@link GPUCameraRenderer} */
options: GPUCameraRendererParams
/**
* GPUCameraRenderer constructor
* @param parameters - {@link GPUCameraRendererParams | parameters} used to create this {@link GPUCameraRenderer}
*/
constructor({
deviceManager,
container,
pixelRatio = 1,
preferredFormat,
alphaMode = 'premultiplied',
renderPass,
camera = {},
}: GPUCameraRendererParams) {
super({
deviceManager,
container,
pixelRatio,
preferredFormat,
alphaMode,
renderPass,
})
this.type = 'GPUCameraRenderer'
camera = { ...{ fov: 50, near: 0.1, far: 150 }, ...camera }
this.options = {
...this.options,
camera,
}
this.setCamera(camera)
this.setCameraBindGroupAndBinding()
}
/**
* Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} is lost.
* Reset all our samplers, force all our scene objects and camera bind group to lose context.
*/
loseContext() {
super.loseContext()
// lose camera bind group context as well
this.cameraBindGroup.loseContext()
}
/**
* Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} should be restored.
* Configure the context again, resize the {@link core/renderPasses/RenderTarget.RenderTarget | render targets} and {@link core/textures/RenderTexture.RenderTexture | render textures}, restore our {@link renderedObjects | rendered objects} context, re-write our {@link cameraBufferBinding | camera buffer binding}.
* @async
*/
async restoreContext(): Promise<void> {
this.cameraBufferBinding.shouldUpdate = true
return super.restoreContext()
}
/**
* Set the {@link camera}
* @param cameraParameters - {@link CameraBasePerspectiveOptions | parameters} used to create the {@link camera}
*/
setCamera(cameraParameters: CameraBasePerspectiveOptions) {
const width = this.boundingRect ? this.boundingRect.width : 1
const height = this.boundingRect ? this.boundingRect.height : 1
this.camera = new Camera({
fov: cameraParameters.fov,
near: cameraParameters.near,
far: cameraParameters.far,
width,
height,
pixelRatio: this.pixelRatio,
onMatricesChanged: () => {
this.onCameraMatricesChanged()
},
})
}
/**
* Update the {@link ProjectedMesh | projected meshes} sizes and positions when the {@link camera} {@link Camera#position | position} changes
*/
onCameraMatricesChanged() {
this.updateCameraBindings()
this.meshes.forEach((mesh) => {
if ('modelViewMatrix' in mesh) {
mesh.shouldUpdateMatrixStack()
}
})
}
/**
* Set the {@link cameraBufferBinding | camera buffer binding} and {@link cameraBindGroup | camera bind group}
*/
setCameraBindGroupAndBinding() {
this.cameraBufferBinding = new BufferBinding({
label: 'Camera',
name: 'camera',
visibility: 'vertex',
struct: {
model: {
// camera model matrix
name: 'model',
type: 'mat4x4f',
value: this.camera.modelMatrix,
},
view: {
// camera view matrix
name: 'view',
type: 'mat4x4f',
value: this.camera.viewMatrix,
},
projection: {
// camera projection matrix
name: 'projection',
type: 'mat4x4f',
value: this.camera.projectionMatrix,
},
},
})
// now initialize bind group
this.cameraBindGroup = new BindGroup(this, {
label: 'Camera Uniform bind group',
bindings: [this.cameraBufferBinding],
})
}
/**
* Create the {@link cameraBindGroup | camera bind group} buffers
*/
setCameraBindGroup() {
if (this.cameraBindGroup && this.cameraBindGroup.shouldCreateBindGroup) {
this.cameraBindGroup.setIndex(0)
this.cameraBindGroup.createBindGroup()
}
}
/**
* Tell our {@link cameraBufferBinding | camera buffer binding} that we should update its struct
*/
updateCameraBindings() {
this.cameraBufferBinding?.shouldUpdateBinding('model')
this.cameraBufferBinding?.shouldUpdateBinding('view')
this.cameraBufferBinding?.shouldUpdateBinding('projection')
}
/**
* Get all objects ({@link RenderedMesh | rendered meshes} or {@link core/computePasses/ComputePass.ComputePass | compute passes}) using a given {@link AllowedBindGroups | bind group}, including {@link cameraBindGroup | camera bind group}.
* Useful to know if a resource is used by multiple objects and if it is safe to destroy it or not.
* @param bindGroup - {@link AllowedBindGroups | bind group} to check
*/
getObjectsByBindGroup(bindGroup: AllowedBindGroups): undefined | SceneObject[] {
return this.deviceRenderedObjects.filter((object) => {
return [
...object.material.bindGroups,
...object.material.inputsBindGroups,
...object.material.clonedBindGroups,
this.cameraBindGroup,
].some((bG) => bG.uuid === bindGroup.uuid)
})
}
/**
* Set our {@link camera} perspective matrix new parameters (fov, near plane and far plane)
* @param parameters - {@link CameraBasePerspectiveOptions | parameters} to use for the perspective
*/
setPerspective({ fov, near, far }: CameraBasePerspectiveOptions = {}) {
this.camera?.setPerspective({
fov,
near,
far,
width: this.boundingRect.width,
height: this.boundingRect.height,
pixelRatio: this.pixelRatio,
})
}
/**
* Set our {@link camera} {@link Camera#position | position}
* @param position - new {@link Camera#position | position}
*/
setCameraPosition(position: Vec3 = new Vec3(0, 0, 1)) {
this.camera.position.copy(position)
}
/**
* Call our {@link GPURenderer#onResize | GPURenderer onResize method} and resize our {@link camera} as well
*/
onResize() {
super.onResize()
this.setPerspective()
this.updateCameraBindings()
}
/* RENDER */
/**
* Update the camera model matrix, check if the {@link cameraBindGroup | camera bind group} should be created, create it if needed and then update it
*/
updateCamera() {
this.camera?.updateMatrixStack()
this.setCameraBindGroup()
this.cameraBindGroup?.update()
}
/**
* Render a single {@link RenderedMesh | mesh} (binds the {@link cameraBindGroup | camera bind group} if needed)
* @param commandEncoder - current {@link GPUCommandEncoder}
* @param mesh - {@link RenderedMesh | mesh} to render
*/
renderSingleMesh(commandEncoder: GPUCommandEncoder, mesh: RenderedMesh) {
const pass = commandEncoder.beginRenderPass(this.renderPass.descriptor)
// bind camera if needed
if (mesh.material.options.rendering.useProjection) {
pass.setBindGroup(this.cameraBindGroup.index, this.cameraBindGroup.bindGroup)
}
mesh.render(pass)
pass.end()
}
/**
* {@link updateCamera | Update the camera} and then call our {@link GPURenderer#render | GPURenderer render method}
* @param commandEncoder - current {@link GPUCommandEncoder}
*/
render(commandEncoder: GPUCommandEncoder) {
if (!this.ready) return
this.updateCamera()
super.render(commandEncoder)
}
/**
* Destroy our {@link GPUCameraRenderer}
*/
destroy() {
this.cameraBindGroup?.destroy()
super.destroy()
}
}