molstar
Version:
A comprehensive macromolecular library.
425 lines (424 loc) • 22 kB
JavaScript
/**
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { DefineSpec, TextureSpec, UniformSpec } from '../../mol-gl/renderable/schema';
import { ValueCell } from '../../mol-util';
import { isTimingMode } from '../../mol-util/debug';
import { ShaderCode } from '../../mol-gl/shader-code';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { createComputeRenderable } from '../../mol-gl/renderable';
import { trace_frag } from '../../mol-gl/shader/illumination/trace.frag';
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color/color';
import { accumulate_frag } from '../../mol-gl/shader/illumination/accumulate.frag';
import { now } from '../../mol-util/now';
import { clamp } from '../../mol-math/interpolate';
export const TracingParams = {
rendersPerFrame: PD.Interval([1, 16], { min: 1, max: 64, step: 1 }, { description: 'Number of rays per pixel each frame. May be adjusted to reach targetFps but will stay within given interval.' }),
targetFps: PD.Numeric(30, { min: 0, max: 120, step: 0.1 }, { description: 'Target FPS per frame. If observed FPS is lower or higher, some parameters may get adjusted.' }),
steps: PD.Numeric(32, { min: 1, max: 1024, step: 1 }),
firstStepSize: PD.Numeric(0.01, { min: 0.001, max: 1, step: 0.001 }),
refineSteps: PD.Numeric(4, { min: 0, max: 8, step: 1 }, { description: 'Number of refine steps per ray hit. May be lower to reach targetFps.' }),
rayDistance: PD.Numeric(256, { min: 1, max: 8192, step: 1 }, { description: 'Maximum distance a ray can travel (in world units).' }),
thicknessMode: PD.Select('auto', PD.arrayToOptions(['auto', 'fixed'])),
minThickness: PD.Numeric(0.5, { min: 0.1, max: 16, step: 0.1 }, { hideIf: p => p.thicknessMode === 'fixed' }),
thicknessFactor: PD.Numeric(1, { min: 0.1, max: 2, step: 0.05 }, { hideIf: p => p.thicknessMode === 'fixed' }),
thickness: PD.Numeric(4, { min: 0.1, max: 512, step: 0.1 }, { hideIf: p => p.thicknessMode === 'auto' }),
bounces: PD.Numeric(4, { min: 1, max: 32, step: 1 }, { description: 'Number of bounces for each ray.' }),
glow: PD.Boolean(true, { description: 'Bounced rays always get the full light. This produces a slight glowing effect.' }),
shadowEnable: PD.Boolean(false),
shadowSoftness: PD.Numeric(0.1, { min: 0.01, max: 1.0, step: 0.01 }),
shadowThickness: PD.Numeric(0.5, { min: 0.1, max: 32, step: 0.1 }),
};
export class TracingPass {
constructor(webgl, drawPass) {
this.webgl = webgl;
this.drawPass = drawPass;
this.clearAdjustedProps = true;
this.prevTime = 0;
this.currTime = 0;
this.rendersPerFrame = 1;
this.refineSteps = 1;
this.steps = 16;
const { extensions: { drawBuffers, colorBufferHalfFloat, textureHalfFloat }, resources, isWebGL2 } = webgl;
const { depthTextureOpaque } = drawPass;
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
if (isWebGL2) {
this.shadedTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.shadedTextureOpaque.define(width, height);
this.normalTextureOpaque = colorBufferHalfFloat && textureHalfFloat
? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.normalTextureOpaque.define(width, height);
this.colorTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.colorTextureOpaque.define(width, height);
}
else {
// webgl1 requires consistent bit plane counts
this.shadedTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.shadedTextureOpaque.define(width, height);
this.normalTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.normalTextureOpaque.define(width, height);
this.colorTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.colorTextureOpaque.define(width, height);
}
this.framebuffer = resources.framebuffer();
this.framebuffer.bind();
drawBuffers.drawBuffers([
drawBuffers.COLOR_ATTACHMENT0,
drawBuffers.COLOR_ATTACHMENT1,
drawBuffers.COLOR_ATTACHMENT2,
]);
this.shadedTextureOpaque.attachFramebuffer(this.framebuffer, 'color0');
this.normalTextureOpaque.attachFramebuffer(this.framebuffer, 'color1');
this.colorTextureOpaque.attachFramebuffer(this.framebuffer, 'color2');
this.thicknessTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'nearest');
this.holdTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.accumulateTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.composeTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.traceRenderable = getTraceRenderable(webgl, this.colorTextureOpaque, this.normalTextureOpaque, this.shadedTextureOpaque, this.thicknessTarget.texture, this.accumulateTarget.texture, this.drawPass.depthTextureOpaque);
this.accumulateRenderable = getAccumulateRenderable(webgl, this.holdTarget.texture);
}
renderInput(renderer, camera, scene, props) {
if (isTimingMode)
this.webgl.timer.mark('TracePass.renderInput');
const { gl, state } = this.webgl;
this.framebuffer.bind();
this.drawPass.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
renderer.clear(true);
renderer.renderTracing(scene.primitives, camera);
//
if (props.thicknessMode === 'auto') {
this.thicknessTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
renderer.renderDepthOpaqueBack(scene.primitives, camera);
}
if (isTimingMode)
this.webgl.timer.markEnd('TracePass.renderInput');
}
setSize(width, height) {
const w = this.composeTarget.getWidth();
const h = this.composeTarget.getHeight();
if (width !== w || height !== h) {
this.thicknessTarget.setSize(width, height);
this.holdTarget.setSize(width, height);
this.accumulateTarget.setSize(width, height);
this.composeTarget.setSize(width, height);
this.colorTextureOpaque.define(width, height);
this.normalTextureOpaque.define(width, height);
this.shadedTextureOpaque.define(width, height);
ValueCell.update(this.traceRenderable.values.uTexSize, Vec2.set(this.traceRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.accumulateRenderable.values.uTexSize, Vec2.set(this.accumulateRenderable.values.uTexSize.ref.value, width, height));
}
}
reset() {
const { drawBuffers } = this.webgl.extensions;
this.framebuffer.bind();
drawBuffers.drawBuffers([
drawBuffers.COLOR_ATTACHMENT0,
drawBuffers.COLOR_ATTACHMENT1,
drawBuffers.COLOR_ATTACHMENT2,
]);
this.shadedTextureOpaque.attachFramebuffer(this.framebuffer, 'color0');
this.normalTextureOpaque.attachFramebuffer(this.framebuffer, 'color1');
this.colorTextureOpaque.attachFramebuffer(this.framebuffer, 'color2');
this.restart(true);
}
restart(clearAdjustedProps = false) {
const { gl, state } = this.webgl;
this.accumulateTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this.composeTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
if (clearAdjustedProps) {
this.prevTime = 0;
this.currTime = 0;
this.clearAdjustedProps = true;
}
}
increaseAdjustedProps(props) {
this.steps += 1;
if (this.steps > props.steps) {
this.refineSteps += 1;
}
if (this.refineSteps > props.refineSteps) {
this.rendersPerFrame += 1;
}
}
decreaseAdjustedProps(props) {
const minRefineSteps = Math.min(1, props.refineSteps);
this.rendersPerFrame -= 1;
if (this.rendersPerFrame < 1) {
this.refineSteps -= 1;
}
if (this.refineSteps < minRefineSteps) {
this.steps -= 1;
}
}
getAdjustedProps(props, iteration) {
this.currTime = now();
const minRefineSteps = Math.min(1, props.refineSteps);
const minSteps = Math.round(props.steps / 2);
if (this.clearAdjustedProps) {
this.rendersPerFrame = props.rendersPerFrame[0];
this.refineSteps = minRefineSteps;
this.steps = minSteps;
this.clearAdjustedProps = false;
}
if (iteration > 0) {
const targetTimeMs = 1000 / props.targetFps;
const deltaTime = this.currTime - this.prevTime;
let f = Math.round(deltaTime / targetTimeMs);
if (f >= 2) {
while (f > 0) {
this.decreaseAdjustedProps(props);
f -= 1;
}
}
else if (deltaTime < targetTimeMs) {
this.increaseAdjustedProps(props);
}
else if (deltaTime > targetTimeMs + 0.5) {
this.decreaseAdjustedProps(props);
}
}
this.prevTime = this.currTime;
this.rendersPerFrame = clamp(this.rendersPerFrame, props.rendersPerFrame[0], props.rendersPerFrame[1]);
this.refineSteps = clamp(this.refineSteps, minRefineSteps, props.refineSteps);
this.steps = clamp(this.steps, minSteps, props.steps);
return {
rendersPerFrame: iteration === 0 ? Math.ceil(this.rendersPerFrame / 2) : this.rendersPerFrame,
refineSteps: iteration === 0 ? minRefineSteps : this.refineSteps,
steps: iteration === 0 ? minSteps : this.steps,
};
}
render(ctx, transparentBackground, props, iteration, forceRenderInput) {
const { rendersPerFrame, refineSteps, steps } = this.getAdjustedProps(props, iteration);
if (isTimingMode) {
this.webgl.timer.mark('TracePass.render', {
note: `${rendersPerFrame} rendersPerFrame, ${refineSteps} refineSteps, ${steps} steps`
});
}
const { renderer, camera, scene } = ctx;
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
if (iteration === 0 || forceRenderInput) {
// render color & depth
renderer.setTransparentBackground(transparentBackground);
renderer.setDrawingBufferSize(this.composeTarget.getWidth(), this.composeTarget.getHeight());
renderer.setPixelRatio(this.webgl.pixelRatio);
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
this.renderInput(renderer, camera, scene, props);
}
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.disable(gl.CULL_FACE);
state.depthMask(false);
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const [w, h] = this.traceRenderable.values.uTexSize.ref.value;
const v = camera.viewport;
const ambientColor = Vec3();
Vec3.scale(ambientColor, Color.toArrayNormalized(renderer.props.ambientColor, ambientColor, 0), renderer.props.ambientIntensity);
const lightStrength = Vec3.clone(ambientColor);
for (let i = 0, il = renderer.light.count; i < il; ++i) {
const light = Vec3.fromArray(Vec3(), renderer.light.color, i * 3);
Vec3.add(lightStrength, lightStrength, light);
}
// trace
this.holdTarget.bind();
let needsUpdateTrace = false;
ValueCell.update(this.traceRenderable.values.uFrameNo, iteration);
if (this.traceRenderable.values.dRendersPerFrame.ref.value !== rendersPerFrame) {
ValueCell.update(this.traceRenderable.values.dRendersPerFrame, rendersPerFrame);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uProjection, camera.projection);
ValueCell.update(this.traceRenderable.values.uInvProjection, invProjection);
Vec4.set(this.traceRenderable.values.uBounds.ref.value, v.x / w, v.y / h, (v.x + v.width) / w, (v.y + v.height) / h);
ValueCell.update(this.traceRenderable.values.uBounds, this.traceRenderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.traceRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.traceRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.traceRenderable.values.uFogFar, camera.fogFar);
ValueCell.updateIfChanged(this.traceRenderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.traceRenderable.values.uFogColor, Color.toVec3Normalized(this.traceRenderable.values.uFogColor.ref.value, renderer.props.backgroundColor));
if (this.traceRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.traceRenderable.values.dOrthographic, orthographic);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uLightDirection, renderer.light.direction);
ValueCell.update(this.traceRenderable.values.uLightColor, renderer.light.color);
if (this.traceRenderable.values.dLightCount.ref.value !== renderer.light.count) {
ValueCell.update(this.traceRenderable.values.dLightCount, renderer.light.count);
needsUpdateTrace = true;
}
ValueCell.update(this.traceRenderable.values.uAmbientColor, ambientColor);
ValueCell.update(this.traceRenderable.values.uLightStrength, lightStrength);
if (this.traceRenderable.values.dGlow.ref.value !== props.glow) {
ValueCell.update(this.traceRenderable.values.dGlow, props.glow);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dBounces.ref.value !== props.bounces) {
ValueCell.update(this.traceRenderable.values.dBounces, props.bounces);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dSteps.ref.value !== steps) {
ValueCell.update(this.traceRenderable.values.dSteps, steps);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dFirstStepSize.ref.value !== props.firstStepSize) {
ValueCell.update(this.traceRenderable.values.dFirstStepSize, props.firstStepSize);
needsUpdateTrace = true;
}
if (this.traceRenderable.values.dRefineSteps.ref.value !== refineSteps) {
ValueCell.update(this.traceRenderable.values.dRefineSteps, refineSteps);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uRayDistance, props.rayDistance);
if (this.traceRenderable.values.dThicknessMode.ref.value !== props.thicknessMode) {
ValueCell.update(this.traceRenderable.values.dThicknessMode, props.thicknessMode);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uMinThickness, props.minThickness);
ValueCell.updateIfChanged(this.traceRenderable.values.uThicknessFactor, props.thicknessFactor);
ValueCell.updateIfChanged(this.traceRenderable.values.uThickness, props.thickness);
if (this.traceRenderable.values.dShadowEnable.ref.value !== props.shadowEnable) {
ValueCell.update(this.traceRenderable.values.dShadowEnable, props.shadowEnable);
needsUpdateTrace = true;
}
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowSoftness, props.shadowSoftness);
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowThickness, props.shadowThickness);
if (needsUpdateTrace)
this.traceRenderable.update();
if (isTimingMode)
this.webgl.timer.mark('TracePass.renderTrace');
this.traceRenderable.render();
if (isTimingMode)
this.webgl.timer.markEnd('TracePass.renderTrace');
// accumulate
this.accumulateTarget.bind();
this.accumulateRenderable.render();
if (isTimingMode)
this.webgl.timer.markEnd('TracePass.render');
}
}
//
const TraceSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tThickness: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tAccumulate: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uFogNear: UniformSpec('f'),
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
uAmbientColor: UniformSpec('v3'),
uLightStrength: UniformSpec('v3'),
uFrameNo: UniformSpec('i'),
dRendersPerFrame: DefineSpec('number'),
dGlow: DefineSpec('boolean'),
dBounces: DefineSpec('number'),
dSteps: DefineSpec('number'),
dFirstStepSize: DefineSpec('number'),
dRefineSteps: DefineSpec('number'),
uRayDistance: UniformSpec('f'),
dThicknessMode: DefineSpec('string'),
uMinThickness: UniformSpec('f'),
uThicknessFactor: UniformSpec('f'),
uThickness: UniformSpec('f'),
dShadowEnable: DefineSpec('boolean'),
uShadowSoftness: UniformSpec('f'),
uShadowThickness: UniformSpec('f'),
};
const TraceShaderCode = ShaderCode('trace', quad_vert, trace_frag);
function getTraceRenderable(ctx, colorTexture, normalTexture, shadedTexture, thicknessTexture, accumulateTexture, depthTexture) {
const values = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
tNormal: ValueCell.create(normalTexture),
tShaded: ValueCell.create(shadedTexture),
tThickness: ValueCell.create(thicknessTexture),
tAccumulate: ValueCell.create(accumulateTexture),
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uFogNear: ValueCell.create(10000),
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
uAmbientColor: ValueCell.create(Vec3()),
uLightStrength: ValueCell.create(Vec3.create(1, 1, 1)),
uFrameNo: ValueCell.create(0),
dRendersPerFrame: ValueCell.create(1),
dGlow: ValueCell.create(true),
dBounces: ValueCell.create(4),
dSteps: ValueCell.create(32),
dFirstStepSize: ValueCell.create(0.01),
dRefineSteps: ValueCell.create(4),
uRayDistance: ValueCell.create(256),
dThicknessMode: ValueCell.create('auto'),
uMinThickness: ValueCell.create(0.5),
uThicknessFactor: ValueCell.create(1),
uThickness: ValueCell.create(4),
dShadowEnable: ValueCell.create(false),
uShadowSoftness: ValueCell.create(0.1),
uShadowThickness: ValueCell.create(0.1),
};
const schema = { ...TraceSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', TraceShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
//
const AccumulateSchema = {
...QuadSchema,
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uWeight: UniformSpec('f'),
};
const AccumulateShaderCode = ShaderCode('accumulate', quad_vert, accumulate_frag);
function getAccumulateRenderable(ctx, colorTexture) {
const values = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
uWeight: ValueCell.create(1.0),
};
const schema = { ...AccumulateSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', AccumulateShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}