molstar
Version:
A comprehensive macromolecular library.
450 lines (449 loc) • 18.3 kB
JavaScript
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createHiZRenderable } from '../../mol-gl/compute/hi-z';
import { isWebGL2 } from '../../mol-gl/webgl/compat';
import { fasterLog2 as _fasterLog2 } from '../../mol-math/approx';
import { Mat4, Vec4 } from '../../mol-math/linear-algebra';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
import { ValueCell } from '../../mol-util/value-cell';
import { Viewport } from '../camera/util';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { getBuffer } from '../../mol-gl/webgl/buffer';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3transformMat4 = Vec3.transformMat4;
const v4set = Vec4.set;
const fasterLog2 = _fasterLog2;
function perspectiveDepthToViewZ(depth, near, far) {
return (near * far) / ((far - near) * depth - far);
}
function orthographicDepthToViewZ(depth, near, far) {
return depth * (near - far) - near;
}
function depthToViewZ(depth, near, far, projection) {
return projection[11] === -1
? perspectiveDepthToViewZ(depth, near, far)
: orthographicDepthToViewZ(depth, near, far);
}
/**
* Bounding rectangle of a clipped, perspective-projected 3D Sphere.
* Michael Mara, Morgan McGuire. 2013
*
* Specialization by Arseny Kapoulkine, MIT License Copyright (c) 2018
* https://github.com/zeux/niagara
*/
function perspectiveProjectSphere(out, p, r, projection) {
const prx = p[0] * r;
const pry = p[1] * r;
const prz = p[2] * r;
const pzr2 = p[2] * p[2] - r * r;
const vx = Math.sqrt(p[0] * p[0] + pzr2);
const minx = ((vx * p[0] - prz) / (vx * p[2] + prx)) * projection[0];
const maxx = ((vx * p[0] + prz) / (vx * p[2] - prx)) * projection[0];
const vy = Math.sqrt(p[1] * p[1] + pzr2);
const miny = ((vy * p[1] - prz) / (vy * p[2] + pry)) * projection[5];
const maxy = ((vy * p[1] + prz) / (vy * p[2] - pry)) * projection[5];
return v4set(out, maxx * -0.5 + 0.5, miny * 0.5 + 0.5, minx * -0.5 + 0.5, maxy * 0.5 + 0.5);
}
function orthographicProjectSphere(out, p, r, projection) {
const sx = projection[0];
const sy = projection[5];
const minx = (p[0] + r) * sx;
const maxx = (p[0] - r) * sx;
const miny = (p[1] + r) * sy;
const maxy = (p[1] - r) * sy;
return v4set(out, maxx * 0.5 + 0.5, miny * -0.5 + 0.5, minx * 0.5 + 0.5, maxy * -0.5 + 0.5);
}
function projectSphere(out, p, r, projection) {
return projection[11] === -1
? perspectiveProjectSphere(out, p, r, projection)
: orthographicProjectSphere(out, p, r, projection);
}
export const HiZParams = {
enabled: PD.Boolean(false, { description: 'Hierarchical Z-buffer occlusion culling. Only available for WebGL2.' }),
maxFrameLag: PD.Numeric(10, { min: 1, max: 30, step: 1 }, { description: 'Maximum number of frames to wait for Z-buffer data.' }),
minLevel: PD.Numeric(3, { min: 1, max: 10, step: 1 }),
};
export class HiZPass {
clear() {
if (!this.supported)
return;
const { gl } = this.webgl;
if (!isWebGL2(gl))
return;
if (this.sync !== null) {
gl.deleteSync(this.sync);
this.sync = null;
}
this.frameLag = 0;
this.ready = false;
if (this.debug) {
this.debug.rect.style.display = 'none';
this.debug.container.style.display = 'none';
}
}
render(camera) {
if (!this.supported || !this.props.enabled)
return;
const { gl, state } = this.webgl;
if (!isWebGL2(gl) || this.sync !== null)
return;
this.nextNear = camera.near;
this.nextFar = camera.far;
Mat4.copy(this.nextView, camera.view);
Mat4.copy(this.nextProjection, camera.projection);
if (isTimingMode)
this.webgl.timer.mark('hi-Z');
state.disable(gl.CULL_FACE);
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.disable(gl.SCISSOR_TEST);
state.depthMask(false);
state.colorMask(true, true, true, true);
state.clearColor(0, 0, 0, 0);
//
const v = this.renderable.values;
const s = Math.pow(2, Math.ceil(Math.log(Math.max(gl.drawingBufferWidth, gl.drawingBufferHeight)) / Math.log(2)) - 1);
for (let i = 0, il = this.levelData.length; i < il; ++i) {
const td = this.levelData[i];
td.framebuffer.bind();
if (i > 0) {
ValueCell.update(v.uInvSize, td.invSize);
ValueCell.update(v.uOffset, Vec2.set(v.uOffset.ref.value, 0, 0));
ValueCell.update(v.tPreviousLevel, this.levelData[i - 1].texture);
}
else {
ValueCell.update(v.uInvSize, Vec2.set(v.uInvSize.ref.value, 1 / s, 1 / s));
ValueCell.update(v.uOffset, Vec2.set(v.uOffset.ref.value, this.viewport.x / gl.drawingBufferWidth, this.viewport.y / gl.drawingBufferHeight));
ValueCell.update(v.tPreviousLevel, this.drawPass.depthTextureOpaque);
}
state.currentRenderItemId = -1;
state.viewport(0, 0, td.size[0], td.size[1]);
gl.clear(gl.COLOR_BUFFER_BIT);
this.renderable.update();
this.renderable.render();
if (i >= this.props.minLevel) {
this.tex.bind(0);
gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, td.offset, 0, 0, 0, td.size[0], td.size[1]);
this.tex.unbind(0);
}
}
//
this.tex.attachFramebuffer(this.fb, 0);
const hw = this.tex.getWidth();
const hh = this.tex.getHeight();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
gl.bufferData(gl.PIXEL_PACK_BUFFER, this.buffer.byteLength, gl.STREAM_READ);
gl.readPixels(0, 0, hw, hh, gl.RED, gl.FLOAT, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
this.sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
if (isTimingMode)
this.webgl.timer.markEnd('hi-Z');
}
tick() {
if (!this.supported || !this.props.enabled || this.sync === null)
return;
const { gl } = this.webgl;
if (!isWebGL2(gl))
return;
const res = gl.clientWaitSync(this.sync, 0, 0);
if (res === gl.WAIT_FAILED || this.frameLag >= this.props.maxFrameLag) {
// console.log(`failed to get buffer data after ${this.frameLag + 1} frames`);
gl.deleteSync(this.sync);
this.sync = null;
this.frameLag = 0;
this.ready = false;
}
else if (res === gl.TIMEOUT_EXPIRED) {
this.frameLag += 1;
// console.log(`waiting for buffer data for ${this.frameLag} frames`);
}
else {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this.buffer);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
// console.log(`got buffer data after ${this.frameLag + 1} frames`);
gl.deleteSync(this.sync);
this.sync = null;
this.frameLag = 0;
this.ready = true;
// if (isDebugMode) {
// const p = PixelData.flipY(PixelData.create(this.buffer.slice(), this.tex.getWidth(), this.tex.getHeight()));
// printTextureImage(p, { scale: MinLevel, id: 'hiz', useCanvas: true, pixelated: true });
// }
this.near = this.nextNear;
this.far = this.nextFar;
Mat4.copy(this.view, this.nextView);
Mat4.copy(this.projection, this.nextProjection);
}
}
transform(s) {
const { view, vp } = this;
v3transformMat4(vp, s.center, view);
const r = (s.radius * 1.2) + 1.52;
return { vp, r };
}
project(vp, r) {
const { projection, aabb, viewport } = this;
projectSphere(aabb, vp, r, projection);
const w = aabb[2] - aabb[0];
const h = aabb[3] - aabb[1];
const pr = Math.max(w * viewport.width, h * viewport.height);
const lod = Math.ceil(fasterLog2(pr / 2));
return { aabb, w, h, pr, lod };
}
setViewport(x, y, width, height) {
if (!this.supported)
return;
// Avoid setting dimensions to 0x0 because it causes "empty textures are not allowed" error.
width = Math.max(width, 2);
height = Math.max(height, 2);
Viewport.set(this.viewport, x, y, width, height);
const levels = Math.ceil(Math.log(Math.max(width, height)) / Math.log(2));
if (levels === this.levelData.length)
return;
const { minLevel } = this.props;
this.buffer = new Float32Array(Math.pow(2, levels - minLevel) * Math.pow(2, levels - 1 - minLevel));
this.tex.define(Math.pow(2, levels - minLevel), Math.pow(2, levels - 1 - minLevel));
for (const td of this.levelData) {
td.framebuffer.destroy();
td.texture.destroy();
}
this.levelData.length = 0;
for (let i = 0; i < levels; ++i) {
const framebuffer = this.webgl.resources.framebuffer();
const levelSize = Math.pow(2, levels - i - 1);
const size = Vec2.create(levelSize, levelSize);
const invSize = Vec2.create(1 / levelSize, 1 / levelSize);
const texture = this.webgl.resources.texture('image-float32', 'alpha', 'float', 'nearest');
texture.define(levelSize, levelSize);
texture.attachFramebuffer(framebuffer, 0);
this.levelData.push({ texture, framebuffer, size, invSize, offset: 0 });
}
let offset = 0;
for (let i = 0, il = levels; i < il; ++i) {
const td = this.levelData[i];
if (i >= minLevel) {
this.levelData[i].offset = offset;
offset += td.size[0];
}
}
this.clear();
}
setProps(props) {
if (!this.supported)
return;
if (this.props.minLevel !== props.minLevel) {
Object.assign(this.props, props);
const { x, y, width, height } = this.viewport;
this.setViewport(x, y, width, height);
}
else {
Object.assign(this.props, props);
if (!this.props.enabled)
this.clear();
}
}
initDebug(element) {
if (!element.parentElement)
return;
const container = document.createElement('div');
Object.assign(container.style, {
display: 'block',
position: 'absolute',
pointerEvents: 'none',
});
element.parentElement.appendChild(container);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('Could not create canvas 2d context');
Object.assign(canvas.style, {
width: '100%',
height: '100%',
imageRendering: 'pixelated',
position: 'relative',
pointerEvents: 'none',
});
container.appendChild(canvas);
const rect = document.createElement('div');
Object.assign(rect.style, {
display: 'none',
position: 'absolute',
pointerEvents: 'none',
});
element.parentElement.appendChild(rect);
this.debug = { container, canvas, ctx, rect };
}
canDebug(debug) {
return this.supported && this.props.enabled && this.ready && !!this.debug;
}
showRect(p, occluded) {
if (!this.canDebug(this.debug))
return;
const { gl: { drawingBufferHeight }, pixelRatio } = this.webgl;
const { viewport: { x, y, width, height } } = this;
const minx = (p[0] * width + x) / pixelRatio;
const miny = (p[1] * height - y) / pixelRatio;
const maxx = (p[2] * width + x) / pixelRatio;
const maxy = (p[3] * height - y) / pixelRatio;
const oy = (drawingBufferHeight - height) / pixelRatio;
Object.assign(this.debug.rect.style, {
border: occluded ? 'solid red' : 'solid green',
display: 'block',
left: `${minx}px`,
top: `${miny + oy}px`,
width: `${maxx - minx}px`,
height: `${maxy - miny}px`,
});
}
showBuffer(lod) {
if (!this.canDebug(this.debug))
return;
if (lod >= this.levelData.length || lod < this.props.minLevel) {
this.debug.container.style.display = 'none';
return;
}
const { offset, size: [tw, th] } = this.levelData[lod];
const dw = this.tex.getWidth();
const data = new Uint8ClampedArray(tw * th * 4);
data.fill(255);
for (let y = 0; y < th; ++y) {
for (let x = 0; x < tw; ++x) {
const i = (th - y - 1) * tw + x;
const v = this.buffer[y * dw + x + offset] * 255;
data[i * 4 + 0] = v;
data[i * 4 + 3] = 255 - v;
}
}
const imageData = new ImageData(data, tw, th);
this.debug.canvas.width = imageData.width;
this.debug.canvas.height = imageData.height;
this.debug.ctx.putImageData(imageData, 0, 0);
const { viewport: { x, y, width, height }, webgl: { pixelRatio } } = this;
Object.assign(this.debug.container.style, {
display: 'block',
bottom: `${y / pixelRatio}px`,
left: `${x / pixelRatio}px`,
width: `${width / pixelRatio}px`,
height: `${height / pixelRatio}px`,
});
}
debugOcclusion(s) {
if (!this.canDebug(this.debug))
return;
if (!s) {
this.debug.rect.style.display = 'none';
this.debug.container.style.display = 'none';
return;
}
const occluded = this.isOccluded(s);
const { vp, r } = this.transform(s);
const { aabb, lod } = this.project(vp, r);
this.showRect(aabb, occluded);
this.showBuffer(lod);
}
//
dispose() {
if (!this.supported)
return;
this.clear();
this.fb.destroy();
this.tex.destroy();
this.webgl.gl.deleteBuffer(this.buf);
this.renderable.dispose();
for (const td of this.levelData) {
td.framebuffer.destroy();
td.texture.destroy();
}
}
constructor(webgl, drawPass, canvas, props) {
this.webgl = webgl;
this.drawPass = drawPass;
this.viewport = Viewport();
this.near = 0;
this.far = 0;
this.view = Mat4();
this.projection = Mat4();
this.nextNear = 0;
this.nextFar = 0;
this.nextView = Mat4();
this.nextProjection = Mat4();
this.aabb = Vec4();
this.vp = Vec3();
this.levelData = [];
this.sync = null;
this.buffer = new Float32Array(0);
this.frameLag = 0;
this.ready = false;
this.isOccluded = (s) => {
if (!this.supported || !this.props.enabled || !this.ready)
return false;
const { vp, r } = this.transform(s);
const { near, far, projection } = this;
const z = vp[2] + r;
if (-z < near)
return false;
const { aabb, w, h, lod } = this.project(vp, r);
if (lod >= this.levelData.length || lod < this.props.minLevel)
return false;
const { offset, size } = this.levelData[lod];
const u = aabb[0] + w / 2;
const v = aabb[1] + h / 2;
const ts = size[0];
const x = u * ts;
const y = v * ts;
const dx = Math.floor(x);
const dy = Math.ceil(y);
const dw = this.tex.getWidth();
if (dx + 1 >= ts || dy + 1 >= ts)
return false;
const di = (ts - dy - 1) * dw + dx + offset;
if (z > depthToViewZ(this.buffer[di], near, far, projection))
return false;
// const di1 = (ts - dy - 1) * dw + dx + 1 + offset;
if (z > depthToViewZ(this.buffer[di + 1], near, far, projection))
return false;
const di2 = (ts - dy + 1 - 1) * dw + dx + offset;
if (z > depthToViewZ(this.buffer[di2], near, far, projection))
return false;
// const di3 = (ts - dy + 1 - 1) * dw + dx + 1 + offset;
if (z > depthToViewZ(this.buffer[di2 + 1], near, far, projection))
return false;
return true;
};
const { gl, extensions } = webgl;
if (!isWebGL2(gl) || !extensions.colorBufferFloat) {
if (isDebugMode) {
console.log('Missing webgl2 and/or colorBufferFloat support required for "Hi-Z"');
}
this.supported = false;
return;
}
this.fb = webgl.resources.framebuffer();
this.tex = webgl.resources.texture('image-float32', 'alpha', 'float', 'nearest');
// check red/float reading support
this.tex.attachFramebuffer(this.fb, 0);
const implFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
const implType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
if (implFormat !== gl.RED || implType !== gl.FLOAT) {
if (isDebugMode) {
console.log('Missing red/float reading support required for "Hi-Z"');
}
this.supported = false;
return;
}
this.supported = true;
this.props = { ...PD.getDefaultValues(HiZParams), ...props };
this.buf = getBuffer(gl);
this.renderable = createHiZRenderable(webgl, this.drawPass.depthTextureOpaque);
if (isDebugMode && canvas) {
this.initDebug(canvas);
}
}
}