/** * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> * @author Ludovic Autin <ludovic.autin@gmail.com> */ import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util'; import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema'; import { ShaderCode } from '../../mol-gl/shader-code'; import { WebGLContext } from '../../mol-gl/webgl/context'; import { Texture } from '../../mol-gl/webgl/texture'; import { deepEqual, ValueCell } from '../../mol-util'; import { createComputeRenderItem } from '../../mol-gl/webgl/render-item'; import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable'; import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import { DrawPass } from './draw'; import { ICamera } from '../../mol-canvas3d/camera'; import { quad_vert } from '../../mol-gl/shader/quad.vert'; import { outlines_frag } from '../../mol-gl/shader/outlines.frag'; import { ssao_frag } from '../../mol-gl/shader/ssao.frag'; import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag'; import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag'; import { Framebuffer } from '../../mol-gl/webgl/framebuffer'; import { Color } from '../../mol-util/color'; import { FxaaParams, FxaaPass } from './fxaa'; import { SmaaParams, SmaaPass } from './smaa'; import { isTimingMode } from '../../mol-util/debug'; import { BackgroundParams, BackgroundPass } from './background'; import { AssetManager } from '../../mol-util/assets'; import { Light } from '../../mol-gl/renderer'; import { shadows_frag } from '../../mol-gl/shader/shadows.frag'; const OutlinesSchema = { ...QuadSchema, tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), uTexSize: UniformSpec('v2'), dOrthographic: DefineSpec('number'), uNear: UniformSpec('f'), uFar: UniformSpec('f'), uInvProjection: UniformSpec('m4'), uOutlineThreshold: UniformSpec('f'), dTransparentOutline: DefineSpec('boolean'), }; type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>> function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable { const width = depthTextureOpaque.getWidth(); const height = depthTextureOpaque.getHeight(); const values: Values<typeof OutlinesSchema> = { ...QuadValues, tDepthOpaque: ValueCell.create(depthTextureOpaque), tDepthTransparent: ValueCell.create(depthTextureTransparent), uTexSize: ValueCell.create(Vec2.create(width, height)), dOrthographic: ValueCell.create(0), uNear: ValueCell.create(1), uFar: ValueCell.create(10000), uInvProjection: ValueCell.create(Mat4.identity()), uOutlineThreshold: ValueCell.create(0.33), dTransparentOutline: ValueCell.create(transparentOutline), }; const schema = { ...OutlinesSchema }; const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag); const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values); return createComputeRenderable(renderItem, values); } const ShadowsSchema = { ...QuadSchema, tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), uTexSize: UniformSpec('v2'), uProjection: UniformSpec('m4'), uInvProjection: UniformSpec('m4'), uBounds: UniformSpec('v4'), dOrthographic: DefineSpec('number'), uNear: UniformSpec('f'), uFar: UniformSpec('f'), dSteps: DefineSpec('number'), uMaxDistance: UniformSpec('f'), uTolerance: UniformSpec('f'), uBias: UniformSpec('f'), uLightDirection: UniformSpec('v3[]'), uLightColor: UniformSpec('v3[]'), dLightCount: DefineSpec('number'), }; type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>> function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable { const width = depthTexture.getWidth(); const height = depthTexture.getHeight(); const values: Values<typeof ShadowsSchema> = { ...QuadValues, tDepth: ValueCell.create(depthTexture), uTexSize: ValueCell.create(Vec2.create(width, height)), uProjection: ValueCell.create(Mat4.identity()), uInvProjection: ValueCell.create(Mat4.identity()), uBounds: ValueCell.create(Vec4()), dOrthographic: ValueCell.create(0), uNear: ValueCell.create(1), uFar: ValueCell.create(10000), dSteps: ValueCell.create(1), uMaxDistance: ValueCell.create(3.0), uTolerance: ValueCell.create(1.0), uBias: ValueCell.create(0.6), uLightDirection: ValueCell.create([]), uLightColor: ValueCell.create([]), dLightCount: ValueCell.create(0), }; const schema = { ...ShadowsSchema }; const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag); const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values); return createComputeRenderable(renderItem, values); } const SsaoSchema = { ...QuadSchema, tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), uSamples: UniformSpec('v3[]'), dNSamples: DefineSpec('number'), uProjection: UniformSpec('m4'), uInvProjection: UniformSpec('m4'), uBounds: UniformSpec('v4'), uTexSize: UniformSpec('v2'), uRadius: UniformSpec('f'), uBias: UniformSpec('f'), dMultiScale: DefineSpec('boolean'), dLevels: DefineSpec('number'), uLevelRadius: UniformSpec('f[]'), uLevelBias: UniformSpec('f[]'), uNearThreshold: UniformSpec('f'), uFarThreshold: UniformSpec('f'), }; type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>> function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable { const values: Values<typeof SsaoSchema> = { ...QuadValues, tDepth: ValueCell.create(depthTexture), tDepthHalf: ValueCell.create(depthHalfTexture), tDepthQuarter: ValueCell.create(depthQuarterTexture), uSamples: ValueCell.create(getSamples(32)), dNSamples: ValueCell.create(32), uProjection: ValueCell.create(Mat4.identity()), uInvProjection: ValueCell.create(Mat4.identity()), uBounds: ValueCell.create(Vec4()), uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)), uRadius: ValueCell.create(Math.pow(2, 5)), uBias: ValueCell.create(0.8), dMultiScale: ValueCell.create(false), dLevels: ValueCell.create(3), uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]), uLevelBias: ValueCell.create([0.8, 0.8, 0.8]), uNearThreshold: ValueCell.create(10.0), uFarThreshold: ValueCell.create(1500.0), }; const schema = { ...SsaoSchema }; const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag); const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values); return createComputeRenderable(renderItem, values); } const SsaoBlurSchema = { ...QuadSchema, tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), uTexSize: UniformSpec('v2'), uKernel: UniformSpec('f[]'), dOcclusionKernelSize: DefineSpec('number'), uBlurDirectionX: UniformSpec('f'), uBlurDirectionY: UniformSpec('f'), uInvProjection: UniformSpec('m4'), uNear: UniformSpec('f'), uFar: UniformSpec('f'), uBounds: UniformSpec('v4'), dOrthographic: DefineSpec('number'), }; type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>> function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable { const values: Values<typeof SsaoBlurSchema> = { ...QuadValues, tSsaoDepth: ValueCell.create(ssaoDepthTexture), uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())), uKernel: ValueCell.create(getBlurKernel(15)), dOcclusionKernelSize: ValueCell.create(15), uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0), uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0), uInvProjection: ValueCell.create(Mat4.identity()), uNear: ValueCell.create(0.0), uFar: ValueCell.create(10000.0), uBounds: ValueCell.create(Vec4()), dOrthographic: ValueCell.create(0), }; const schema = { ...SsaoBlurSchema }; const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag); const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values); return createComputeRenderable(renderItem, values); } function getBlurKernel(kernelSize: number): number[] { const sigma = kernelSize / 3.0; const halfKernelSize = Math.floor((kernelSize + 1) / 2); const kernel = []; for (let x = 0; x < halfKernelSize; x++) { kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma))); } return kernel; } const RandomHemisphereVector: Vec3[] = []; for (let i = 0; i < 256; i++) { const v = Vec3(); v[0] = Math.random() * 2.0 - 1.0; v[1] = Math.random() * 2.0 - 1.0; v[2] = Math.random(); Vec3.normalize(v, v); Vec3.scale(v, v, Math.random()); RandomHemisphereVector.push(v); } function getSamples(nSamples: number): number[] { const samples = []; for (let i = 0; i < nSamples; i++) { let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples); scale = 0.1 + scale * (1.0 - 0.1); samples.push(RandomHemisphereVector[i][0] * scale); samples.push(RandomHemisphereVector[i][1] * scale); samples.push(RandomHemisphereVector[i][2] * scale); } return samples; } const PostprocessingSchema = { ...QuadSchema, tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), tOutlines: 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'), uOutlineColor: UniformSpec('v3'), uOcclusionColor: UniformSpec('v3'), uTransparentBackground: UniformSpec('b'), dOcclusionEnable: DefineSpec('boolean'), uOcclusionOffset: UniformSpec('v2'), dShadowEnable: DefineSpec('boolean'), dOutlineEnable: DefineSpec('boolean'), dOutlineScale: DefineSpec('number'), dTransparentOutline: DefineSpec('boolean'), }; type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>> function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable { const values: Values<typeof PostprocessingSchema> = { ...QuadValues, tSsaoDepth: ValueCell.create(ssaoDepthTexture), tColor: ValueCell.create(colorTexture), tDepthOpaque: ValueCell.create(depthTextureOpaque), tDepthTransparent: ValueCell.create(depthTextureTransparent), tShadows: ValueCell.create(shadowsTexture), tOutlines: ValueCell.create(outlinesTexture), 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)), uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)), uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)), uTransparentBackground: ValueCell.create(false), dOcclusionEnable: ValueCell.create(true), uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)), dShadowEnable: ValueCell.create(false), dOutlineEnable: ValueCell.create(false), dOutlineScale: ValueCell.create(1), dTransparentOutline: ValueCell.create(transparentOutline), }; const schema = { ...PostprocessingSchema }; const shaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag); const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values); return createComputeRenderable(renderItem, values); } export const PostprocessingParams = { occlusion: PD.MappedStatic('on', { on: PD.Group({ samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }), multiScale: PD.MappedStatic('off', { on: PD.Group({ levels: PD.ObjectList({ radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }), bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }), }, o => `${o.radius}, ${o.bias}`, { defaultValue: [ { radius: 2, bias: 1 }, { radius: 5, bias: 1 }, { radius: 8, bias: 1 }, { radius: 11, bias: 1 }, ] }), nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }), farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }), }), off: PD.Group({}) }, { cycle: true }), radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }), bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }), blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }), resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }), color: PD.Color(Color(0x000000)), }), off: PD.Group({}) }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }), shadow: PD.MappedStatic('off', { on: PD.Group({ steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }), bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }), maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }), tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }), }), off: PD.Group({}) }, { cycle: true, description: 'Simplistic shadows' }), outline: PD.MappedStatic('off', { on: PD.Group({ scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }), threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }), color: PD.Color(Color(0x000000)), includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }), }), off: PD.Group({}) }, { cycle: true, description: 'Draw outline around 3D objects' }), antialiasing: PD.MappedStatic('smaa', { fxaa: PD.Group(FxaaParams), smaa: PD.Group(SmaaParams), off: PD.Group({}) }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }), background: PD.Group(BackgroundParams, { isFlat: true }), }; export type PostprocessingProps = PD.Values<typeof PostprocessingParams> type Levels = { count: number radius: number[] bias: number[] } function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels { const count = props.length; const { radius, bias } = levels || { radius: (new Array(count * 3)).fill(0), bias: (new Array(count * 3)).fill(0), }; props = props.slice().sort((a, b) => a.radius - b.radius); for (let i = 0; i < count; ++i) { const p = props[i]; radius[i] = Math.pow(2, p.radius); bias[i] = p.bias; } return { count, radius, bias }; } export class PostprocessingPass { static isEnabled(props: PostprocessingProps) { return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off'; } static isTransparentOutlineEnabled(props: PostprocessingProps) { return props.outline.name === 'on' && props.outline.params.includeTransparent; } readonly target: RenderTarget; private readonly outlinesTarget: RenderTarget; private readonly outlinesRenderable: OutlinesRenderable; private readonly shadowsTarget: RenderTarget; private readonly shadowsRenderable: ShadowsRenderable; private readonly ssaoFramebuffer: Framebuffer; private readonly ssaoBlurFirstPassFramebuffer: Framebuffer; private readonly ssaoBlurSecondPassFramebuffer: Framebuffer; private readonly downsampledDepthTarget: RenderTarget; private readonly downsampleDepthRenderable: CopyRenderable; private readonly depthHalfTarget: RenderTarget; private readonly depthHalfRenderable: CopyRenderable; private readonly depthQuarterTarget: RenderTarget; private readonly depthQuarterRenderable: CopyRenderable; private readonly ssaoDepthTexture: Texture; private readonly ssaoDepthBlurProxyTexture: Texture; private readonly ssaoRenderable: SsaoRenderable; private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable; private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable; private nSamples: number; private blurKernelSize: number; private downsampleFactor: number; private readonly renderable: PostprocessingRenderable; private ssaoScale: number; private calcSsaoScale() { // downscale ssao for high pixel-ratios return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor; } private levels: { radius: number, bias: number }[]; private readonly bgColor = Vec3(); readonly background: BackgroundPass; constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) { const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass; const width = colorTarget.getWidth(); const height = colorTarget.getHeight(); this.nSamples = 1; this.blurKernelSize = 1; this.downsampleFactor = 1; this.ssaoScale = this.calcSsaoScale(); this.levels = []; // needs to be linear for anti-aliasing pass this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear'); this.outlinesTarget = webgl.createRenderTarget(width, height, false); this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true); this.shadowsTarget = webgl.createRenderTarget(width, height, false); this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque); this.ssaoFramebuffer = webgl.resources.framebuffer(); this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer(); this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer(); const sw = Math.floor(width * this.ssaoScale); const sh = Math.floor(height * this.ssaoScale); const hw = Math.max(1, Math.floor(sw * 0.5)); const hh = Math.max(1, Math.floor(sh * 0.5)); const qw = Math.max(1, Math.floor(sw * 0.25)); const qh = Math.max(1, Math.floor(sh * 0.25)); this.downsampledDepthTarget = drawPass.packedDepth ? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba') : webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba'); this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque); this.depthHalfTarget = drawPass.packedDepth ? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba') : webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba'); this.depthHalfRenderable = createCopyRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture); this.depthQuarterTarget = drawPass.packedDepth ? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba') : webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba'); this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture); this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); this.ssaoDepthTexture.define(sw, sh); this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0'); this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); this.ssaoDepthBlurProxyTexture.define(sw, sh); this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0'); this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0'); this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture); this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal'); this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical'); this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true); this.background = new BackgroundPass(webgl, assetManager, width, height); } setSize(width: number, height: number) { const [w, h] = this.renderable.values.uTexSize.ref.value; const ssaoScale = this.calcSsaoScale(); if (width !== w || height !== h || this.ssaoScale !== ssaoScale) { this.ssaoScale = ssaoScale; this.target.setSize(width, height); this.outlinesTarget.setSize(width, height); this.shadowsTarget.setSize(width, height); const sw = Math.floor(width * this.ssaoScale); const sh = Math.floor(height * this.ssaoScale); this.downsampledDepthTarget.setSize(sw, sh); this.ssaoDepthTexture.define(sw, sh); this.ssaoDepthBlurProxyTexture.define(sw, sh); const hw = Math.max(1, Math.floor(sw * 0.5)); const hh = Math.max(1, Math.floor(sh * 0.5)); this.depthHalfTarget.setSize(hw, hh); const qw = Math.max(1, Math.floor(sw * 0.25)); const qh = Math.max(1, Math.floor(sh * 0.25)); this.depthQuarterTarget.setSize(qw, qh); ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height)); ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height)); ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height)); ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh)); ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh)); ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh)); this.background.setSize(width, height); } } private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) { let needsUpdateShadows = false; let needsUpdateMain = false; let needsUpdateSsao = false; let needsUpdateSsaoBlur = false; let needsUpdateOutlines = false; const orthographic = camera.state.mode === 'orthographic' ? 1 : 0; const outlinesEnabled = props.outline.name === 'on'; const shadowsEnabled = props.shadow.name === 'on'; const occlusionEnabled = props.occlusion.name === 'on'; const invProjection = Mat4.identity(); Mat4.invert(invProjection, camera.projection); const [w, h] = this.renderable.values.uTexSize.ref.value; const v = camera.viewport; if (props.occlusion.name === 'on') { ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection); ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection); const b = this.ssaoRenderable.values.uBounds; const s = this.ssaoScale; Vec4.set(b.ref.value, Math.floor(v.x * s) / (w * s), Math.floor(v.y * s) / (h * s), Math.ceil((v.x + v.width) * s) / (w * s), Math.ceil((v.y + v.height) * s) / (h * s) ); ValueCell.update(b, b.ref.value); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value); ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection); if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic); } if (this.nSamples !== props.occlusion.params.samples) { needsUpdateSsao = true; this.nSamples = props.occlusion.params.samples; ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples)); ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples); } const multiScale = props.occlusion.params.multiScale.name === 'on'; if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) { needsUpdateSsao = true; ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale); } if (props.occlusion.params.multiScale.name === 'on') { const mp = props.occlusion.params.multiScale.params; if (!deepEqual(this.levels, mp.levels)) { needsUpdateSsao = true; this.levels = mp.levels; const levels = getLevels(mp.levels); ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count); ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius); ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias); } ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold); ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold); } else { ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius)); } ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias); if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) { needsUpdateSsaoBlur = true; this.blurKernelSize = props.occlusion.params.blurKernelSize; const kernel = getBlurKernel(this.blurKernelSize); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize); } if (this.downsampleFactor !== props.occlusion.params.resolutionScale) { needsUpdateSsao = true; this.downsampleFactor = props.occlusion.params.resolutionScale; this.ssaoScale = this.calcSsaoScale(); const sw = Math.floor(w * this.ssaoScale); const sh = Math.floor(h * this.ssaoScale); this.downsampledDepthTarget.setSize(sw, sh); this.ssaoDepthTexture.define(sw, sh); this.ssaoDepthBlurProxyTexture.define(sw, sh); const hw = Math.floor(sw * 0.5); const hh = Math.floor(sh * 0.5); this.depthHalfTarget.setSize(hw, hh); const qw = Math.floor(sw * 0.25); const qh = Math.floor(sh * 0.25); this.depthQuarterTarget.setSize(qw, qh); if (this.ssaoScale === 1) { ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureOpaque); } else { ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture); } ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture); ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture); ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh)); ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh)); ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh)); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh)); } ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color)); } if (props.shadow.name === 'on') { ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection); ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection); Vec4.set(this.shadowsRenderable.values.uBounds.ref.value, v.x / w, v.y / h, (v.x + v.width) / w, (v.y + v.height) / h ); ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value); ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far); if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) { ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic); needsUpdateShadows = true; } ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance); ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance); ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias); if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) { ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps); needsUpdateShadows = true; } ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction); ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color); if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) { ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count); needsUpdateShadows = true; } } if (props.outline.name === 'on') { const transparentOutline = props.outline.params.includeTransparent ?? true; const outlineScale = props.outline.params.scale - 1; const outlineThreshold = 50 * props.outline.params.threshold; ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far); ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection); if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateOutlines = true; ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline); } if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateOutlines = true; ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic); } ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold); ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color)); if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dOutlineScale, outlineScale); } if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline); } } ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far); ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.renderable.values.uFogFar, camera.fogFar); ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear); ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor)); ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground); if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dOrthographic, orthographic); } if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled); } if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled); } if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled); } if (needsUpdateOutlines) { this.outlinesRenderable.update(); } if (needsUpdateShadows) { this.shadowsRenderable.update(); } if (needsUpdateSsao) { this.ssaoRenderable.update(); } if (needsUpdateSsaoBlur) { this.ssaoBlurFirstPassRenderable.update(); this.ssaoBlurSecondPassRenderable.update(); } if (needsUpdateMain) { this.renderable.update(); } const { gl, state } = this.webgl; state.enable(gl.SCISSOR_TEST); state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); state.depthMask(false); } private occlusionOffset: [x: number, y: number] = [0, 0]; setOcclusionOffset(x: number, y: number) { this.occlusionOffset[0] = x; this.occlusionOffset[1] = y; ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y)); } private transparentBackground = false; setTransparentBackground(value: boolean) { this.transparentBackground = value; } render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) { if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render'); this.updateState(camera, transparentBackground, backgroundColor, props, light); const { gl, state } = this.webgl; const { x, y, width, height } = camera.viewport; // don't render occlusion if offset is given, // which will reuse the existing occlusion if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) { if (isTimingMode) this.webgl.timer.mark('SSAO.render'); const sx = Math.floor(x * this.ssaoScale); const sy = Math.floor(y * this.ssaoScale); const sw = Math.ceil(width * this.ssaoScale); const sh = Math.ceil(height * this.ssaoScale); state.viewport(sx, sy, sw, sh); state.scissor(sx, sy, sw, sh); if (this.ssaoScale < 1) { if (isTimingMode) this.webgl.timer.mark('SSAO.downsample'); this.downsampledDepthTarget.bind(); this.downsampleDepthRenderable.render(); if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample'); } if (isTimingMode) this.webgl.timer.mark('SSAO.half'); this.depthHalfTarget.bind(); this.depthHalfRenderable.render(); if (isTimingMode) this.webgl.timer.markEnd('SSAO.half'); if (isTimingMode) this.webgl.timer.mark('SSAO.quarter'); this.depthQuarterTarget.bind(); this.depthQuarterRenderable.render(); if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter'); this.ssaoFramebuffer.bind(); this.ssaoRenderable.render(); this.ssaoBlurFirstPassFramebuffer.bind(); this.ssaoBlurFirstPassRenderable.render(); this.ssaoBlurSecondPassFramebuffer.bind(); this.ssaoBlurSecondPassRenderable.render(); if (isTimingMode) this.webgl.timer.markEnd('SSAO.render'); } state.viewport(x, y, width, height); state.scissor(x, y, width, height); if (props.outline.name === 'on') { this.outlinesTarget.bind(); this.outlinesRenderable.render(); } if (props.shadow.name === 'on') { this.shadowsTarget.bind(); this.shadowsRenderable.render(); } if (toDrawingBuffer) { this.webgl.unbindFramebuffer(); } else { this.target.bind(); } this.background.update(camera, props.background); if (this.background.isEnabled(props.background)) { if (this.transparentBackground) { state.clearColor(0, 0, 0, 0); } else { Color.toVec3Normalized(this.bgColor, backgroundColor); state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1); } gl.clear(gl.COLOR_BUFFER_BIT); state.enable(gl.BLEND); state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); this.background.render(); } else { state.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); } this.renderable.render(); if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render'); } } export class AntialiasingPass { static isEnabled(props: PostprocessingProps) { return props.antialiasing.name !== 'off'; } readonly target: RenderTarget; private readonly fxaa: FxaaPass; private readonly smaa: SmaaPass; constructor(webgl: WebGLContext, private drawPass: DrawPass) { const { colorTarget } = drawPass; const width = colorTarget.getWidth(); const height = colorTarget.getHeight(); this.target = webgl.createRenderTarget(width, height, false); this.fxaa = new FxaaPass(webgl, this.target.texture); this.smaa = new SmaaPass(webgl, this.target.texture); } setSize(width: number, height: number) { const w = this.target.texture.getWidth(); const h = this.target.texture.getHeight(); if (width !== w || height !== h) { this.target.setSize(width, height); this.fxaa.setSize(width, height); if (this.smaa.supported) this.smaa.setSize(width, height); } } private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) { if (props.antialiasing.name !== 'fxaa') return; const input = PostprocessingPass.isEnabled(props) ? this.drawPass.postprocessing.target.texture : this.drawPass.colorTarget.texture; this.fxaa.update(input, props.antialiasing.params); this.fxaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target); } private _renderSmaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) { if (props.antialiasing.name !== 'smaa') return; const input = PostprocessingPass.isEnabled(props) ? this.drawPass.postprocessing.target.texture : this.drawPass.colorTarget.texture; this.smaa.update(input, props.antialiasing.params); this.smaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target); } render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) { if (props.antialiasing.name === 'off') return; if (props.antialiasing.name === 'fxaa') { this._renderFxaa(camera, toDrawingBuffer, props); } else if (props.antialiasing.name === 'smaa') { if (!this.smaa.supported) { throw new Error('SMAA not supported, missing "HTMLImageElement"'); } this._renderSmaa(camera, toDrawingBuffer, props); } } }