playcanvas
Version:
PlayCanvas WebGL game engine
196 lines (193 loc) • 6.48 kB
JavaScript
import { Color } from '../../core/math/color.js';
import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA8 } from '../../platform/graphics/constants.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { Texture } from '../../platform/graphics/texture.js';
import { Layer } from '../../scene/layer.js';
import { RenderPassPicker } from './render-pass-picker.js';
import { math } from '../../core/math/math.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Vec4 } from '../../core/math/vec4.js';
import { Mat4 } from '../../core/math/mat4.js';
const tempSet = new Set();
const _rect = new Vec4();
const _floatView = new Float32Array(1);
const _int32View = new Int32Array(_floatView.buffer);
class Picker {
constructor(app, width, height, depth = false){
this.renderTarget = null;
this.colorBuffer = null;
this.depthBuffer = null;
this.renderTargetDepth = null;
this.mapping = new Map();
this.deviceValid = true;
this.device = app.graphicsDevice;
this.renderPass = new RenderPassPicker(this.device, app.renderer);
this.depth = depth;
this.width = 0;
this.height = 0;
this.resize(width, height);
this.allocateRenderTarget();
this.device.on('destroy', ()=>{
this.deviceValid = false;
});
}
destroy() {
this.releaseRenderTarget();
this.renderPass?.destroy();
}
getSelection(x, y, width = 1, height = 1) {
const device = this.device;
if (device.isWebGPU) {
return [];
}
y = this.renderTarget.height - (y + height);
const rect = this.sanitizeRect(x, y, width, height);
device.setRenderTarget(this.renderTarget);
device.updateBegin();
const pixels = new Uint8Array(4 * rect.z * rect.w);
device.readPixels(rect.x, rect.y, rect.z, rect.w, pixels);
device.updateEnd();
return this.decodePixels(pixels, this.mapping);
}
getSelectionAsync(x, y, width = 1, height = 1) {
if (!this.renderTarget || !this.renderTarget.colorBuffer) {
return Promise.resolve([]);
}
return this._readTexture(this.renderTarget.colorBuffer, x, y, width, height, this.renderTarget).then((pixels)=>{
return this.decodePixels(pixels, this.mapping);
});
}
_readTexture(texture, x, y, width, height, renderTarget) {
if (this.device?.isWebGL2) {
y = renderTarget.height - (y + height);
}
const rect = this.sanitizeRect(x, y, width, height);
return texture.read(rect.x, rect.y, rect.z, rect.w, {
immediate: true,
renderTarget: renderTarget
});
}
async getWorldPointAsync(x, y) {
const camera = this.renderPass.camera;
if (!camera) {
return null;
}
const viewProjMat = new Mat4().mul2(camera.camera.projectionMatrix, camera.camera.viewMatrix);
const invViewProj = viewProjMat.invert();
const depth = await this.getPointDepthAsync(x, y);
if (depth === null) {
return null;
}
const deviceCoord = new Vec4(x / this.width * 2 - 1, (1 - y / this.height) * 2 - 1, depth * 2 - 1, 1.0);
invViewProj.transformVec4(deviceCoord, deviceCoord);
deviceCoord.mulScalar(1.0 / deviceCoord.w);
return new Vec3(deviceCoord.x, deviceCoord.y, deviceCoord.z);
}
async getPointDepthAsync(x, y) {
if (!this.depthBuffer) {
return null;
}
const pixels = await this._readTexture(this.depthBuffer, x, y, 1, 1, this.renderTargetDepth);
const intBits = pixels[0] << 24 | pixels[1] << 16 | pixels[2] << 8 | pixels[3];
if (intBits === 0xFFFFFFFF) {
return null;
}
_int32View[0] = intBits;
return _floatView[0];
}
sanitizeRect(x, y, width, height) {
const maxWidth = this.renderTarget.width;
const maxHeight = this.renderTarget.height;
x = math.clamp(Math.floor(x), 0, maxWidth - 1);
y = math.clamp(Math.floor(y), 0, maxHeight - 1);
width = Math.floor(Math.max(width, 1));
width = Math.min(width, maxWidth - x);
height = Math.floor(Math.max(height, 1));
height = Math.min(height, maxHeight - y);
return _rect.set(x, y, width, height);
}
decodePixels(pixels, mapping) {
const selection = [];
if (this.deviceValid) {
const count = pixels.length;
for(let i = 0; i < count; i += 4){
const r = pixels[i + 0];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
const index = (a << 24 | r << 16 | g << 8 | b) >>> 0;
if (index !== 0xFFFFFFFF) {
tempSet.add(mapping.get(index));
}
}
tempSet.forEach((meshInstance)=>{
if (meshInstance) {
selection.push(meshInstance);
}
});
tempSet.clear();
}
return selection;
}
createTexture(name) {
return new Texture(this.device, {
format: PIXELFORMAT_RGBA8,
width: this.width,
height: this.height,
mipmaps: false,
minFilter: FILTER_NEAREST,
magFilter: FILTER_NEAREST,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE,
name: name
});
}
allocateRenderTarget() {
this.colorBuffer = this.createTexture('pick');
const colorBuffers = [
this.colorBuffer
];
if (this.depth) {
this.depthBuffer = this.createTexture('pick-depth');
colorBuffers.push(this.depthBuffer);
this.renderTargetDepth = new RenderTarget({
colorBuffer: this.depthBuffer,
depth: false
});
}
this.renderTarget = new RenderTarget({
colorBuffers: colorBuffers,
depth: true
});
}
releaseRenderTarget() {
this.renderTarget?.destroyTextureBuffers();
this.renderTarget?.destroy();
this.renderTarget = null;
this.renderTargetDepth?.destroy();
this.renderTargetDepth = null;
this.colorBuffer = null;
this.depthBuffer = null;
}
prepare(camera, scene, layers) {
if (layers instanceof Layer) {
layers = [
layers
];
}
this.renderTarget?.resize(this.width, this.height);
this.renderTargetDepth?.resize(this.width, this.height);
this.mapping.clear();
const renderPass = this.renderPass;
renderPass.init(this.renderTarget);
renderPass.setClearColor(Color.WHITE);
renderPass.depthStencilOps.clearDepth = true;
renderPass.update(camera, scene, layers, this.mapping, this.depth);
renderPass.render();
}
resize(width, height) {
this.width = Math.floor(width);
this.height = Math.floor(height);
}
}
export { Picker };