UNPKG

@smoud/tiny

Version:

Fast and tiny JavaScript library for HTML5 game and playable ads creation.

456 lines (386 loc) 15.5 kB
import { Vec3 } from '../math/Vec3.js'; import { Texture } from './Texture'; // TODO: Handle context loss https://www.khronos.org/webgl/wiki/HandlingContextLost // Not automatic - devs to use these methods manually // gl.colorMask( colorMask, colorMask, colorMask, colorMask ); // gl.clearColor( r, g, b, a ); // gl.stencilMask( stencilMask ); // gl.stencilFunc( stencilFunc, stencilRef, stencilMask ); // gl.stencilOp( stencilFail, stencilZFail, stencilZPass ); // gl.clearStencil( stencil ); var tempVec3 = new Vec3(); var ID = 1; function WebGLRenderer({ canvas = document.createElement('canvas'), width = 300, height = 150, dpr = 1, alpha = false, depth = true, stencil = true, antialias = false, premultipliedAlpha = false, preserveDrawingBuffer = true, powerPreference = 'default', autoClear = true, webgl = 2 } = {}) { var attributes = { alpha, depth, stencil, antialias, premultipliedAlpha, preserveDrawingBuffer, powerPreference }; this.dpr = dpr; this.alpha = alpha; this.color = true; this.depth = depth; this.stencil = stencil; this.premultipliedAlpha = premultipliedAlpha; this.autoClear = autoClear; this.id = ID++; this.domElement = canvas; // Attempt WebGL2 unless forced to 1, if not supported fallback to WebGL1 if (webgl === 2) this.gl = canvas.getContext('webgl2', attributes); this.isWebgl2 = !!this.gl; if (!this.gl) this.gl = canvas.getContext('webgl', attributes); if (!this.gl) console.error('unable to create webgl context'); // Attach renderer to gl so that all classes have access to internal state functions this.gl.renderer = this; // initialise size values this.resize(width, height); // gl state stores to avoid redundant calls on methods used internally this.state = {}; this.state.blendFunc = { src: this.gl.ONE, dst: this.gl.ZERO }; this.state.blendEquation = { modeRGB: this.gl.FUNC_ADD }; this.state.cullFace = null; this.state.frontFace = this.gl.CCW; this.state.depthMask = true; this.state.depthFunc = this.gl.LESS; this.state.premultiplyAlpha = false; this.state.flipY = false; this.state.unpackAlignment = 4; this.state.framebuffer = null; this.state.viewport = { x: 0, y: 0, width: null, height: null }; this.state.textureUnits = []; this.state.activeTextureUnit = 0; this.state.boundBuffer = null; this.state.uniformLocations = new Map(); this.state.currentProgram = null; // store requested extensions this.extensions = {}; // Initialise extra format types if (this.isWebgl2) { this.getExtension('EXT_color_buffer_float'); this.getExtension('OES_texture_float_linear'); } else { this.getExtension('OES_texture_float'); this.getExtension('OES_texture_float_linear'); this.getExtension('OES_texture_half_float'); this.getExtension('OES_texture_half_float_linear'); this.getExtension('OES_element_index_uint'); this.getExtension('OES_standard_derivatives'); this.getExtension('EXT_sRGB'); this.getExtension('WEBGL_depth_texture'); this.getExtension('WEBGL_draw_buffers'); } this.getExtension('WEBGL_compressed_texture_astc'); this.getExtension('EXT_texture_compression_bptc'); this.getExtension('WEBGL_compressed_texture_s3tc'); this.getExtension('WEBGL_compressed_texture_etc1'); this.getExtension('WEBGL_compressed_texture_pvrtc'); this.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'); // Create method aliases using extension (WebGL1) or native if available (WebGL2) this.vertexAttribDivisor = this.getExtension( 'ANGLE_instanced_arrays', 'vertexAttribDivisor', 'vertexAttribDivisorANGLE' ); this.drawArraysInstanced = this.getExtension( 'ANGLE_instanced_arrays', 'drawArraysInstanced', 'drawArraysInstancedANGLE' ); this.drawElementsInstanced = this.getExtension( 'ANGLE_instanced_arrays', 'drawElementsInstanced', 'drawElementsInstancedANGLE' ); this.createVertexArray = this.getExtension( 'OES_vertex_array_object', 'createVertexArray', 'createVertexArrayOES' ); this.bindVertexArray = this.getExtension( 'OES_vertex_array_object', 'bindVertexArray', 'bindVertexArrayOES' ); this.deleteVertexArray = this.getExtension( 'OES_vertex_array_object', 'deleteVertexArray', 'deleteVertexArrayOES' ); this.drawBuffers = this.getExtension('WEBGL_draw_buffers', 'drawBuffers', 'drawBuffersWEBGL'); // Store device parameters this.parameters = {}; this.parameters.maxTextureUnits = this.gl.getParameter(this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); this.parameters.maxAnisotropy = this.getExtension('EXT_texture_filter_anisotropic') ? this.gl.getParameter( this.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT ) : 0; //@TODO remove later, just for non mapped objects currently Texture.WHITE = new Texture(this.gl, { image: new Uint8Array([255, 255, 255, 255]), width: 1, height: 1 }); } WebGLRenderer.prototype = { constructor: WebGLRenderer, setClearColor: function (color, a) { this.gl.clearColor(color.r, color.g, color.b, a); }, setPixelRatio: function (value) { this.dpr = value; this.resize(this.width, this.height); }, resize: function (width, height) { this.width = width; this.height = height; this.gl.canvas.width = width * this.dpr; this.gl.canvas.height = height * this.dpr; if (!this.gl.canvas.style) return; Object.assign(this.gl.canvas.style, { width: width + 'px', height: height + 'px' }); }, setViewport: function (width, height, x = 0, y = 0) { if (this.state.viewport.width === width && this.state.viewport.height === height) return; this.state.viewport.width = width; this.state.viewport.height = height; this.state.viewport.x = x; this.state.viewport.y = y; this.gl.viewport(x, y, width, height); }, setScissor: function (width, height, x = 0, y = 0) { this.gl.scissor(x, y, width, height); }, enable: function (id) { if (this.state[id] === true) return; this.gl.enable(id); this.state[id] = true; }, disable: function (id) { if (this.state[id] === false) return; this.gl.disable(id); this.state[id] = false; }, setBlendFunc: function (src, dst, srcAlpha, dstAlpha) { if ( this.state.blendFunc.src === src && this.state.blendFunc.dst === dst && this.state.blendFunc.srcAlpha === srcAlpha && this.state.blendFunc.dstAlpha === dstAlpha ) return; this.state.blendFunc.src = src; this.state.blendFunc.dst = dst; this.state.blendFunc.srcAlpha = srcAlpha; this.state.blendFunc.dstAlpha = dstAlpha; if (srcAlpha !== undefined) this.gl.blendFuncSeparate(src, dst, srcAlpha, dstAlpha); else this.gl.blendFunc(src, dst); }, setBlendEquation: function (modeRGB, modeAlpha) { modeRGB = modeRGB || this.gl.FUNC_ADD; if (this.state.blendEquation.modeRGB === modeRGB && this.state.blendEquation.modeAlpha === modeAlpha) return; this.state.blendEquation.modeRGB = modeRGB; this.state.blendEquation.modeAlpha = modeAlpha; if (modeAlpha !== undefined) this.gl.blendEquationSeparate(modeRGB, modeAlpha); else this.gl.blendEquation(modeRGB); }, setCullFace: function (value) { if (this.state.cullFace === value) return; this.state.cullFace = value; this.gl.cullFace(value); }, setFrontFace: function (value) { if (this.state.frontFace === value) return; this.state.frontFace = value; this.gl.frontFace(value); }, setDepthMask: function (value) { if (this.state.depthMask === value) return; this.state.depthMask = value; this.gl.depthMask(value); }, setDepthFunc: function (value) { if (this.state.depthFunc === value) return; this.state.depthFunc = value; this.gl.depthFunc(value); }, activeTexture: function (value) { if (this.state.activeTextureUnit === value) return; this.state.activeTextureUnit = value; this.gl.activeTexture(this.gl.TEXTURE0 + value); }, bindFramebuffer: function ({ target = this.gl.FRAMEBUFFER, buffer = null } = {}) { if (this.state.framebuffer === buffer) return; this.state.framebuffer = buffer; this.gl.bindFramebuffer(target, buffer); }, getExtension: function (extension, webgl2Func, extFunc) { // if webgl2 function supported, return func bound to gl context if (webgl2Func && this.gl[webgl2Func]) return this.gl[webgl2Func].bind(this.gl); // fetch extension once only if (!this.extensions[extension]) { this.extensions[extension] = this.gl.getExtension(extension); } // return extension if no function requested if (!webgl2Func) return this.extensions[extension]; // Return null if extension not supported if (!this.extensions[extension]) return null; // return extension function, bound to extension return this.extensions[extension][extFunc].bind(this.extensions[extension]); }, // @TODO maybe not need sortOpaque: function (a, b) { if (a.renderOrder !== b.renderOrder) { return a.renderOrder - b.renderOrder; } else if (a.material.id !== b.material.id) { return a.material.id - b.material.id; } else if (a.zDepth !== b.zDepth) { return a.zDepth - b.zDepth; } else { return b.id - a.id; } }, sortTransparent: function (a, b) { if (a.renderOrder !== b.renderOrder) { return a.renderOrder - b.renderOrder; } if (a.zDepth !== b.zDepth) { return b.zDepth - a.zDepth; } else { return b.id - a.id; } }, // @TODO maybe not need sortUI: function (a, b) { if (a.renderOrder !== b.renderOrder) { return a.renderOrder - b.renderOrder; } else if (a.material.id !== b.material.id) { return a.material.id - b.material.id; } else { return b.id - a.id; } }, getRenderList: function ({ scene, camera, frustumCull, sort }) { var renderList = []; if (camera && frustumCull) camera.updateFrustum(); // Get visible scene.traverse((node) => { if (!node.visible) return true; if (!node.draw) return; if (frustumCull && node.frustumCulled && camera) { if (!camera.frustumIntersectsMesh(node)) return; } renderList.push(node); }); if (sort) { var opaque = []; var transparent = []; // depthTest true var ui = []; // depthTest false for (var i = 0; i < renderList.length; i++) { var node = renderList[i]; // Split into the 3 render groups if (!node.material.transparent) { opaque.push(node); } else if (node.material.depthTest) { transparent.push(node); } else { ui.push(node); } node.zDepth = 0; // Only calculate z-depth if renderOrder unset and depthTest is true if (node.renderOrder !== 0 || !node.material.depthTest || !camera) continue; tempVec3.set( node.worldMatrix.elements[12], node.worldMatrix.elements[13], node.worldMatrix.elements[14] ); // update z-depth // node.worldMatrix.getTranslation(tempVec3); tempVec3.applyMatrix4(camera.projectionViewMatrix); node.zDepth = tempVec3.z; } // @TODO really don't know if we need sort opaque // opaque.sort(this.sortOpaque); transparent.sort(this.sortTransparent); // @TODO don't know what ui means. I think should be deleted later // ui.sort(this.sortUI); renderList = opaque.concat(transparent, ui); } return renderList; }, render: function ({ scene, camera, directionalLight, ambientLight, target = null, update = true, sort = true, frustumCull = false, clear }) { if (target === null) { // make sure no render target bound so draws to canvas this.bindFramebuffer(); this.setViewport(this.width * this.dpr, this.height * this.dpr); } else { // bind supplied render target and update viewport this.bindFramebuffer(target); this.setViewport(target.width, target.height); } if (clear || (this.autoClear && clear !== false)) { // Ensure depth buffer writing is enabled so it can be cleared if (this.depth && (!target || target.depth)) { this.enable(this.gl.DEPTH_TEST); this.setDepthMask(true); } this.gl.clear( (this.color ? this.gl.COLOR_BUFFER_BIT : 0) | (this.depth ? this.gl.DEPTH_BUFFER_BIT : 0) | (this.stencil ? this.gl.STENCIL_BUFFER_BIT : 0) ); } // updates all scene graph matrices if (update) scene.updateTransform(); // Update camera separately, in case not in scene graph if (camera) camera.updateTransform(); // Get render list - entails culling and sorting var renderList = this.getRenderList({ scene, camera, frustumCull, sort }); for (var i = 0; i < renderList.length; i++) { var node = renderList[i]; if (!node.geometry.gl) { node.geometry.initialize(this.gl); } if (!node.material.gl) { node.material.initialize(this.gl); } node.draw({ camera, directionalLight, ambientLight }); } //@TODO maybe move to different place, or create dirty flags for math objects camera.projectMatrixDirty = false; } }; const StaticDrawUsage = 35044; const DynamicDrawUsage = 35048; export { WebGLRenderer, StaticDrawUsage, DynamicDrawUsage };