molstar
Version:
A comprehensive macromolecular library.
347 lines (346 loc) • 15.7 kB
JavaScript
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PickType } from '../../mol-gl/renderer';
import { isWebGL2 } from '../../mol-gl/webgl/compat';
import { Vec3 } from '../../mol-math/linear-algebra';
import { spiral2d } from '../../mol-math/misc';
import { isTimingMode } from '../../mol-util/debug';
import { unpackRGBToInt, unpackRGBAToDepth } from '../../mol-util/number-packing';
import { StereoCamera } from '../camera/stereo';
import { cameraUnproject } from '../camera/util';
import { Viewport } from '../camera/util';
const NullId = Math.pow(2, 24) - 2;
export class PickPass {
constructor(webgl, drawPass, pickScale) {
this.webgl = webgl;
this.drawPass = drawPass;
this.pickScale = pickScale;
const pickRatio = pickScale / webgl.pixelRatio;
this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickRatio);
this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * 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);
}
}
get pickRatio() {
return this.pickScale / this.webgl.pixelRatio;
}
setPickScale(pickScale) {
this.pickScale = pickScale;
this.syncSize();
}
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.drawPass.colorTarget.getHeight();
}
syncSize() {
const pickRatio = this.pickScale / this.webgl.pixelRatio;
const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickRatio);
const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * 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);
}
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);
// printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' })
this.depthPickTarget.bind();
this.renderVariant(renderer, camera, scene, helper, 'depth', PickType.None);
}
}
}
export class PickHelper {
setupBuffers() {
const bufferSize = this.pickWidth * this.pickHeight * 4;
if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
this.objectBuffer = new Uint8Array(bufferSize);
this.instanceBuffer = new Uint8Array(bufferSize);
this.groupBuffer = new Uint8Array(bufferSize);
this.depthBuffer = new Uint8Array(bufferSize);
}
}
setViewport(x, y, width, height) {
Viewport.set(this.viewport, x, y, width, height);
this.update();
}
setPickPadding(pickPadding) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
update() {
const { x, y, width, height } = this.viewport;
this.pickRatio = this.pickPass.pickRatio;
this.pickX = Math.ceil(x * this.pickRatio);
this.pickY = Math.ceil(y * this.pickRatio);
const pickWidth = Math.floor(width * this.pickRatio);
const pickHeight = Math.floor(height * this.pickRatio);
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
this.pickWidth = pickWidth;
this.pickHeight = pickHeight;
this.halfPickWidth = Math.floor(this.pickWidth / 2);
this.setupBuffers();
}
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
this.dirty = true;
}
syncBuffers() {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.syncBuffers');
const { pickX, pickY, pickWidth, pickHeight } = this;
this.pickPass.bindObject();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer);
this.pickPass.bindInstance();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer);
this.pickPass.bindGroup();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer);
this.pickPass.bindDepth();
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.syncBuffers');
}
getBufferIdx(x, y) {
return (y * this.pickWidth + x) * 4;
}
getDepth(x, y) {
const idx = this.getBufferIdx(x, y);
const b = this.depthBuffer;
return unpackRGBAToDepth(b[idx], b[idx + 1], b[idx + 2], b[idx + 3]);
}
getId(x, y, buffer) {
const idx = this.getBufferIdx(x, y);
return unpackRGBToInt(buffer[idx], buffer[idx + 1], buffer[idx + 2]);
}
render(camera) {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.render', { captureStats: true });
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
renderer.setTransparentBackground(false);
renderer.setDrawingBufferSize(pickWidth, pickHeight);
renderer.setPixelRatio(this.pickRatio);
if (StereoCamera.is(camera)) {
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.left, scene, helper);
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
this.pickPass.render(renderer, camera.right, scene, helper);
}
else {
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
this.pickPass.render(renderer, camera, scene, helper);
}
this.dirty = false;
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.render');
}
identifyInternal(x, y, camera) {
if (this.pickRatio !== this.pickPass.pickRatio) {
this.update();
}
const { webgl, pickRatio } = this;
if (webgl.isContextLost)
return;
x *= webgl.pixelRatio;
y *= webgl.pixelRatio;
y = this.pickPass.drawingBufferHeight - y; // flip y
const { viewport } = this;
// check if within viewport
if (x < viewport.x ||
y < viewport.y ||
x > viewport.x + viewport.width ||
y > viewport.y + viewport.height)
return;
if (this.dirty) {
if (isTimingMode)
this.webgl.timer.mark('PickHelper.identify');
this.render(camera);
this.syncBuffers();
if (isTimingMode)
this.webgl.timer.markEnd('PickHelper.identify');
}
const xv = x - viewport.x;
const yv = y - viewport.y;
const xp = Math.floor(xv * pickRatio);
const yp = Math.floor(yv * pickRatio);
const objectId = this.getId(xp, yp, this.objectBuffer);
// console.log('objectId', objectId);
if (objectId === -1 || objectId === NullId)
return;
const instanceId = this.getId(xp, yp, this.instanceBuffer);
// console.log('instanceId', instanceId);
if (instanceId === -1 || instanceId === NullId)
return;
const groupId = this.getId(xp, yp, this.groupBuffer);
// console.log('groupId', groupId);
if (groupId === -1 || groupId === NullId)
return;
const z = this.getDepth(xp, yp);
// console.log('z', z);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {
position[0] = viewport.x + (xv - halfWidth) * 2;
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
}
else {
position[0] = viewport.x + xv * 2;
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
}
}
else {
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
}
// console.log({ id: { objectId, instanceId, groupId }, position });
return { id: { objectId, instanceId, groupId }, position };
}
identify(x, y, camera) {
for (const d of this.spiral) {
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
if (pickData)
return pickData;
}
}
constructor(webgl, renderer, scene, helper, pickPass, viewport, pickPadding = 1) {
this.webgl = webgl;
this.renderer = renderer;
this.scene = scene;
this.helper = helper;
this.pickPass = pickPass;
this.pickPadding = pickPadding;
this.dirty = true;
this.viewport = Viewport();
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
}
}