Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converted Picker to use render pass internally #5608

Merged
merged 3 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/src/examples/graphics/area-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ class AreaPickerExample {
// array of highlighted materials
const highlights: pc.StandardMaterial[] = [];

// the layers picker renders
const worldLayer = app.scene.layers.getLayerByName("World");
const pickerLayers = [worldLayer];

// update each frame
let time = 0;
app.on("update", function (dt: number) {
Expand All @@ -171,7 +175,7 @@ class AreaPickerExample {
// Make sure the picker is the right size, and prepare it, which renders meshes into its render target
if (picker) {
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
picker.prepare(camera.camera, app.scene);
picker.prepare(camera.camera, app.scene, pickerLayers);
}

// areas we want to sample - two larger rectangles, one small square, and one pixel at a mouse position
Expand Down
3 changes: 1 addition & 2 deletions src/deprecated/deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import { GraphNode } from '../scene/graph-node.js';
import { Material } from '../scene/materials/material.js';
import { Mesh } from '../scene/mesh.js';
import { Morph } from '../scene/morph.js';
import { MeshInstance, Command } from '../scene/mesh-instance.js';
import { MeshInstance } from '../scene/mesh-instance.js';
import { Model } from '../scene/model.js';
import { ParticleEmitter } from '../scene/particle-system/particle-emitter.js';
import { Picker } from '../framework/graphics/picker.js';
Expand Down Expand Up @@ -731,7 +731,6 @@ export const scene = {
createBox: createBox
},
BasicMaterial: BasicMaterial,
Command: Command,
ForwardRenderer: ForwardRenderer,
GraphNode: GraphNode,
Material: Material,
Expand Down
247 changes: 87 additions & 160 deletions src/framework/graphics/picker.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { Color } from '../../core/math/color.js';

import { ADDRESS_CLAMP_TO_EDGE, CLEARFLAG_DEPTH, FILTER_NEAREST, PIXELFORMAT_RGBA8 } from '../../platform/graphics/constants.js';
import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA8 } from '../../platform/graphics/constants.js';
import { GraphicsDevice } from '../../platform/graphics/graphics-device.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { Texture } from '../../platform/graphics/texture.js';
import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';

import { SHADER_PICK, SORTMODE_NONE } from '../../scene/constants.js';
import { SHADER_PICK } from '../../scene/constants.js';
import { Camera } from '../../scene/camera.js';
import { Command } from '../../scene/mesh-instance.js';
import { Layer } from '../../scene/layer.js';
import { LayerComposition } from '../../scene/composition/layer-composition.js';

import { getApplication } from '../globals.js';
import { Entity } from '../entity.js';
import { Debug } from '../../core/debug.js';
import { Debug, DebugHelper } from '../../core/debug.js';
import { BlendState } from '../../platform/graphics/blend-state.js';
import { RenderPass } from '../../platform/graphics/render-pass.js';

const tempSet = new Set();

const clearDepthOptions = {
depth: 1.0,
flags: CLEARFLAG_DEPTH
};
const tempMeshInstances = [];

/**
* Picker object used to select mesh instances from screen coordinates.
Expand All @@ -38,6 +32,12 @@ class Picker {
// internal render target
renderTarget = null;

// uniform for the mesh index encoded into rgba
pickColor = new Float32Array(4);

// mapping table from ids to meshInstances
mapping = new Map();

/**
* Create a new Picker instance.
*
Expand All @@ -52,28 +52,11 @@ class Picker {
Debug.deprecated('pc.Picker now takes pc.AppBase as first argument. Passing pc.GraphicsDevice is deprecated.');
}

this.app = app;
// Note: The only reason this class needs the app is to access the renderer. Ideally we remove this dependency and move
// the Picker from framework to the scene level, or even the extras.
this.renderer = app.renderer;
this.device = app.graphicsDevice;

// uniform for the mesh index encoded into rgba
this.pickColor = new Float32Array(4);
this.pickColor[3] = 1;

// mapping table from ids to meshInstances
this.mapping = [];

// create layer composition with the layer and camera
this.cameraEntity = null;
this.layer = null;
this.layerComp = null;
this.initLayerComposition();

// clear command user to simulate layer clearing, required due to storing meshes from multiple layers on a singe layer
const device = this.device;
this.clearDepthCommand = new Command(0, 0, function () {
device.clear(clearDepthOptions);
});

this.width = 0;
this.height = 0;
this.resize(width, height);
Expand All @@ -100,7 +83,7 @@ class Picker {
const device = this.device;

if (typeof x === 'object') {
Debug.deprecated('Picker.getSelection:param \'rect\' is deprecated, use \'x, y, width, height\' instead.');
Debug.deprecated(`Picker.getSelection:param 'rect' is deprecated, use 'x, y, width, height' instead.`);

const rect = x;
x = rect.x;
Expand All @@ -117,12 +100,7 @@ class Picker {
width = Math.floor(Math.max(width || 1, 1));
height = Math.floor(Math.max(height || 1, 1));

// backup active render target
const origRenderTarget = device.renderTarget;

DebugGraphics.pushGpuMarker(device, 'PICKER');

// Ready the device for rendering to the pick buffer
// read pixels from the render target
device.setRenderTarget(this.renderTarget);
device.updateBegin();

Expand All @@ -131,21 +109,17 @@ class Picker {

device.updateEnd();

// Restore render target
device.setRenderTarget(origRenderTarget);

DebugGraphics.popGpuMarker(device);

const mapping = this.mapping;
for (let i = 0; i < width * height; i++) {
const r = pixels[4 * i + 0];
const g = pixels[4 * i + 1];
const b = pixels[4 * i + 2];
const index = r << 16 | g << 8 | b;
const a = pixels[4 * i + 3];
const index = a << 24 | r << 16 | g << 8 | b;

// White is 'no selection'
if (index !== 0xffffff) {
tempSet.add(mapping[index]);
if (index !== 0xffffffff) {
tempSet.add(mapping.get(index));
}
}

Expand Down Expand Up @@ -178,52 +152,13 @@ class Picker {
}

releaseRenderTarget() {

// unset it from the camera
this.cameraEntity.camera.renderTarget = null;

if (this.renderTarget) {
this.renderTarget.destroyTextureBuffers();
this.renderTarget.destroy();
this.renderTarget = null;
}
}

initLayerComposition() {

const device = this.device;
const self = this;
const pickColorId = device.scope.resolve('uColor');

// camera
this.cameraEntity = new Entity();
this.cameraEntity.addComponent('camera');

// layer all meshes rendered for picking at added to
this.layer = new Layer({
name: 'Picker',
shaderPass: SHADER_PICK,
opaqueSortMode: SORTMODE_NONE,

// executes just before the mesh is rendered. And index encoded in rgb is assigned to it
onDrawCall: function (meshInstance, index) {
self.pickColor[0] = ((index >> 16) & 0xff) / 255;
self.pickColor[1] = ((index >> 8) & 0xff) / 255;
self.pickColor[2] = (index & 0xff) / 255;
pickColorId.setValue(self.pickColor);
device.setBlendState(BlendState.NOBLEND);

// keep the index -> meshInstance index mapping
self.mapping[index] = meshInstance;
}
});
this.layer.addCamera(this.cameraEntity.camera);

// composition
this.layerComp = new LayerComposition('picker');
this.layerComp.pushOpaque(this.layer);
}

/**
* Primes the pick buffer with a rendering of the specified models from the point of view of
* the supplied camera. Once the pick buffer has been prepared, {@link Picker#getSelection} can
Expand All @@ -250,99 +185,91 @@ class Picker {
layers = [layers];
}

// populate the layer with meshes and depth clear commands
this.layer.clearMeshInstances();
const destMeshInstances = this.layer.meshInstances;

// source mesh instances
const srcLayers = scene.layers.layerList;
const subLayerEnabled = scene.layers.subLayerEnabled;
const isTransparent = scene.layers.subLayerList;
// make the render target the right size
if (!this.renderTarget || (this.width !== this.renderTarget.width || this.height !== this.renderTarget.height)) {
this.releaseRenderTarget();
this.allocateRenderTarget();
}

for (let i = 0; i < srcLayers.length; i++) {
const srcLayer = srcLayers[i];
// clear registered meshes mapping
this.mapping.clear();

// skip the layer if it does not match the provided ones
if (layers && layers.indexOf(srcLayer) < 0) {
continue;
}
// create a render pass
const device = this.device;
const renderPass = new RenderPass(device, () => {

if (srcLayer.enabled && subLayerEnabled[i]) {
DebugGraphics.pushGpuMarker(device, 'PICKER');

// if the layer is rendered by the camera
const layerCamId = srcLayer.cameras.indexOf(camera);
if (layerCamId >= 0) {
const renderer = this.renderer;
const srcLayers = scene.layers.layerList;
const subLayerEnabled = scene.layers.subLayerEnabled;
const isTransparent = scene.layers.subLayerList;

// if the layer clears the depth, add command to clear it
if (srcLayer._clearDepthBuffer) {
destMeshInstances.push(this.clearDepthCommand);
}
const pickColorId = device.scope.resolve('uColor');
const pickColor = this.pickColor;

// copy all pickable mesh instances
const transparent = isTransparent[i];
const meshInstances = srcLayer.meshInstances;
for (let j = 0; j < meshInstances.length; j++) {
const meshInstance = meshInstances[j];
if (meshInstance.pick && meshInstance.transparent === transparent) {

// as we only render opaque meshes on our internal meshes, mark this mesh as opaque
if (meshInstance.transparent) {
meshInstance.transparent = false;
tempSet.add(meshInstance);
}
for (let i = 0; i < srcLayers.length; i++) {
const srcLayer = srcLayers[i];

destMeshInstances.push(meshInstance);
}
}
// skip the layer if it does not match the provided ones
if (layers && layers.indexOf(srcLayer) < 0) {
continue;
}
}
}

// make the render target the right size
if (!this.renderTarget || (this.width !== this.renderTarget.width || this.height !== this.renderTarget.height)) {
this.releaseRenderTarget();
this.allocateRenderTarget();
}
if (srcLayer.enabled && subLayerEnabled[i]) {

// prepare the rendering camera
this.updateCamera(camera);
// if the layer is rendered by the camera
const layerCamId = srcLayer.cameras.indexOf(camera);
if (layerCamId >= 0) {

// clear registered meshes mapping
this.mapping.length = 0;
// if the layer clears the depth
if (srcLayer._clearDepthBuffer) {
renderer.clear(camera.camera, false, true, false);
}

// render
this.app.renderComposition(this.layerComp);
const culledInstances = srcLayer.getCulledInstances(camera.camera);
const meshInstances = isTransparent[i] ? culledInstances.transparent : culledInstances.opaque;

// mark all meshes as transparent again
tempSet.forEach((meshInstance) => {
meshInstance.transparent = true;
});
tempSet.clear();
}
// only need mesh instances with a pick flag
for (let j = 0; j < meshInstances.length; j++) {
const meshInstance = meshInstances[j];
if (meshInstance.pick) {
tempMeshInstances.push(meshInstance);

updateCamera(srcCamera) {
// keep the index -> meshInstance index mapping
this.mapping.set(meshInstance.id, meshInstance);
}
}

// copy transform
this.cameraEntity.copy(srcCamera.entity);
this.cameraEntity.name = 'PickerCamera';
const lights = [[], [], []];
renderer.renderForward(camera.camera, tempMeshInstances, lights, SHADER_PICK, (meshInstance) => {
const miId = meshInstance.id;
pickColor[0] = ((miId >> 16) & 0xff) / 255;
pickColor[1] = ((miId >> 8) & 0xff) / 255;
pickColor[2] = (miId & 0xff) / 255;
pickColor[3] = ((miId >> 24) & 0xff) / 255;
pickColorId.setValue(pickColor);
device.setBlendState(BlendState.NOBLEND);
});

tempMeshInstances.length = 0;
}
}
}

// copy camera component properties - which overwrites few properties we change to what is needed later
const destCamera = this.cameraEntity.camera;
destCamera.copy(srcCamera);
DebugGraphics.popGpuMarker(device);
});

DebugHelper.setName(renderPass, `RenderPass-Picker`);
renderPass.init(this.renderTarget);

// set up clears
destCamera.clearColorBuffer = true;
destCamera.clearDepthBuffer = true;
destCamera.clearStencilBuffer = true;
destCamera.clearColor = Color.WHITE;

// render target
destCamera.renderTarget = this.renderTarget;

// layers
this.layer.clearCameras();
this.layer.addCamera(destCamera);
destCamera.layers = [this.layer.id];
renderPass.colorOps.clearValue = Color.WHITE;
renderPass.colorOps.clear = true;
renderPass.depthStencilOps.clearDepth = true;

// render the pass to update the render target
renderPass.render();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export { LightingParams } from './scene/lighting/lighting-params.js';
export { LitShaderOptions } from './scene/shader-lib/programs/lit-shader-options.js';
export { Material } from './scene/materials/material.js';
export { Mesh } from './scene/mesh.js';
export { MeshInstance, Command } from './scene/mesh-instance.js';
export { MeshInstance } from './scene/mesh-instance.js';
export { Model } from './scene/model.js';
export { Morph } from './scene/morph.js';
export { MorphInstance } from './scene/morph-instance.js';
Expand Down