UNPKG

molstar

Version:

A comprehensive macromolecular library.

413 lines (412 loc) 17.5 kB
/** * Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { PickingId } from '../../mol-geo/geometry/picking.js'; import { PickType } from '../../mol-gl/renderer.js'; import { isWebGL2 } from '../../mol-gl/webgl/compat.js'; import { isDebugMode, isTimingMode } from '../../mol-util/debug.js'; import { now } from '../../mol-util/now.js'; import { unpackRGBAToDepth, unpackRGBToInt } from '../../mol-util/number-packing.js'; import { Viewport } from '../camera/util.js'; export const DefaultPickOptions = { pickPadding: 1, maxAsyncReadLag: 5, }; // export class PickPass { constructor(webgl, width, height, pickScale) { this.webgl = webgl; this.width = width; this.height = height; this.pickScale = pickScale; const pickRatio = pickScale / webgl.pixelRatio; this.pickWidth = Math.ceil(width * pickRatio); this.pickHeight = Math.ceil(height * pickRatio); const { resources, extensions: { drawBuffers }, gl } = webgl; if (drawBuffers) { this.objectPickTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.objectPickTexture.define(this.pickWidth, this.pickHeight); this.instancePickTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.instancePickTexture.define(this.pickWidth, this.pickHeight); this.groupPickTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.groupPickTexture.define(this.pickWidth, this.pickHeight); this.depthPickTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.depthPickTexture.define(this.pickWidth, this.pickHeight); this.framebuffer = resources.framebuffer(); this.objectPickFramebuffer = resources.framebuffer(); this.instancePickFramebuffer = resources.framebuffer(); this.groupPickFramebuffer = resources.framebuffer(); this.depthPickFramebuffer = resources.framebuffer(); this.framebuffer.bind(); drawBuffers.drawBuffers([ drawBuffers.COLOR_ATTACHMENT0, drawBuffers.COLOR_ATTACHMENT1, drawBuffers.COLOR_ATTACHMENT2, drawBuffers.COLOR_ATTACHMENT3, ]); this.objectPickTexture.attachFramebuffer(this.framebuffer, 'color0'); this.instancePickTexture.attachFramebuffer(this.framebuffer, 'color1'); this.groupPickTexture.attachFramebuffer(this.framebuffer, 'color2'); this.depthPickTexture.attachFramebuffer(this.framebuffer, 'color3'); this.depthRenderbuffer = isWebGL2(gl) ? resources.renderbuffer('depth32f', 'depth', this.pickWidth, this.pickHeight) : resources.renderbuffer('depth16', 'depth', this.pickWidth, this.pickHeight); this.depthRenderbuffer.attachFramebuffer(this.framebuffer); this.objectPickTexture.attachFramebuffer(this.objectPickFramebuffer, 'color0'); this.instancePickTexture.attachFramebuffer(this.instancePickFramebuffer, 'color0'); this.groupPickTexture.attachFramebuffer(this.groupPickFramebuffer, 'color0'); this.depthPickTexture.attachFramebuffer(this.depthPickFramebuffer, 'color0'); } else { this.objectPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.instancePickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.groupPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.depthPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); } } getByteCount() { if (this.webgl.extensions.drawBuffers) { return (this.objectPickTexture.getByteCount() + this.instancePickTexture.getByteCount() + this.groupPickTexture.getByteCount() + this.depthPickTexture.getByteCount() + this.depthRenderbuffer.getByteCount()); } else { return (this.objectPickTarget.getByteCount() + this.instancePickTarget.getByteCount() + this.groupPickTarget.getByteCount() + this.depthPickTarget.getByteCount()); } } dispose() { if (this.webgl.extensions.drawBuffers) { this.framebuffer.destroy(); this.objectPickTexture.destroy(); this.instancePickTexture.destroy(); this.groupPickTexture.destroy(); this.depthPickTexture.destroy(); this.objectPickFramebuffer.destroy(); this.instancePickFramebuffer.destroy(); this.groupPickFramebuffer.destroy(); this.depthPickFramebuffer.destroy(); this.depthRenderbuffer.destroy(); } else { this.objectPickTarget.destroy(); this.instancePickTarget.destroy(); this.groupPickTarget.destroy(); this.depthPickTarget.destroy(); } } get pickRatio() { return this.pickScale / this.webgl.pixelRatio; } setPickScale(pickScale) { this.pickScale = pickScale; this.setSize(this.width, this.height); } bindObject() { if (this.webgl.extensions.drawBuffers) { this.objectPickFramebuffer.bind(); } else { this.objectPickTarget.bind(); } } bindInstance() { if (this.webgl.extensions.drawBuffers) { this.instancePickFramebuffer.bind(); } else { this.instancePickTarget.bind(); } } bindGroup() { if (this.webgl.extensions.drawBuffers) { this.groupPickFramebuffer.bind(); } else { this.groupPickTarget.bind(); } } bindDepth() { if (this.webgl.extensions.drawBuffers) { this.depthPickFramebuffer.bind(); } else { this.depthPickTarget.bind(); } } get drawingBufferHeight() { return this.height; } setSize(width, height) { this.width = width; this.height = height; const pickRatio = this.pickScale / this.webgl.pixelRatio; const pickWidth = Math.ceil(this.width * pickRatio); const pickHeight = Math.ceil(this.height * pickRatio); if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) { this.pickWidth = pickWidth; this.pickHeight = pickHeight; if (this.webgl.extensions.drawBuffers) { this.objectPickTexture.define(this.pickWidth, this.pickHeight); this.instancePickTexture.define(this.pickWidth, this.pickHeight); this.groupPickTexture.define(this.pickWidth, this.pickHeight); this.depthPickTexture.define(this.pickWidth, this.pickHeight); this.depthRenderbuffer.setSize(this.pickWidth, this.pickHeight); } else { this.objectPickTarget.setSize(this.pickWidth, this.pickHeight); this.instancePickTarget.setSize(this.pickWidth, this.pickHeight); this.groupPickTarget.setSize(this.pickWidth, this.pickHeight); this.depthPickTarget.setSize(this.pickWidth, this.pickHeight); } } } reset() { const { drawBuffers } = this.webgl.extensions; if (drawBuffers) { this.framebuffer.bind(); drawBuffers.drawBuffers([ drawBuffers.COLOR_ATTACHMENT0, drawBuffers.COLOR_ATTACHMENT1, drawBuffers.COLOR_ATTACHMENT2, drawBuffers.COLOR_ATTACHMENT3, ]); this.objectPickTexture.attachFramebuffer(this.framebuffer, 'color0'); this.instancePickTexture.attachFramebuffer(this.framebuffer, 'color1'); this.groupPickTexture.attachFramebuffer(this.framebuffer, 'color2'); this.depthPickTexture.attachFramebuffer(this.framebuffer, 'color3'); this.depthRenderbuffer.attachFramebuffer(this.framebuffer); this.objectPickTexture.attachFramebuffer(this.objectPickFramebuffer, 'color0'); this.instancePickTexture.attachFramebuffer(this.instancePickFramebuffer, 'color0'); this.groupPickTexture.attachFramebuffer(this.groupPickFramebuffer, 'color0'); this.depthPickTexture.attachFramebuffer(this.depthPickFramebuffer, 'color0'); } } renderVariant(renderer, camera, scene, helper, variant, pickType) { renderer.clear(false); renderer.update(camera, scene); renderer.renderPick(scene.primitives, camera, variant, pickType); if (helper.handle.isEnabled) { renderer.renderPick(helper.handle.scene, camera, variant, pickType); } if (helper.camera.isEnabled) { helper.camera.update(camera); renderer.update(helper.camera.camera, helper.camera.scene); renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, pickType); } } render(renderer, camera, scene, helper) { if (this.webgl.extensions.drawBuffers) { this.framebuffer.bind(); this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.None); // if (this.pickWidth < 256) { // printTextureImage(readTexture(this.webgl, this.groupPickTexture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true }); // } } else { this.objectPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Object); this.instancePickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Instance); this.groupPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Group); // printTextureImage(readTexture(this.webgl, this.groupPickTarget.texture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true }); this.depthPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'depth', PickType.None); } } } let AsyncPickingWarningShown = false; export function checkAsyncPickingSupport(webgl) { if (webgl.isWebGL2) return true; if (isDebugMode && !AsyncPickingWarningShown) { console.log('WebGL2 required for async picking. Falling back to synchronous picking.'); AsyncPickingWarningShown = true; } return false; } export var AsyncPickStatus; (function (AsyncPickStatus) { AsyncPickStatus[AsyncPickStatus["Pending"] = 0] = "Pending"; AsyncPickStatus[AsyncPickStatus["Resolved"] = 1] = "Resolved"; AsyncPickStatus[AsyncPickStatus["Failed"] = 2] = "Failed"; })(AsyncPickStatus || (AsyncPickStatus = {})); ; export class PickBuffers { setup() { const size = this.viewport.width * this.viewport.height * 4; if (!this.object || this.object.length !== size) { this.object = new Uint8Array(size); this.instance = new Uint8Array(size); this.group = new Uint8Array(size); this.depth = new Uint8Array(size); } } setViewport(x, y, width, height) { Viewport.set(this.viewport, x, y, width, height); this.setup(); } read() { if (isTimingMode) this.webgl.timer.mark('PickBuffers.read'); const { x, y, width, height } = this.viewport; this.pickPass.bindObject(); this.webgl.readPixels(x, y, width, height, this.object); this.pickPass.bindInstance(); this.webgl.readPixels(x, y, width, height, this.instance); this.pickPass.bindGroup(); this.webgl.readPixels(x, y, width, height, this.group); this.pickPass.bindDepth(); this.webgl.readPixels(x, y, width, height, this.depth); this.ready = true; if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.read'); } asyncRead() { const { gl } = this.webgl; if (!isWebGL2(gl)) return; if (isTimingMode) this.webgl.timer.mark('PickBuffers.asyncRead'); if (this.fenceSync !== null) { gl.deleteSync(this.fenceSync); } const { x, y, width, height } = this.viewport; this.pickPass.bindObject(); this.objectBuffer.read(x, y, width, height); this.pickPass.bindInstance(); this.instanceBuffer.read(x, y, width, height); this.pickPass.bindGroup(); this.groupBuffer.read(x, y, width, height); this.pickPass.bindDepth(); this.depthBuffer.read(x, y, width, height); this.fenceTimestamp = now(); this.fenceSync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); gl.flush(); this.ready = false; if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.asyncRead'); } check() { if (this.ready) return AsyncPickStatus.Resolved; if (this.fenceSync === null) return AsyncPickStatus.Failed; const { gl } = this.webgl; if (!isWebGL2(gl)) return AsyncPickStatus.Failed; const res = gl.clientWaitSync(this.fenceSync, 0, 0); if (res === gl.WAIT_FAILED || this.lag >= this.maxAsyncReadLag) { // console.log(`failed to get buffer data after ${this.lag + 1} checks`); if (res !== gl.WAIT_FAILED && now() - this.fenceTimestamp < 1000 / 60) { this.lag += 1; return AsyncPickStatus.Pending; } gl.deleteSync(this.fenceSync); this.fenceSync = null; this.lag = 0; this.ready = false; return AsyncPickStatus.Failed; } else if (res === gl.TIMEOUT_EXPIRED) { this.lag += 1; // console.log(`waiting for buffer data for ${this.lag} checks`); return AsyncPickStatus.Pending; } else { this.objectBuffer.getSubData(this.object); this.instanceBuffer.getSubData(this.instance); this.groupBuffer.getSubData(this.group); this.depthBuffer.getSubData(this.depth); // console.log(`got buffer data after ${this.lag + 1} checks`); gl.deleteSync(this.fenceSync); this.fenceSync = null; this.lag = 0; this.ready = true; return AsyncPickStatus.Resolved; } } getIdx(x, y) { return (y * this.viewport.width + x) * 4; } getDepth(x, y) { if (!this.ready) return -1; const idx = this.getIdx(x, y); const b = this.depth; return unpackRGBAToDepth(b[idx], b[idx + 1], b[idx + 2], b[idx + 3]); } getId(x, y, buffer) { if (!this.ready) return -1; const idx = this.getIdx(x, y); return unpackRGBToInt(buffer[idx], buffer[idx + 1], buffer[idx + 2]); } getObjectId(x, y) { return this.getId(x, y, this.object); } getInstanceId(x, y) { return this.getId(x, y, this.instance); } getGroupId(x, y) { return this.getId(x, y, this.group); } getPickingId(x, y) { const objectId = this.getObjectId(x, y); // console.log('objectId', objectId); if (objectId === -1 || objectId === PickingId.Null) return; const instanceId = this.getInstanceId(x, y); // console.log('instanceId', instanceId); if (instanceId === -1 || instanceId === PickingId.Null) return; const groupId = this.getGroupId(x, y); // console.log('groupId', groupId); if (groupId === -1) return; return { objectId, instanceId, groupId }; } reset() { this.fenceSync = null; this.ready = false; this.lag = 0; this.fenceTimestamp = 0; } dispose() { const { gl } = this.webgl; if (!isWebGL2(gl)) return; this.objectBuffer.destroy(); this.instanceBuffer.destroy(); this.groupBuffer.destroy(); this.depthBuffer.destroy(); if (this.fenceSync !== null) { gl.deleteSync(this.fenceSync); this.fenceSync = null; } } constructor(webgl, pickPass, maxAsyncReadLag = 5) { this.webgl = webgl; this.pickPass = pickPass; this.maxAsyncReadLag = maxAsyncReadLag; this.viewport = Viewport.create(0, 0, 0, 0); this.fenceSync = null; this.fenceTimestamp = 0; this.ready = false; this.lag = 0; if (webgl.isWebGL2) { this.objectBuffer = webgl.resources.pixelPack('rgba', 'ubyte'); this.instanceBuffer = webgl.resources.pixelPack('rgba', 'ubyte'); this.groupBuffer = webgl.resources.pixelPack('rgba', 'ubyte'); this.depthBuffer = webgl.resources.pixelPack('rgba', 'ubyte'); } this.setup(); } }