/** * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose */ import { Viewport } from '../mol-canvas3d/camera/util'; import { Camera } from '../mol-canvas3d/camera'; import Scene from './scene'; import { WebGLContext } from './webgl/context'; import { Mat4, Vec3, Vec4, Vec2 } from '../mol-math/linear-algebra'; import { Renderable } from './renderable'; import { Color } from '../mol-util/color'; import { ValueCell } from '../mol-util'; import { RenderableValues, GlobalUniformValues, BaseValues } from './renderable/schema'; import { GraphicsRenderVariant } from './webgl/render-item'; import { ParamDefinition as PD } from '../mol-util/param-definition'; import { deepClone } from '../mol-util/object'; export interface RendererStats { programCount: number shaderCount: number attributeCount: number elementsCount: number framebufferCount: number renderbufferCount: number textureCount: number vertexArrayCount: number drawCount: number instanceCount: number instancedDrawCount: number } interface Renderer { readonly stats: RendererStats readonly props: Readonly clear: (transparentBackground: boolean) => void render: (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean) => void setProps: (props: Partial) => void setViewport: (x: number, y: number, width: number, height: number) => void dispose: () => void } export const RendererParams = { backgroundColor: PD.Color(Color(0x000000), { description: 'Background color of the 3D canvas' }), // the following are general 'material' parameters pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }), interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), interiorColorFlag: PD.Boolean(true, { label: 'Use Interior Color' }), interiorColor: PD.Color(Color.fromNormalizedRgb(0.3, 0.3, 0.3)), highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)), selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)), style: PD.MappedStatic('matte', { custom: PD.Group({ lightIntensity: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }), ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 1.0, step: 0.01 }), metalness: PD.Numeric(0.0, { min: 0.0, max: 1.0, step: 0.01 }), roughness: PD.Numeric(1.0, { min: 0.0, max: 1.0, step: 0.01 }), reflectivity: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), }, { isExpanded: true }), flat: PD.Group({}), matte: PD.Group({}), glossy: PD.Group({}), metallic: PD.Group({}), plastic: PD.Group({}), }, { label: 'Render Style', description: 'Style in which the 3D scene is rendered' }), } export type RendererProps = PD.Values function getStyle(props: RendererProps['style']) { switch (props.name) { case 'custom': return props.params case 'flat': return { lightIntensity: 0, ambientIntensity: 1, metalness: 0, roughness: 0.4, reflectivity: 0.5 } case 'matte': return { lightIntensity: 0.6, ambientIntensity: 0.4, metalness: 0, roughness: 1, reflectivity: 0.5 } case 'glossy': return { lightIntensity: 0.6, ambientIntensity: 0.4, metalness: 0, roughness: 0.4, reflectivity: 0.5 } case 'metallic': return { lightIntensity: 0.6, ambientIntensity: 0.4, metalness: 0.4, roughness: 0.6, reflectivity: 0.5 } case 'plastic': return { lightIntensity: 0.6, ambientIntensity: 0.4, metalness: 0, roughness: 0.2, reflectivity: 0.5 } } } namespace Renderer { export function create(ctx: WebGLContext, props: Partial = {}): Renderer { const { gl, state, stats } = ctx const p = deepClone({ ...PD.getDefaultValues(RendererParams), ...props }) const style = getStyle(p.style) const viewport = Viewport() const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor) const view = Mat4() const invView = Mat4() const modelView = Mat4() const invModelView = Mat4() const invProjection = Mat4() const modelViewProjection = Mat4() const invModelViewProjection = Mat4() const viewOffset = Vec2() const globalUniforms: GlobalUniformValues = { uModel: ValueCell.create(Mat4.identity()), uView: ValueCell.create(view), uInvView: ValueCell.create(invView), uModelView: ValueCell.create(modelView), uInvModelView: ValueCell.create(invModelView), uInvProjection: ValueCell.create(invProjection), uProjection: ValueCell.create(Mat4()), uModelViewProjection: ValueCell.create(modelViewProjection), uInvModelViewProjection: ValueCell.create(invModelViewProjection), uIsOrtho: ValueCell.create(1), uViewOffset: ValueCell.create(viewOffset), uPixelRatio: ValueCell.create(ctx.pixelRatio), uViewportHeight: ValueCell.create(viewport.height), uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)), uCameraPosition: ValueCell.create(Vec3()), uNear: ValueCell.create(1), uFar: ValueCell.create(10000), uFogNear: ValueCell.create(1), uFogFar: ValueCell.create(10000), uFogColor: ValueCell.create(bgColor), uTransparentBackground: ValueCell.create(0), // the following are general 'material' uniforms uLightIntensity: ValueCell.create(style.lightIntensity), uAmbientIntensity: ValueCell.create(style.ambientIntensity), uMetalness: ValueCell.create(style.metalness), uRoughness: ValueCell.create(style.roughness), uReflectivity: ValueCell.create(style.reflectivity), uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold), uInteriorDarkening: ValueCell.create(p.interiorDarkening), uInteriorColorFlag: ValueCell.create(p.interiorColorFlag ? 1 : 0), uInteriorColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.interiorColor)), uHighlightColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.highlightColor)), uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)), } const globalUniformList = Object.entries(globalUniforms) let globalUniformsNeedUpdate = true const renderObject = (r: Renderable, variant: GraphicsRenderVariant) => { const program = r.getProgram(variant) if (r.state.visible) { if (state.currentProgramId !== program.id) { // console.log('new program') globalUniformsNeedUpdate = true program.use() } if (globalUniformsNeedUpdate) { // console.log('globalUniformsNeedUpdate') program.setUniforms(globalUniformList) globalUniformsNeedUpdate = false } if (r.values.dDoubleSided) { if (r.values.dDoubleSided.ref.value) { state.disable(gl.CULL_FACE) } else { state.enable(gl.CULL_FACE) } } else { // webgl default state.disable(gl.CULL_FACE) } if (r.values.dFlipSided) { if (r.values.dFlipSided.ref.value) { state.frontFace(gl.CW) state.cullFace(gl.FRONT) } else { state.frontFace(gl.CCW) state.cullFace(gl.BACK) } } else { // webgl default state.frontFace(gl.CCW) state.cullFace(gl.BACK) } r.render(variant) } } const render = (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean) => { ValueCell.update(globalUniforms.uModel, scene.view) ValueCell.update(globalUniforms.uView, camera.view) ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view)) ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, scene.view, camera.view)) ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView)) ValueCell.update(globalUniforms.uProjection, camera.projection) ValueCell.update(globalUniforms.uInvProjection, Mat4.invert(invProjection, camera.projection)) ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection)) ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection)) ValueCell.update(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0) ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0)) ValueCell.update(globalUniforms.uCameraPosition, camera.state.position) ValueCell.update(globalUniforms.uFar, camera.far) ValueCell.update(globalUniforms.uNear, camera.near) ValueCell.update(globalUniforms.uFogFar, camera.fogFar) ValueCell.update(globalUniforms.uFogNear, camera.fogNear) ValueCell.update(globalUniforms.uTransparentBackground, transparentBackground ? 1 : 0) globalUniformsNeedUpdate = true state.currentRenderItemId = -1 const { renderables } = scene state.disable(gl.SCISSOR_TEST) state.disable(gl.BLEND) state.depthMask(true) state.colorMask(true, true, true, true) state.enable(gl.DEPTH_TEST) if (clear) { if (variant === 'color') { state.clearColor(bgColor[0], bgColor[1], bgColor[2], transparentBackground ? 0 : 1) } else { state.clearColor(1, 1, 1, 1) } gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) } if (variant === 'color') { for (let i = 0, il = renderables.length; i < il; ++i) { const r = renderables[i] if (r.state.opaque) renderObject(r, variant) } state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE) state.enable(gl.BLEND) for (let i = 0, il = renderables.length; i < il; ++i) { const r = renderables[i] if (!r.state.opaque) { state.depthMask(false) renderObject(r, variant) } } } else { // picking & depth for (let i = 0, il = renderables.length; i < il; ++i) { renderObject(renderables[i], variant) } } gl.finish() } return { clear: (transparentBackground: boolean) => { state.depthMask(true) state.colorMask(true, true, true, true) state.clearColor(bgColor[0], bgColor[1], bgColor[2], transparentBackground ? 0 : 1) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) }, render, setProps: (props: Partial) => { if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) { p.backgroundColor = props.backgroundColor Color.toVec3Normalized(bgColor, p.backgroundColor) ValueCell.update(globalUniforms.uFogColor, Vec3.copy(globalUniforms.uFogColor.ref.value, bgColor)) } if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== p.pickingAlphaThreshold) { p.pickingAlphaThreshold = props.pickingAlphaThreshold ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold) } if (props.interiorDarkening !== undefined && props.interiorDarkening !== p.interiorDarkening) { p.interiorDarkening = props.interiorDarkening ValueCell.update(globalUniforms.uInteriorDarkening, p.interiorDarkening) } if (props.interiorColorFlag !== undefined && props.interiorColorFlag !== p.interiorColorFlag) { p.interiorColorFlag = props.interiorColorFlag ValueCell.update(globalUniforms.uInteriorColorFlag, p.interiorColorFlag ? 1 : 0) } if (props.interiorColor !== undefined && props.interiorColor !== p.interiorColor) { p.interiorColor = props.interiorColor ValueCell.update(globalUniforms.uInteriorColor, Color.toVec3Normalized(globalUniforms.uInteriorColor.ref.value, p.interiorColor)) } if (props.highlightColor !== undefined && props.highlightColor !== p.highlightColor) { p.highlightColor = props.highlightColor ValueCell.update(globalUniforms.uHighlightColor, Color.toVec3Normalized(globalUniforms.uHighlightColor.ref.value, p.highlightColor)) } if (props.selectColor !== undefined && props.selectColor !== p.selectColor) { p.selectColor = props.selectColor ValueCell.update(globalUniforms.uSelectColor, Color.toVec3Normalized(globalUniforms.uSelectColor.ref.value, p.selectColor)) } if (props.style !== undefined) { p.style = props.style Object.assign(style, getStyle(props.style)) ValueCell.updateIfChanged(globalUniforms.uLightIntensity, style.lightIntensity) ValueCell.updateIfChanged(globalUniforms.uAmbientIntensity, style.ambientIntensity) ValueCell.updateIfChanged(globalUniforms.uMetalness, style.metalness) ValueCell.updateIfChanged(globalUniforms.uRoughness, style.roughness) ValueCell.updateIfChanged(globalUniforms.uReflectivity, style.reflectivity) } }, setViewport: (x: number, y: number, width: number, height: number) => { gl.viewport(x, y, width, height) if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) { Viewport.set(viewport, x, y, width, height) ValueCell.update(globalUniforms.uViewportHeight, height) ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height)) } }, get props() { return p }, get stats(): RendererStats { return { programCount: ctx.stats.resourceCounts.program, shaderCount: ctx.stats.resourceCounts.shader, attributeCount: ctx.stats.resourceCounts.attribute, elementsCount: ctx.stats.resourceCounts.elements, framebufferCount: ctx.stats.resourceCounts.framebuffer, renderbufferCount: ctx.stats.resourceCounts.renderbuffer, textureCount: ctx.stats.resourceCounts.texture, vertexArrayCount: ctx.stats.resourceCounts.vertexArray, drawCount: stats.drawCount, instanceCount: stats.instanceCount, instancedDrawCount: stats.instancedDrawCount, } }, dispose: () => { // TODO } } } } export default Renderer