UNPKG

molstar

Version:

A comprehensive macromolecular library.

425 lines (424 loc) 22 kB
/** * 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); }