playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
154 lines (151 loc) • 7.8 kB
JavaScript
import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
import { BlendState } from '../../platform/graphics/blend-state.js';
import { RenderPass } from '../../platform/graphics/render-pass.js';
import { SHADER_DEPTH_PICK, SHADER_PICK } from '../../scene/constants.js';
/**
* @import { BindGroup } from '../../platform/graphics/bind-group.js'
* @import { CameraComponent } from '../components/camera/component.js'
* @import { Scene } from '../../scene/scene.js'
* @import { Layer } from '../../scene/layer.js'
* @import { MeshInstance } from '../../scene/mesh-instance.js'
* @import { GSplatComponent } from '../components/gsplat/component.js'
*/ const tempMeshInstances = [];
const lights = [
[],
[],
[]
];
const defaultShadowAtlasParams = new Float32Array(2);
/**
* A render pass implementing rendering of mesh instances into a pick buffer.
*
* @ignore
*/ class RenderPassPicker extends RenderPass {
destroy() {
this.viewBindGroups.forEach((bg)=>{
bg.defaultUniformBuffer.destroy();
bg.destroy();
});
this.viewBindGroups.length = 0;
}
/**
* @param {CameraComponent} camera - The camera component used for picking.
* @param {Scene} scene - The scene to pick from.
* @param {Layer[]} layers - The layers to pick from.
* @param {Map<number, MeshInstance | GSplatComponent>} mapping - Map to store ID to object mappings.
* @param {boolean} depth - Whether to render depth information.
*/ update(camera, scene, layers, mapping, depth) {
this.camera = camera;
this.scene = scene;
this.layers = layers;
this.mapping = mapping;
this.depth = depth;
if (scene.clusteredLightingEnabled) {
this.emptyWorldClusters = this.renderer.worldClustersAllocator.empty;
}
}
// Filter qualifying layers and prepare gsplat pick mesh instances for the compute-based
// renderer. The execute() loop iterates the pre-built list instead of re-filtering.
before() {
this._qualifiedLayerIndices.length = 0;
this._pickMeshInstances.clear();
const { camera, scene, layers } = this;
const srcLayers = scene.layers.layerList;
const subLayerEnabled = scene.layers.subLayerEnabled;
const gsplatDirector = this.renderer.gsplatDirector;
const pickerWidth = this.renderTarget?.width ?? 1;
const pickerHeight = this.renderTarget?.height ?? 1;
for(let i = 0; i < srcLayers.length; i++){
const srcLayer = srcLayers[i];
if (layers && layers.indexOf(srcLayer) < 0) continue;
if (!srcLayer.enabled || !subLayerEnabled[i]) continue;
if (!srcLayer.camerasSet.has(camera.camera)) continue;
// store the index of the layers we need to render
this._qualifiedLayerIndices.push(i);
// kick off a compute tiled renderer for the gsplat manager on this layer, and store the mesh instance
// which copies the results to the pick buffer
if (gsplatDirector) {
const pickMI = gsplatDirector.prepareForPicking(camera.camera, pickerWidth, pickerHeight, srcLayer);
if (pickMI) {
this._pickMeshInstances.set(i, pickMI);
}
}
}
}
execute() {
const device = this.device;
const { renderer, camera, scene, mapping, renderTarget } = this;
const srcLayers = scene.layers.layerList;
const isTransparent = scene.layers.subLayerList;
for (const i of this._qualifiedLayerIndices){
const srcLayer = srcLayers[i];
const transparent = isTransparent[i];
DebugGraphics.pushGpuMarker(device, `${srcLayer.name}(${transparent ? 'TRANSP' : 'OPAQUE'})`);
// if the layer clears the depth
if (srcLayer._clearDepthBuffer) {
renderer.clear(camera.camera, false, true, false);
}
// Use mesh instances from the layer. Ideally we'd just pick culled instances for the camera,
// but we have no way of knowing if culling has been performed since changes to the layer.
// Disadvantage here is that we render all mesh instances, even those not visible by the camera.
const meshInstances = srcLayer.meshInstances;
// only need mesh instances with a pick flag
for(let j = 0; j < meshInstances.length; j++){
const meshInstance = meshInstances[j];
if (meshInstance.pick && meshInstance.transparent === transparent) {
tempMeshInstances.push(meshInstance);
// keep the index -> meshInstance index mapping
mapping.set(meshInstance.id, meshInstance);
}
}
// Inject gsplat pick mesh instance for this layer (compute-based renderer only)
const pickMI = this._pickMeshInstances.get(i);
if (pickMI) {
tempMeshInstances.push(pickMI);
}
// Process gsplat placements when ID is enabled
// The gsplat unified mesh instance is already handled above (added to layer.meshInstances)
// Here we just need to add the placement ID -> component mapping
if (scene.gsplat.enableIds) {
const placements = srcLayer.gsplatPlacements;
for(let j = 0; j < placements.length; j++){
const placement = placements[j];
const component = placement.node?.gsplat;
if (component) {
mapping.set(placement.id, component);
}
}
}
if (tempMeshInstances.length > 0) {
// upload clustered lights uniforms
const clusteredLightingEnabled = scene.clusteredLightingEnabled;
if (clusteredLightingEnabled) {
const lightClusters = this.emptyWorldClusters;
lightClusters.activate();
}
renderer.setCameraUniforms(camera.camera, renderTarget);
// TODO: These uniforms are not required by the picker pass, but it uses the
// forward view format which includes them. Ideally, each pass should be able
// to specify its own view format to avoid setting unnecessary uniforms.
renderer.dispatchGlobalLights(scene);
device.scope.resolve('shadowAtlasParams').setValue(defaultShadowAtlasParams);
if (device.supportsUniformBuffers) {
// Initialize view bind group format if not already done
renderer.initViewBindGroupFormat(clusteredLightingEnabled);
renderer.setupViewUniformBuffers(this.viewBindGroups, renderer.viewUniformFormat, renderer.viewBindGroupFormat, null);
}
const shaderPass = this.depth ? SHADER_DEPTH_PICK : SHADER_PICK;
renderer.renderForward(camera.camera, renderTarget, tempMeshInstances, lights, shaderPass, (meshInstance)=>{
device.setBlendState(this.blendState);
});
tempMeshInstances.length = 0;
}
DebugGraphics.popGpuMarker(device);
}
}
constructor(device, renderer){
super(device), /** @type {BindGroup[]} */ this.viewBindGroups = [], /** @type {BlendState} */ this.blendState = BlendState.NOBLEND, /** @type {number[]} */ this._qualifiedLayerIndices = [], /** @type {Map<number, MeshInstance|null>} */ this._pickMeshInstances = new Map();
this.renderer = renderer;
}
}
export { RenderPassPicker };