UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

198 lines (197 loc) 6.3 kB
import { Color } from "../../core/math/color.js"; import { 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 { PROJECTION_ORTHOGRAPHIC } from "../../scene/constants.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 = /* @__PURE__ */ new Set(); const _rect = new Vec4(); const _floatView = new Float32Array(1); const _int32View = new Int32Array(_floatView.buffer); class Picker { device; renderPass; depth; width; height; renderTarget = null; colorBuffer = null; depthBuffer = null; renderTargetDepth = null; mapping = /* @__PURE__ */ new Map(); deviceValid = true; constructor(app, width, height, depth = false) { 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 }); } 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 near = camera.nearClip; const far = camera.farClip; const isOrtho = camera.projection === PROJECTION_ORTHOGRAPHIC; const linearDepth = await this.getPointDepthAsync(x, y); if (linearDepth === null) { return null; } const ndcDepth = isOrtho ? linearDepth : far * linearDepth / (linearDepth * (far - near) + near); const deviceCoord = new Vec4( x / this.width * 2 - 1, (1 - y / this.height) * 2 - 1, ndcDepth * 2 - 1, 1 ); invViewProj.transformVec4(deviceCoord, deviceCoord); deviceCoord.mulScalar(1 / 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]) >>> 0; if (intBits === 4294967295) { return null; } _int32View[0] = intBits; return _floatView[0]; } // sanitize the rectangle to make sure it's inside the texture and does not use fractions 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 !== 4294967295) { tempSet.add(mapping.get(index)); } } tempSet.forEach((meshInstance) => { if (meshInstance) { selection.push(meshInstance); } }); tempSet.clear(); } return selection; } createTexture(name) { return Texture.createDataTexture2D(this.device, name, this.width, this.height, PIXELFORMAT_RGBA8); } 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, 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 };