UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

1,652 lines (1,382 loc) 71.9 kB
/** * Simplified webGL renderer */ import { Camera } from "./Camera"; import { DoubleSide, BackSide } from "./constants/Sides"; import { UnsignedByteType, RGBAFormat, NearestFilter, } from "./constants/TextureConstants"; import { Light } from "./core"; import { Color } from "../colors"; import { MeshOutlineMaterial, SphereImposterOutlineMaterial, StickImposterOutlineMaterial, } from "./materials"; import { Matrix4, Vector3, Matrix3 } from "./math"; import { Mesh, Line, Sprite } from "./objects"; import { ShaderLib, ShaderUtils } from "./shaders"; import { SpritePlugin } from "./SpritePlugin"; // share a single offscreen renderer across all Renderers var _offscreen_singleton = null; var _gl_singleton = null; export class Renderer { row: any; col: any; rows: any; cols: any; context = null; devicePixelRatio = 1.0; //set in setSize domElement: HTMLCanvasElement; // scene graph sortObjects = true; autoUpdateObjects = true; autoUpdateScene = true; // info info = { memory: { programs: 0, geometries: 0, textures: 0, }, render: { calls: 0, vertices: 0, faces: 0, points: 0, }, }; // webgl rednering context private _gl: WebGLRenderingContext | WebGL2RenderingContext; private _offscreen: OffscreenCanvas = null; private _bitmap: ImageBitmapRenderingContext = null; // internal properties private _programs = []; private _programs_counter = 0; private _webglversion = 1; // internal state cache private _currentProgram = null; private _currentMaterialId = -1; private _currentGeometryGroupHash = null; private _currentCamera = null; private _geometryGroupCounter = 0; // GL state cache private _oldDoubleSided = -1 as number | boolean; private _oldFlipSided = -1 as number | boolean; private _oldDepthTest = -1; private _oldDepthWrite = -1; private _oldPolygonOffset = null; private _oldLineWidth = null; private _viewportWidth = 0; private _viewportHeight = 0; private _currentWidth = 0; private _currentHeight = 0; private _enabledAttributes = {}; // camera matrices cache private _vector3 = new Vector3(); private _worldInverse = new Matrix4(); private _projInverse = new Matrix4(); private _textureMatrix = new Matrix4(); private _fullProjModelMatrix = new Matrix4(); private _fullProjModelMatrixInv = new Matrix4(); // light arrays cach private _direction = new Vector3(); private _lightsNeedUpdate = true; private _lights = { ambient: [0, 0, 0], directional: { length: 0, colors: [], positions: [], }, point: { length: 0, colors: [], positions: [], distances: [], }, spot: { length: 0, colors: [], positions: [], distances: [], directions: [], anglesCos: [], exponents: [], }, hemi: { length: 0, skyColors: [], groundColors: [], positions: [], }, }; sprites = new SpritePlugin(); //screensshader related variables private _screenshader = null; private _AOshader = null; private _blurshader = null; private _vertexattribpos = null; private _aovertexattribpos = null; private _blurvertexattribpos = null; private _screenQuadVBO = null; //framebuffer variables private _fb = null; private _targetTexture = null; private _depthTexture = null; private _shadingTexture = null; private _scratchTexture = null; private _canvas: any; private _precision: any; private _alpha: any; private _premultipliedAlpha: any; private _antialias: any; private _upscale: boolean | null = null; private _preserveDrawingBuffer: any; private _clearColor: Color; private _clearAlpha: any; private _outlineMaterial: MeshOutlineMaterial; private _outlineSphereImposterMaterial: SphereImposterOutlineMaterial; private _outlineStickImposterMaterial: StickImposterOutlineMaterial; private _outlineEnabled: boolean; private _AOEnabled: boolean; private _AOstrength: number = 1.0; private _AOradius: number = 5.0; private _extInstanced: any; private _extFragDepth: ReturnType<WebGL2RenderingContext["getExtension"]>; private _extFloatLinear: ReturnType<WebGL2RenderingContext["getExtension"]>; private _extColorBufferFloat: ReturnType<WebGL2RenderingContext["getExtension"]>; private SHADE_TEXTURE: number = 3; constructor(parameters) { parameters = parameters || {}; this.row = parameters.row; this.col = parameters.col; this.rows = parameters.rows; this.cols = parameters.cols; this._canvas = parameters.canvas !== undefined ? parameters.canvas : document.createElement("canvas"); this._precision = parameters.precision !== undefined ? parameters.precision : "highp"; this._alpha = parameters.alpha !== undefined ? parameters.alpha : true; this._premultipliedAlpha = parameters.premultipliedAlpha !== undefined ? parameters.premultipliedAlpha : true; this._antialias = parameters.antialias !== undefined ? parameters.antialias : false; this._upscale = parameters.upscale !== undefined ? parameters.upscale : this._antialias; this._preserveDrawingBuffer = parameters.preserveDrawingBuffer !== undefined ? parameters.preserveDrawingBuffer : false; this._clearColor = parameters.clearColor !== undefined ? new Color(parameters.clearColor) : new Color(0x000000); this._clearAlpha = parameters.clearAlpha !== undefined ? parameters.clearAlpha : 0; this._outlineMaterial = new MeshOutlineMaterial(parameters.outline); this._outlineSphereImposterMaterial = new SphereImposterOutlineMaterial( parameters.outline ); this._outlineStickImposterMaterial = new StickImposterOutlineMaterial( parameters.outline ); this._outlineEnabled = !!parameters.outline; this._AOEnabled = !!parameters.ambientOcclusion; if (parameters.ambientOcclusion && typeof (parameters.ambientOcclusion.strength) !== 'undefined') { this._AOstrength = parseFloat(parameters.ambientOcclusion.strength); } if (this._AOstrength == 0) { this._AOEnabled = false; } if (parameters.ambientOcclusion && typeof (parameters.ambientOcclusion.radius) !== 'undefined') { this._AOradius = parseFloat(parameters.ambientOcclusion.radius); } this.domElement = this._canvas; this._canvas.id = parameters.id; if (parameters.containerWidth == 0 || parameters.containerHeight == 0) { return; //start lost } this.initGL(); this.setDefaultGLState(); this.context = this._gl; if (this.isWebGL1()) { this._extInstanced = this._gl.getExtension("ANGLE_instanced_arrays"); } else { //no longer an extension, wrap this._extInstanced = { vertexAttribDivisorANGLE: (this._gl as WebGL2RenderingContext).vertexAttribDivisor.bind(this._gl), drawElementsInstancedANGLE: (this._gl as WebGL2RenderingContext).drawElementsInstanced.bind(this._gl), }; } this._extFragDepth = this._gl.getExtension("EXT_frag_depth"); this._extFloatLinear = this._gl.getExtension("OES_texture_float_linear"); this._extColorBufferFloat = this._gl.getExtension("EXT_color_buffer_float"); this.sprites.init(this); } // API supportedExtensions() { return { supportsAIA: Boolean(this._extInstanced), supportsImposters: Boolean(this._extFragDepth) || !this.isWebGL1(), regen: false }; } getContext() { return this._gl; } getCanvas() { return this._canvas; } isLost() { return this._gl == null || this._gl.isContextLost(); } getPrecision() { return this._precision; } setClearColorHex(hex, alpha) { this._clearColor.setHex(hex); this._clearAlpha = alpha; if (!this.isLost()) { this._gl.clearColor(this._clearColor.r, this._clearColor.g, this._clearColor.b, this._clearAlpha); } } enableOutline(parameters) { this._outlineMaterial = new MeshOutlineMaterial(parameters); this._outlineSphereImposterMaterial = new SphereImposterOutlineMaterial( parameters ); this._outlineStickImposterMaterial = new StickImposterOutlineMaterial( parameters ); this._outlineEnabled = true; } disableOutline() { this._outlineEnabled = false; } enableAmbientOcclusion(parameters) { if (parameters) { if (parameters.strength) this._AOstrength = parameters.strength; if (parameters.scale) this._AOradius = parameters.scale; } this._AOEnabled = this._AOstrength > 0; } disableAmbientOcclusion() { this._AOEnabled = false; } setViewport() { if (this._offscreen) { //set viewport is called before every render, so setup offscreen size here this._offscreen.width = this._canvas.width; this._offscreen.height = this._canvas.height; } if ( this.rows != undefined && this.cols != undefined && this.row != undefined && this.col != undefined ) { //note that drawingBuffer may be smaller than the requested width //for large canvases var wid = this._gl.drawingBufferWidth / this.cols; var hei = this._gl.drawingBufferHeight / this.rows; this._viewportWidth = wid; this._viewportHeight = hei; if (!this.isLost()) { this._gl.enable(this._gl.SCISSOR_TEST); this._gl.scissor(wid * this.col, hei * this.row, wid, hei); this._gl.viewport(wid * this.col, hei * this.row, wid, hei); } } } setSize(width, height) { //zooming (in the browser) changes the pixel ratio and width/height this.devicePixelRatio = window.devicePixelRatio !== undefined ? window.devicePixelRatio : 1; //with antialiasing on, render at double rsolution to eliminate jaggies //don't do it with high resolution displays if (this._upscale && this.devicePixelRatio < 2.0) this.devicePixelRatio = 2.0; this._canvas.width = width * this.devicePixelRatio; this._canvas.height = height * this.devicePixelRatio; this._canvas.style.width = width + "px"; this._canvas.style.height = height + "px"; if ( this.rows != undefined && this.cols != undefined && this.row != undefined && this.col != undefined ) { var wid = width / this.cols; var hei = height / this.rows; this._viewportWidth = wid * this.devicePixelRatio; this._viewportHeight = hei * this.devicePixelRatio; this._viewportWidth = this._gl.drawingBufferWidth /this.cols; this._viewportHeight = this._gl.drawingBufferHeight/this.rows; this.setViewport(); } else { this._viewportWidth = this._canvas.width; this._viewportHeight = this._canvas.height; if (!this.isLost()) { this._gl.viewport(0, 0, this._gl.drawingBufferWidth, this._gl.drawingBufferHeight); } } this.initFrameBuffer(); } clear(color, depth, stencil) { var bits = 0; if (color === undefined || color) bits |= this._gl.COLOR_BUFFER_BIT; if (depth === undefined || depth) bits |= this._gl.DEPTH_BUFFER_BIT; if (stencil === undefined || stencil) bits |= this._gl.STENCIL_BUFFER_BIT; this._gl.clear(bits); } setMaterialFaces(material, reflected) { var doubleSided = material.side === DoubleSide; var flipSided = material.side === BackSide; if (!material.imposter) // Ignore reflection with imposters flipSided = reflected ? !flipSided : flipSided; if (this._oldDoubleSided !== doubleSided) { if (doubleSided) { this._gl.disable(this._gl.CULL_FACE); } else { this._gl.enable(this._gl.CULL_FACE); } this._oldDoubleSided = doubleSided; } if (this._oldFlipSided !== flipSided) { if (flipSided) { this._gl.frontFace(this._gl.CW); } else { this._gl.frontFace(this._gl.CCW); } this._oldFlipSided = flipSided; } this._gl.cullFace(this._gl.BACK); } setDepthTest(depthTest) { if (this._oldDepthTest !== depthTest) { if (depthTest) { this._gl.enable(this._gl.DEPTH_TEST); } else { this._gl.disable(this._gl.DEPTH_TEST); } this._oldDepthTest = depthTest; } } setDepthWrite(depthWrite) { if (this._oldDepthWrite !== depthWrite) { this._gl.depthMask(depthWrite); this._oldDepthWrite = depthWrite; } } setBlending(blending) { if (!blending) { this._gl.disable(this._gl.BLEND); } else { this._gl.enable(this._gl.BLEND); this._gl.blendEquationSeparate(this._gl.FUNC_ADD, this._gl.FUNC_ADD); this._gl.blendFuncSeparate( this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA, this._gl.ONE, this._gl.ONE_MINUS_SRC_ALPHA ); } } // TODO: need to set up shader attributes and uniforms as attributes on // material object after attaching prgm // We need to attach appropriate uniform variables to material after shaders // have been chosen initMaterial(material, lights, fog, objects) { material.addEventListener("dispose", this.onMaterialDispose.bind(this)); var parameters, shaderID; shaderID = material.shaderID; if (shaderID) { var shader = ShaderLib[shaderID]; material.vertexShader = shader.vertexShader; material.fragmentShader = shader.fragmentShader; material.uniforms = ShaderUtils.clone(shader.uniforms); // TODO: set material uniforms to shader uniform variables if (material.shaded) { material.makeShaded(this.SHADE_TEXTURE); } } parameters = { wireframe: material.wireframe, fragdepth: material.imposter, volumetric: material.volumetric, shaded: material.shaded }; material.program = this.buildProgram( material.fragmentShader, material.vertexShader, material.uniforms, parameters ); } renderBuffer(camera, lights, fog, material, geometryGroup, object) { if (!material.visible) return; var program, attributes; // Sets up proper vertex and fragment shaders and attaches them to webGL // program // Also sets appropriate uniform variables program = this.setProgram(camera, lights, fog, material, object, this); if (!program) return; attributes = program.attributes; var updateBuffers = false, wireframeBit = material.wireframe ? 1 : 0, geometryGroupHash = geometryGroup.id * 0xffffff + program.id * 2 + wireframeBit; if (geometryGroupHash !== this._currentGeometryGroupHash) { this._currentGeometryGroupHash = geometryGroupHash; updateBuffers = true; } // rebind shader attributes to appropriate (and already initialized) gl // buffers if (updateBuffers) { this.disableAttributes(); // Vertices if (attributes.position >= 0) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglVertexBuffer); this.enableAttribute(attributes.position); this._gl.vertexAttribPointer(attributes.position, 3, this._gl.FLOAT, false, 0, 0); } // Colors if (attributes.color >= 0) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglColorBuffer); this.enableAttribute(attributes.color); this._gl.vertexAttribPointer(attributes.color, 3, this._gl.FLOAT, false, 0, 0); } // Normals if (attributes.normal >= 0) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglNormalBuffer); this.enableAttribute(attributes.normal); this._gl.vertexAttribPointer(attributes.normal, 3, this._gl.FLOAT, false, 0, 0); } // Offsets (Instanced only) if (attributes.offset >= 0) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglOffsetBuffer); this.enableAttribute(attributes.offset); this._gl.vertexAttribPointer(attributes.offset, 3, this._gl.FLOAT, false, 0, 0); } // Radii (Instanced only) if (attributes.radius >= 0) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglRadiusBuffer); this.enableAttribute(attributes.radius); this._gl.vertexAttribPointer(attributes.radius, 1, this._gl.FLOAT, false, 0, 0); } } // Render var faceCount, lineCount; // lambert shaders - draw triangles // TODO: make sure geometryGroup's face count is setup correctly if (object instanceof Mesh) { if (material.shaderID === "instanced") { var sphereGeometryGroup = material.sphere.geometryGroups[0]; if (updateBuffers) { this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglVertexBuffer); this._gl.bufferData( this._gl.ARRAY_BUFFER, sphereGeometryGroup.vertexArray, this._gl.STATIC_DRAW ); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, geometryGroup.__webglNormalBuffer); this._gl.bufferData( this._gl.ARRAY_BUFFER, sphereGeometryGroup.normalArray, this._gl.STATIC_DRAW ); this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, geometryGroup.__webglFaceBuffer ); this._gl.bufferData( this._gl.ELEMENT_ARRAY_BUFFER, sphereGeometryGroup.faceArray, this._gl.STATIC_DRAW ); } faceCount = sphereGeometryGroup.faceidx; this._extInstanced.vertexAttribDivisorANGLE(attributes.offset, 1); this._extInstanced.vertexAttribDivisorANGLE(attributes.radius, 1); this._extInstanced.vertexAttribDivisorANGLE(attributes.color, 1); this._extInstanced.drawElementsInstancedANGLE( this._gl.TRIANGLES, faceCount, this._gl.UNSIGNED_SHORT, 0, geometryGroup.radiusArray.length ); this._extInstanced.vertexAttribDivisorANGLE(attributes.offset, 0); this._extInstanced.vertexAttribDivisorANGLE(attributes.radius, 0); this._extInstanced.vertexAttribDivisorANGLE(attributes.color, 0); } else if (material.wireframe) { lineCount = geometryGroup.lineidx; this.setLineWidth(material.wireframeLinewidth); if (updateBuffers) this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, geometryGroup.__webglLineBuffer ); this._gl.drawElements(this._gl.LINES, lineCount, this._gl.UNSIGNED_SHORT, 0); } else { faceCount = geometryGroup.faceidx; if (updateBuffers) this._gl.bindBuffer( this._gl.ELEMENT_ARRAY_BUFFER, geometryGroup.__webglFaceBuffer ); this._gl.drawElements(this._gl.TRIANGLES, faceCount, this._gl.UNSIGNED_SHORT, 0); } this.info.render.calls++; this.info.render.vertices += faceCount; this.info.render.faces += faceCount / 3; } // basic shaders - draw lines else if (object instanceof Line) { lineCount = geometryGroup.vertices; this.setLineWidth(material.linewidth); this._gl.drawArrays(this._gl.LINES, 0, lineCount); this.info.render.calls++; } } /* clear out the shading textures */ clearShading() { this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._shadingTexture, 0 ); this.clear(false, true, false); this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._depthTexture, 0 ); } /* Setup the shading buffer to reflect desired shading (ambient occlusion) values. Only the matching object with materialType are considered. */ setShading(scene, camera, materialType) { //identify all matching objects let lights = scene.__lights; let fog = scene.fog; let renderList = []; for (let i = 0, il = scene.__webglObjects.length; i < il; i++) { let webglObject = scene.__webglObjects[i]; if (webglObject.render && webglObject[materialType]) { renderList.push(webglObject); } } if (renderList.length == 0) return; //setup shading texture as depth buffer this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._shadingTexture, 0 ); this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.COLOR_ATTACHMENT0, this._gl.TEXTURE_2D, null, //don't write colors (can we do this?) 0 ); //calculate depth map this.renderObjects(scene.__webglObjects, true, materialType + "Depth", camera, lights, fog, false); //detach so we can read and attach scratch this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._scratchTexture, 0 ); this.clear(false, true, false); //perform AO calculation from depth map to scratch buffer // set screen shader and use it this._gl.useProgram(this._AOshader); this._currentProgram = this._AOshader; // disable depth test this.setDepthTest(-1); this.setDepthWrite(-1); let p_uniforms = this._AOshader.uniforms; this._gl.uniform1f(p_uniforms.total_strength, this._AOstrength); this._gl.uniform1f(p_uniforms.radius, this._AOradius); //setup full projection matrix from model to screen and inverted //use first object this._fullProjModelMatrix = new Matrix4(); this._fullProjModelMatrixInv = new Matrix4(); let object = renderList[0].object; this._fullProjModelMatrix.multiplyMatrices(camera.projectionMatrix, object._modelViewMatrix); this._fullProjModelMatrixInv.getInverse(this._fullProjModelMatrix); this._gl.uniformMatrix4fv( p_uniforms.projectionMatrix, false, this._fullProjModelMatrix.elements ); this._gl.uniformMatrix4fv(p_uniforms.projinv, false, this._fullProjModelMatrixInv.elements); // bind vertexarray buffer and texture this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._screenQuadVBO); this._gl.enableVertexAttribArray(this._aovertexattribpos); this._gl.vertexAttribPointer(this._aovertexattribpos, 2, this._gl.FLOAT, false, 0, 0); this._gl.activeTexture(this._gl.TEXTURE0); this._gl.bindTexture(this._gl.TEXTURE_2D, this._shadingTexture); // Draw 6 vertexes => 2 triangles: this._gl.drawArrays(this._gl.TRIANGLES, 0, 6); //perform blur from scratch to shading this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._shadingTexture, 0 ); this.clear(false, true, false); this._gl.useProgram(this._blurshader); this._currentProgram = this._blurshader; this.setDepthTest(-1); this.setDepthWrite(-1); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._screenQuadVBO); this._gl.enableVertexAttribArray(this._blurvertexattribpos); this._gl.vertexAttribPointer(this._blurvertexattribpos, 2, this._gl.FLOAT, false, 0, 0); this._gl.activeTexture(this._gl.TEXTURE0); this._gl.bindTexture(this._gl.TEXTURE_2D, this._scratchTexture); // Draw 6 vertexes => 2 triangles: this._gl.drawArrays(this._gl.TRIANGLES, 0, 6); //restore original depth, color this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.COLOR_ATTACHMENT0, this._gl.TEXTURE_2D, this._targetTexture, 0 ); this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._depthTexture, 0 ); } render(scene, camera) { if (camera instanceof Camera === false) { console.error("Renderer.render: camera is not an instance of Camera."); return; } var i, il, webglObject, object, renderList, lights = scene.__lights, fog = scene.fog; // reset caching for this frame this._currentMaterialId = -1; this._lightsNeedUpdate = true; // update scene graph if (this.autoUpdateScene) scene.updateMatrixWorld(); // update camera matrices // Pretty sure camera's parent is always going to be undefined for our // purposes... if (camera.parent === undefined) camera.updateMatrixWorld(); camera.matrixWorldInverse.getInverse(camera.matrixWorld); if (this.isLost()) { return; } // update WebGL objects if (this.autoUpdateObjects) this.initWebGLObjects(scene); this.info.render.calls = 0; this.info.render.vertices = 0; this.info.render.faces = 0; this.info.render.points = 0; this._currentWidth = this._viewportWidth; this._currentHeight = this._viewportHeight; this.setViewport(); this.setFrameBuffer(); this._gl.clearColor(this._clearColor.r, this._clearColor.g, this._clearColor.b, this._clearAlpha); this.clear(true, true, true); // set matrices for regular objects (frustum culled) renderList = scene.__webglObjects; let hasvolumetric = false; let hasAO = this._AOEnabled; for (i = 0, il = renderList.length; i < il; i++) { webglObject = renderList[i]; object = webglObject.object; webglObject.render = false; if (object.visible) { this.setupMatrices(object, camera); this.unrollBufferMaterial(webglObject); webglObject.render = true; if (webglObject.volumetric) hasvolumetric = true; if (webglObject.hasAO) hasAO = true; } } // set matrices for immediate objects // opaque pass (front-to-back order) this.setBlending(false); if (hasAO) { this.setShading(scene, camera, "opaque"); } this.renderObjects(scene.__webglObjects, true, "opaque", camera, lights, fog, false); if (hasAO) { this.clearShading(); } // Render embedded labels (sprites) this.renderSprites(scene, camera, false); // prime depth buffer with transparent objects this.renderObjects(scene.__webglObjects, true, "transparentDepth", camera, lights, fog, true); // transparent pass (back-to-front order) this.renderObjects(scene.__webglObjects, false, "transparent", camera, lights, fog, true); //volumetric is separate if (hasvolumetric && this._fb) { //disconnect depth texture from framebuffer so we can read it this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, null, 0 ); this.renderObjects(scene.__webglObjects, false, "volumetric", camera, lights, fog, true); } this.renderFrameBuffertoScreen(); this.setDepthTest(true); this.setDepthWrite(true); // Render floating labels (sprites) this.renderSprites(scene, camera, true); //if using offscreen render, copy final image if (this._bitmap) { const bitmap = this._offscreen.transferToImageBitmap(); this._bitmap.transferFromImageBitmap(bitmap); bitmap.close(); } } //setup framebuffer for drawing into, assumes buffers already allocated setFrameBuffer() { if (this.isWebGL1() || !this._fb) return; let width = this._viewportWidth; let height = this._viewportHeight; //when using framebuffer, always draw from origin, will shift the viewport when we render this._gl.enable(this._gl.SCISSOR_TEST); this._gl.scissor(0, 0, width, height); this._gl.viewport(0, 0, width, height); //color texture this._gl.bindTexture(this._gl.TEXTURE_2D, this._targetTexture); this._gl.texImage2D( this._gl.TEXTURE_2D, 0, this._gl.RGBA, width, height, 0, this._gl.RGBA, this._gl.UNSIGNED_BYTE, null ); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.LINEAR); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.LINEAR); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); //depth texture this._gl.bindTexture(this._gl.TEXTURE_2D, this._depthTexture); this._gl.texImage2D( this._gl.TEXTURE_2D, 0, (this._gl as WebGL2RenderingContext).DEPTH_COMPONENT32F, width, height, 0, this._gl.DEPTH_COMPONENT, this._gl.FLOAT, null ); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); //shading texture - for AO and maybe eventually shadows? I don't like shadows if (this._shadingTexture) { //for whatever reason, chrome seems to require this manual memory management //for these textures, at least in the auto tests webpage where many viewers are being created/destroyed this._gl.deleteTexture(this._shadingTexture); this._shadingTexture = this._gl.createTexture(); this._gl.bindTexture(this._gl.TEXTURE_2D, this._shadingTexture); this._gl.texImage2D( this._gl.TEXTURE_2D, 0, (this._gl as WebGL2RenderingContext).DEPTH_COMPONENT32F, width, height, 0, this._gl.DEPTH_COMPONENT, this._gl.FLOAT, null ); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); //scratch texture, needed by AO to do blur this._gl.deleteTexture(this._scratchTexture); this._scratchTexture = this._gl.createTexture(); this._gl.bindTexture(this._gl.TEXTURE_2D, this._scratchTexture); this._gl.texImage2D( this._gl.TEXTURE_2D, 0, (this._gl as WebGL2RenderingContext).DEPTH_COMPONENT32F, width, height, 0, this._gl.DEPTH_COMPONENT, this._gl.FLOAT, null ); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.NEAREST); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); } //bind fb this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, this._fb); this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.COLOR_ATTACHMENT0, this._gl.TEXTURE_2D, this._targetTexture, 0 ); this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._shadingTexture, 0 ); this._gl.clearDepth(1); this._gl.clear(this._gl.DEPTH_BUFFER_BIT); //shading all ones this._gl.framebufferTexture2D( this._gl.FRAMEBUFFER, this._gl.DEPTH_ATTACHMENT, this._gl.TEXTURE_2D, this._depthTexture, 0 ); } //allocate buffers for framebuffer, needs to be called with every resize initFrameBuffer() { // only needed/works with webgl2 if (this.isWebGL1()) return; let width = this._viewportWidth; let height = this._viewportHeight; //when using framebuffer, always draw from origin, will shift the viewport when we render this._gl.enable(this._gl.SCISSOR_TEST); this._gl.scissor(0, 0, width, height); this._gl.viewport(0, 0, width, height); //create textures and frame buffer, will be initialized in setFrameBuffer this._targetTexture = this._gl.createTexture(); this._depthTexture = this._gl.createTexture(); this._shadingTexture = this._gl.createTexture(); this._scratchTexture = this._gl.createTexture(); this._fb = this._gl.createFramebuffer(); // build screenshader var screenshader = this._antialias ? ShaderLib.screenaa : ShaderLib.screen; this._screenshader = this.buildProgram( screenshader.fragmentShader, screenshader.vertexShader, screenshader.uniforms, {} ); this._vertexattribpos = this._gl.getAttribLocation(this._screenshader, "vertexPosition"); // create the vertex array and attrib array for the full screenquad var verts = [ // First triangle: 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, // Second triangle: -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, ]; this._screenQuadVBO = this._gl.createBuffer(); this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._screenQuadVBO); this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array(verts), this._gl.STATIC_DRAW); // build aoshader let aoshader = ShaderLib.ssao; this._AOshader = this.buildProgram( aoshader.fragmentShader, aoshader.vertexShader, aoshader.uniforms, {} ); this._aovertexattribpos = this._gl.getAttribLocation(this._AOshader, "vertexPosition"); // create the vertex array and attrib array for the full screenquad //blur shader let bshader = ShaderLib.blur; this._blurshader = this.buildProgram( bshader.fragmentShader, bshader.vertexShader, bshader.uniforms, {} ); this._blurvertexattribpos = this._gl.getAttribLocation(this._blurshader, "vertexPosition"); } renderFrameBuffertoScreen() { // only needed/works with webgl2 if (this.isWebGL1() || this._fb === null) return; this.setViewport(); //draw texture in correct viewport // bind default framebuffer this._gl.bindFramebuffer(this._gl.FRAMEBUFFER, null); this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT); this._gl.frontFace(this._gl.CCW); this._gl.cullFace(this._gl.BACK); // set screen shader and use it this._gl.useProgram(this._screenshader); this._currentProgram = this._screenshader; // disable depth test this.setDepthTest(-1); this.setDepthWrite(-1); // bind vertexarray buffer and texture this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._screenQuadVBO); this._gl.enableVertexAttribArray(this._vertexattribpos); this._gl.vertexAttribPointer(this._vertexattribpos, 2, this._gl.FLOAT, false, 0, 0); this._gl.activeTexture(this._gl.TEXTURE0); this._gl.bindTexture(this._gl.TEXTURE_2D, this._targetTexture); // Draw 6 vertexes => 2 triangles: this._gl.drawArrays(this._gl.TRIANGLES, 0, 6); } initWebGLObjects(scene) { if (!scene.__webglObjects) { scene.__webglObjects = []; scene.__webglObjectsImmediate = []; scene.__webglSprites = []; scene.__webglFlares = []; } // Add objects; this sets up buffers for each geometryGroup if (scene.__objectsAdded.length) { while (scene.__objectsAdded.length) { this.addObject(scene.__objectsAdded[0], scene); scene.__objectsAdded.splice(0, 1); } // Force buffer update during render // Hackish fix for initial cartoon-render-then-transparent-surface // bug this._currentGeometryGroupHash = -1; } while (scene.__objectsRemoved.length) { this.removeObject(scene.__objectsRemoved[0], scene); scene.__objectsRemoved.splice(0, 1); } // update must be called after objects adding / removal // This sends typed arrays to GL buffers for each geometryGroup for (var o = 0, ol = scene.__webglObjects.length; o < ol; o++) { this.updateObject(scene.__webglObjects[o].object); } } getYRatio() { if (this.rows !== undefined && this.row !== undefined) return this.rows; return 1; } getXRatio() { if (this.cols !== undefined && this.col !== undefined) return this.cols; return 1; } getAspect(width, height) { if (width == undefined || height == undefined) { width = this._canvas.width; height = this._canvas.height; } var aspect = width / height; if ( this.rows != undefined && this.cols != undefined && this.row != undefined && this.col != undefined ) { var wid = width / this.cols; var hei = height / this.rows; aspect = wid / hei; } return aspect; } setTexture(texture, slot, is3D) { if (texture.needsUpdate) { if (!texture.__webglInit) { texture.__webglInit = true; texture.addEventListener("dispose", this.onTextureDispose.bind(this)); texture.__webglTexture = this._gl.createTexture(); this.info.memory.textures++; } this._gl.activeTexture(this._gl.TEXTURE0 + slot); var gltextureType = is3D ? (this._gl as WebGL2RenderingContext).TEXTURE_3D : this._gl.TEXTURE_2D; this._gl.bindTexture(gltextureType, texture.__webglTexture); this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, texture.flipY); this._gl.pixelStorei( this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha ); this._gl.pixelStorei(this._gl.UNPACK_ALIGNMENT, texture.unpackAlignment); this._gl.pixelStorei(this._gl.PACK_ALIGNMENT, texture.unpackAlignment); var glFormat = this.paramToGL(texture.format), glType = this.paramToGL(texture.type); if (!is3D) { var image = texture.image; var width = image.width; //might not be defined var height = image.height; if (typeof width === "undefined") { //if no width, width = image.length; if (glFormat == this._gl.RGBA) { width /= 4; //each element takes up 4 bytes } height = 1; } this.setTextureParameters(this._gl.TEXTURE_2D, texture); if (!this.isWebGL1()) { //webgl2 this._gl.texImage2D( this._gl.TEXTURE_2D, 0, glFormat, width, height, 0, glFormat, glType, texture.image ); } else { this._gl.texImage2D( this._gl.TEXTURE_2D, 0, glFormat, glFormat, glType, texture.image ); } } else { //3D this.setTextureParameters((this._gl as WebGL2RenderingContext).TEXTURE_3D, texture); (this._gl as WebGL2RenderingContext).texImage3D( (this._gl as WebGL2RenderingContext).TEXTURE_3D, 0, (this._gl as WebGL2RenderingContext).R32F, texture.image.size.z, texture.image.size.y, texture.image.size.x, 0, (this._gl as WebGL2RenderingContext).RED, this._gl.FLOAT, texture.image.data ); } texture.needsUpdate = false; if (texture.onUpdate) texture.onUpdate(); } else { this._gl.activeTexture(this._gl.TEXTURE0 + slot); if (is3D) this._gl.bindTexture((this._gl as WebGL2RenderingContext).TEXTURE_3D, texture.__webglTexture); else this._gl.bindTexture(this._gl.TEXTURE_2D, texture.__webglTexture); } } supportsVolumetric() { return !this.isWebGL1(); } private enableAttribute(attribute) { if (!this._enabledAttributes[attribute]) { this._gl.enableVertexAttribArray(attribute); this._enabledAttributes[attribute] = true; } } private disableAttributes() { for (let attribute in this._enabledAttributes) { if (this._enabledAttributes[attribute]) { this._gl.disableVertexAttribArray(attribute as any); this._enabledAttributes[attribute] = false; } } } private setPolygonOffset(polygonOffset, offsetFactor, offsetUnits) { if (this._oldPolygonOffset !== polygonOffset) { if (polygonOffset) this._gl.enable(this._gl.POLYGON_OFFSET_FILL); else this._gl.disable(this._gl.POLYGON_OFFSET_FILL); } } private setLineWidth(width: number) { if (width !== this._oldLineWidth) { this._gl.lineWidth(width); this._oldLineWidth = width; } } private deallocateGeometry(geometry) { geometry.__webglInit = undefined; if (geometry.__webglVertexBuffer !== undefined) this._gl.deleteBuffer(geometry.__webglVertexBuffer); if (geometry.__webglColorBuffer !== undefined) this._gl.deleteBuffer(geometry.__webglColorBuffer); if (geometry.geometryGroups !== undefined) { for (var g = 0, gl = geometry.groups; g < gl; g++) { var geometryGroup = geometry.geometryGroups[g]; if (geometryGroup.__webglVertexBuffer !== undefined) this._gl.deleteBuffer(geometryGroup.__webglVertexBuffer); if (geometryGroup.__webglColorBuffer !== undefined) this._gl.deleteBuffer(geometryGroup.__webglColorBuffer); if (geometryGroup.__webglNormalBuffer !== undefined) this._gl.deleteBuffer(geometryGroup.__webglNormalBuffer); if (geometryGroup.__webglFaceBuffer !== undefined) this._gl.deleteBuffer(geometryGroup.__webglFaceBuffer); if (geometryGroup.__webglLineBuffer !== undefined) this._gl.deleteBuffer(geometryGroup.__webglLineBuffer); } } } private deallocateMaterial(material) { var program = material.program; if (program === undefined) return; material.program = undefined; // only deallocate GL program if this was the last use of shared program // assumed there is only single copy of any program in the _programs // list // (that's how it's constructed) var i, il, programInfo; var deleteProgram = false; for (i = 0, il = this._programs.length; i < il; i++) { programInfo = this._programs[i]; if (programInfo.program === program) { programInfo.usedTimes--; if (programInfo.usedTimes === 0) { deleteProgram = true; } break; } } if (deleteProgram === true) { // avoid using array.splice, this is costlier than creating new // array from scratch var newPrograms = []; for (i = 0, il = this._programs.length; i < il; i++) { programInfo = this._programs[i]; if (programInfo.program !== program) { newPrograms.push(programInfo); } } this._programs = newPrograms; this._gl.deleteProgram(program); this.info.memory.programs--; } } private deallocateTexture(texture) { if (texture.image && texture.image.__webglTextureCube) { // cube texture this._gl.deleteTexture(texture.image.__webglTextureCube); } else { // 2D texture if (!texture.__webglInit) return; texture.__webglInit = false; this._gl.deleteTexture(texture.__webglTexture); } } private onGeometryDispose(event) { var geometry = event.target; geometry.removeEventListener("dispose", this.onGeometryDispose); this.deallocateGeometry(geometry); this.info.memory.geometries--; } private onTextureDispose(event) { var texture = event.target; texture.removeEventListener("dispose", this.onTextureDispose); this.deallocateTexture(texture); this.info.memory.textures--; } private onMaterialDispose(event) { var material = event.target; material.removeEventListener("dispose", this.onMaterialDispose); this.deallocateMaterial(material); } // Compile and return shader private getShader(type, str) { var shader; if (!this.isWebGL1() && !str.startsWith("#version")) { //convert webgl1 to webgl2, unless a version is already explicit str = str.replace(/gl_FragDepthEXT/g, "gl_FragDepth"); if (type == "fragment") { str = str.replace(/varying/g, "in"); } else { str = str.replace(/varying/g, "out"); } str = str.replace(/attribute/g, "in"); str = str.replace(/texture2D/g, "texture"); str = str.replace(/\/\/DEFINEFRAGCOLOR/g, "out vec4 glFragColor;"); str = str.replace(/gl_FragColor/g, "glFragColor"); str = "#version 300 es\n" + str; } if (type === "fragment") shader = this._gl.createShader(this._gl.FRAGMENT_SHADER); else if (type === "vertex") shader = this._gl.createShader(this._gl.VERTEX_SHADER); if (shader == null) return null; this._gl.shaderSource(shader, str); this._gl.compileShader(shader); if (!this._gl.getShaderParameter(shader, this._gl.COMPILE_STATUS)) { console.error(this._gl.getShaderInfoLog(shader)); console.error("could not initialize shader"); return null; } return shader; } // Compile appropriate shaders (if necessary) from source code and attach to // gl program. private buildProgram(fragmentShader, vertexShader, uniforms, parameters) { var p, pl, program, code; var chunks = []; chunks.push(fragmentShader); chunks.push(vertexShader); for (p in parameters) { chunks.push(p); chunks.push(parameters[p]); } code = chunks.join(); // check if program has already been compiled for (p = 0, pl = this._programs.length; p < pl; p++) { var programInfo = this._programs[p]; if (programInfo.code === code) { programInfo.usedTimes++; return programInfo.program; } } // check if program requires webgl2 if (this.isWebGL1()) { if (parameters.volumetric) throw new Error( "Volumetric rendering requires webgl2 which is not supported by your hardware." ); } // Set up new program and compile shaders program = this._gl.createProgram(); if (program == null) return null; // set up precision var precision = this._precision; var prefix = "precision " + precision + " float;"; var prefix_vertex = [ parameters.volumetric ? "#version 300 es" : "", prefix, ].join("\n"); var prefix_fragment = [ parameters.volumetric ? "#version 300 es" : "", parameters.fragdepth && this.isWebGL1() ? "#extension GL_EXT_frag_depth: enable" : "", parameters.shaded ? "#define SHADED 1" : "", parameters.wireframe ? "#define WIREFRAME 1" : "", prefix, ].join("\n"); var glFragmentShader = this.getShader( "fragment", prefix_fragment + fragmentShader ); var glVertexShader = this.getShader("vertex", prefix_vertex + vertexShader); if (glVertexShader != null) this._gl.attachShader(program, glVertexShader); if (glFragmentShader != null) this._gl.attachShader(program, glFragmentShader); this._gl.linkProgram(program); if (!this._gl.getProgramParameter(program, this._gl.LINK_STATUS)) console.error("Could not initialize shader"); // gather and cache uniform variables and attributes program.uniforms = {}; program.attributes = {}; var identifiers, u, i; // uniform vars identifiers = [ "viewMatrix", "modelViewMatrix", "projectionMatrix", "normalMatrix", "vWidth", "vHeight" ]; // custom uniform vars for (u in uniforms) identifiers.push(u); for (i = 0; i < identifiers.length; i++) { var uniformVar = identifiers[i]; program.uniforms[uniformVar] = this._gl.getUniformLocation( program, uniformVar ); } // attributes identifiers = [ "position", "normal", "color", "lineDistance", "offset", "radius", ]; /* * for (a in attributes) identifiers.push(a); */ for (i = 0; i < identifiers.length; i++) { var attributeVar = identifiers[i]; program.attributes[attributeVar] = this._gl.getAttribLocation( program, attributeVar ); } program.id = this._programs_counter++; this._programs.push({ program: program, code: code, usedTimes: 1, }); this.info.memory.programs = this._programs.length; return program; } private setProgram(camera, lights, fog, material, object, renderer) { if (material.needsUpdate) { if (material.program) this.deallocateMaterial(material); this.initMaterial(material, lights, fog, object); material.needsUpdate = false; } if (material.program == null) return null; var refreshMaterial = false; // p_uniforms: uniformVarName => uniformLocation // m_uniforms: uniformVarName => uniformJsVal var program = material.program, p_uniforms = program.uniforms, m_uniforms = material.uniforms; if (program != this._currentProgram) { this._gl.useProgram(program); this._currentProgram = program; refreshMaterial = true; } if (material.id != this._currentMaterialId) { this._currentMaterialId = material.id; refreshMaterial = true; } if (camera != this._currentCamera) { this._currentCamera = camera; refreshMaterial = true; } if (p_uniforms.projectionMatrix) { this._gl.uniformMatrix4fv( p_uniforms.projectionMatrix, false, camera.projectionMatrix.elements ); } if (p_uniforms.modelViewMatrix) { this._gl.uniformMatrix4fv( p_uniforms.modelViewMatrix, false, object._modelViewMatrix.elements ); } if (p_uniforms.normalMatrix) { this._gl.uniformMatrix3fv( p_uniforms.normalMatrix, false,