UNPKG

pex-renderer

Version:

Physically Based Renderer for Pex

1,524 lines (1,398 loc) 52.6 kB
const path = require('path') const vec3 = require('pex-math/vec3') const vec4 = require('pex-math/vec4') const mat3 = require('pex-math/mat3') const mat4 = require('pex-math/mat4') const aabb = require('pex-geom/aabb') const createProfiler = require('./profiler') const isBrowser = require('is-browser') const createEntity = require('./entity') const createTransform = require('./transform') const createSkin = require('./skin') const createMorph = require('./morph') const createAnimation = require('./animation') const createGeometry = require('./geometry') const createMaterial = require('./material') const createCamera = require('./camera') const createPostProcessing = require('./post-processing') const createOrbiter = require('./orbiter') const createAmbientLight = require('./ambient-light') const createDirectionalLight = require('./directional-light') const createPointLight = require('./point-light') const createSpotLight = require('./spot-light') const createAreaLight = require('./area-light') const createReflectionProbe = require('./reflection-probe') const createSkybox = require('./skybox') const createOverlay = require('./overlay') const createBoundingBoxHelper = require('./helpers/bounding-box-helper') const createLightHelper = require('./helpers/light-helper') const createCameraHelper = require('./helpers/camera-helper') const createAxisHelper = require('./helpers/axis-helper') const createGridHelper = require('./helpers/grid-helper') const loadGltf = require('./loaders/glTF') const createGeomBuilder = require('geom-builder') const PBR_VERT = require('./shaders/pipeline/material.vert.js') const PBR_FRAG = require('./shaders/pipeline/material.frag.js') const DEPTH_PASS_VERT = require('./shaders/pipeline/depth-pass.vert.js') const DEPTH_PASS_FRAG = require('./shaders/pipeline/depth-pass.frag.js') const DEPTH_PRE_PASS_FRAG = require('./shaders/pipeline/depth-pre-pass.frag.js') const OVERLAY_VERT = require('./shaders/pipeline/overlay.vert.js') const OVERLAY_FRAG = require('./shaders/pipeline/overlay.frag.js') const HELPER_VERT = require('./shaders/pipeline/helper.vert.js') const HELPER_FRAG = require('./shaders/pipeline/helper.frag.js') const ERROR_VERT = require('./shaders/error/error.vert.js') const ERROR_FRAG = require('./shaders/error/error.frag.js') const SHADERS_CHUNKS = require('./shaders/chunks') const LOADERS = new Map().set(['.gltf', '.glb'], loadGltf) const LOADERS_ENTRIES = Array.from(LOADERS.entries()) var State = { frame: 0, shadowQuality: 2, debug: false, profile: false, profiler: null, paused: false, rgbm: false } function isNil(x) { return x == null } // TODO remove, should be in AABB function aabbToPoints(bbox) { if (aabb.isEmpty(bbox)) return [] return [ [bbox[0][0], bbox[0][1], bbox[0][2], 1], [bbox[1][0], bbox[0][1], bbox[0][2], 1], [bbox[1][0], bbox[0][1], bbox[1][2], 1], [bbox[0][0], bbox[0][1], bbox[1][2], 1], [bbox[0][0], bbox[1][1], bbox[0][2], 1], [bbox[1][0], bbox[1][1], bbox[0][2], 1], [bbox[1][0], bbox[1][1], bbox[1][2], 1], [bbox[0][0], bbox[1][1], bbox[1][2], 1] ] } // opts = Context // opts = { ctx: Context, width: Number, height: Number, profile: Boolean } function Renderer(opts) { this.entities = [] this.root = this.entity() // check if we passed gl context or options object opts = opts.texture2D ? { ctx: opts } : opts const ctx = (this._ctx = opts.ctx) const gl = opts.ctx.gl gl.getExtension('OES_standard_derivatives') this._dummyTexture2D = ctx.texture2D({ width: 4, height: 4 }) this._dummyTextureCube = ctx.textureCube({ width: 4, height: 4 }) this._defaultMaterial = createMaterial({ ctx: ctx, unlit: true, baseColor: [1, 0, 0, 1] }) this._debug = false this._programCacheMap = { values: [], getValue: function(flags, vert, frag) { for (var i = 0; i < this.values.length; i++) { var v = this.values[i] if (v.frag === frag && v.vert === vert) { if (v.flags.length === flags.length) { var found = true for (var j = 0; j < flags.length; j++) { if (v.flags[j] !== flags[j]) { found = false break } } if (found) { return v.program } } } } return false }, setValue: function(flags, vert, frag, program) { this.values.push({ flags, vert, frag, program }) } } if (opts.profile) { State.profiler = createProfiler(opts.ctx, this) State.profiler.flush = opts.profileFlush } if (opts.pauseOnBlur && isBrowser) { window.addEventListener('focus', () => { State.paused = false }) window.addEventListener('blur', () => { State.paused = true }) } // TODO: move from State object to internal probs and renderer({ opts }) setter? Object.assign(State, opts) this._state = State this.shaders = { chunks: SHADERS_CHUNKS, pipeline: { depthPrePass: { vert: DEPTH_PASS_VERT, frag: DEPTH_PRE_PASS_FRAG }, depthPass: { vert: DEPTH_PASS_VERT, frag: DEPTH_PASS_FRAG }, material: { vert: PBR_VERT, frag: PBR_FRAG } } } this.helperPositionVBuffer = ctx.vertexBuffer({ data: [0, 0, 0] }) this.helperColorVBuffer = ctx.vertexBuffer({ data: [0, 0, 0, 0] }) this.drawHelperLinesCmd = { pipeline: ctx.pipeline({ vert: ` ${HELPER_VERT} `, frag: ` ${HELPER_FRAG} `, depthTest: true, primitive: ctx.Primitive.Lines }), attributes: { aPosition: this.helperPositionVBuffer, aVertexColor: this.helperColorVBuffer }, count: 1 } this.drawHelperLinesPostProcCmd = { pipeline: ctx.pipeline({ vert: ` ${HELPER_VERT} `, frag: ` ${ctx.capabilities.maxColorAttachments > 1 ? '#define USE_DRAW_BUFFERS' : '' } ${HELPER_FRAG} `, depthTest: true, primitive: ctx.Primitive.Lines }), attributes: { aPosition: this.helperPositionVBuffer, aVertexColor: this.helperColorVBuffer }, count: 1 } } Renderer.prototype.updateDirectionalLightShadowMap = function( light, geometries ) { const ctx = this._ctx const position = light.entity.transform.worldPosition const target = [0, 0, 1, 0] const up = [0, 1, 0, 0] vec4.multMat4(target, light.entity.transform.modelMatrix) vec3.add(target, position) vec4.multMat4(up, light.entity.transform.modelMatrix) mat4.lookAt(light._viewMatrix, position, target, up) const shadowBboxPoints = geometries.reduce((points, geometry) => { return points.concat(aabbToPoints(geometry.entity.transform.worldBounds)) }, []) // TODO: gc vec3.copy, all the bounding box creation const bboxPointsInLightSpace = shadowBboxPoints.map((p) => vec3.multMat4(vec3.copy(p), light._viewMatrix) ) const sceneBboxInLightSpace = aabb.fromPoints(bboxPointsInLightSpace) const lightNear = -sceneBboxInLightSpace[1][2] const lightFar = -sceneBboxInLightSpace[0][2] light.set({ _near: lightNear, _far: lightFar }) mat4.ortho( light._projectionMatrix, sceneBboxInLightSpace[0][0], sceneBboxInLightSpace[1][0], sceneBboxInLightSpace[0][1], sceneBboxInLightSpace[1][1], lightNear, lightFar ) ctx.submit(light._shadowMapDrawCommand, () => { this.drawMeshes(null, true, light, geometries) }) } Renderer.prototype.updateSpotLightShadowMap = function(light, geometries) { const position = light.entity.transform.worldPosition const target = [0, 0, 1, 0] const up = [0, 1, 0, 0] vec4.multMat4(target, light.entity.transform.modelMatrix) // vec3.add(target, position) vec4.multMat4(up, light.entity.transform.modelMatrix) mat4.lookAt(light._viewMatrix, position, target, up) const shadowBboxPoints = geometries.reduce((points, geometry) => { return points.concat(aabbToPoints(geometry.entity.transform.worldBounds)) }, []) // TODO: gc vec3.copy, all the bounding box creation const bboxPointsInLightSpace = shadowBboxPoints.map((p) => vec3.multMat4(vec3.copy(p), light._viewMatrix) ) const sceneBboxInLightSpace = aabb.fromPoints(bboxPointsInLightSpace) const lightNear = -sceneBboxInLightSpace[1][2] const lightFar = -sceneBboxInLightSpace[0][2] light.set({ _near: lightNear, _far: lightFar }) mat4.perspective( light._projectionMatrix, 2 * light.angle, light._shadowMap.width / light._shadowMap.height, lightNear, lightFar ) const ctx = this._ctx ctx.submit(light._shadowMapDrawCommand, () => { this.drawMeshes(null, true, light, geometries) }) } Renderer.prototype.updatePointLightShadowMap = function(light, geometries) { const ctx = this._ctx light._sides.forEach((side) => { var target = [0, 0, 0] ctx.submit(side.drawPassCmd, () => { const position = light.entity.transform.worldPosition vec3.set(target, position) vec3.add(target, side.target) mat4.lookAt(side.viewMatrix, position, target, side.up) var sideLight = { _projectionMatrix: side.projectionMatrix, _viewMatrix: side.viewMatrix } this.drawMeshes(null, true, sideLight, geometries) }) }) } Renderer.prototype.parseShader = function(string, options) { // Unroll loop const unrollLoopPattern = /#pragma unroll_loop[\s]+?for \(int i = (\d+); i < (\d+|\D+); i\+\+\) \{([\s\S]+?)(?=\})\}/g string = string.replace(unrollLoopPattern, function( match, start, end, snippet ) { let unroll = '' // Replace lights number end = end .replace(/NUM_AMBIENT_LIGHTS/g, options.numAmbientLights || 0) .replace(/NUM_DIRECTIONAL_LIGHTS/g, options.numDirectionalLights || 0) .replace(/NUM_POINT_LIGHTS/g, options.numPointLights || 0) .replace(/NUM_SPOT_LIGHTS/g, options.numSpotLights || 0) .replace(/NUM_AREA_LIGHTS/g, options.numAreaLights || 0) for (let i = Number.parseInt(start); i < Number.parseInt(end); i++) { unroll += snippet.replace(/\[i\]/g, `[${i}]`) } return unroll }) return string } Renderer.prototype.getMaterialProgramAndFlags = function( geometry, material, skin, options ) { var ctx = this._ctx var flags = [] if (!geometry._attributes.aNormal) { flags.push('#define USE_UNLIT_WORKFLOW') } else { flags.push('#define USE_NORMALS') } if (geometry._attributes.aTangent) { flags.push('#define USE_TANGENTS') } if (geometry._attributes.aTexCoord0) { flags.push('#define USE_TEXCOORD_0') } if (geometry._attributes.aTexCoord1) { flags.push('#define USE_TEXCOORD_1') } if (geometry._attributes.aOffset) { flags.push('#define USE_INSTANCED_OFFSET') } if (geometry._attributes.aScale) { flags.push('#define USE_INSTANCED_SCALE') } if (geometry._attributes.aRotation) { flags.push('#define USE_INSTANCED_ROTATION') } if (geometry._attributes.aColor) { flags.push('#define USE_INSTANCED_COLOR') } if (geometry._attributes.aVertexColor) { flags.push('#define USE_VERTEX_COLORS') } if (options.useSSAO) { flags.push('#define USE_AO') } if (material.displacementMap) { flags.push('#define USE_DISPLACEMENT_MAP') } if (skin) { flags.push('#define USE_SKIN') flags.push('#define NUM_JOINTS ' + skin.joints.length) } if (ctx.capabilities.maxColorAttachments > 1) { flags.push('#define USE_DRAW_BUFFERS') } if (material.baseColorMap) { flags.push('#define USE_BASE_COLOR_MAP') if (!material.baseColor) { material.baseColor = [1, 1, 1, 1] } flags.push( `#define BASE_COLOR_MAP_TEX_COORD_INDEX ${material.baseColorMap .texCoord || 0}` ) if (material.baseColorMap.texCoordTransformMatrix) { flags.push('#define USE_BASE_COLOR_MAP_TEX_COORD_TRANSFORM') } } if (material.alphaMap) { flags.push('#define USE_ALPHA_MAP') flags.push( `#define ALPHA_MAP_TEX_COORD_INDEX ${material.alphaMap.texCoord || 0}` ) if (material.alphaMap.texCoordTransformMatrix) { flags.push('#define USE_ALPHA_MAP_TEX_COORD_TRANSFORM') } } if (material.alphaTest) { flags.push('#define USE_ALPHA_TEST') } if (options.depthPrePassOnly) { flags.push('#define DEPTH_PRE_PASS_ONLY') flags.push('#define SHADOW_QUALITY ' + 0) flags.push('#define NUM_AMBIENT_LIGHTS ' + 0) flags.push('#define NUM_DIRECTIONAL_LIGHTS ' + 0) flags.push('#define NUM_POINT_LIGHTS ' + 0) flags.push('#define NUM_SPOT_LIGHTS ' + 0) flags.push('#define NUM_AREA_LIGHTS ' + 0) return { flags: flags, vert: material.vert || DEPTH_PASS_VERT, frag: material.frag || DEPTH_PRE_PASS_FRAG } } if (options.depthPassOnly) { flags.push('#define DEPTH_PASS_ONLY') flags.push('#define SHADOW_QUALITY ' + 0) flags.push('#define NUM_AMBIENT_LIGHTS ' + 0) flags.push('#define NUM_DIRECTIONAL_LIGHTS ' + 0) flags.push('#define NUM_POINT_LIGHTS ' + 0) flags.push('#define NUM_SPOT_LIGHTS ' + 0) flags.push('#define NUM_AREA_LIGHTS ' + 0) return { flags: flags, vert: material.vert || DEPTH_PASS_VERT, frag: material.frag || DEPTH_PASS_FRAG } } flags.push( '#define SHADOW_QUALITY ' + (material.receiveShadows ? State.shadowQuality : 0) ) if (material.unlit) { if (flags.indexOf('#define USE_UNLIT_WORKFLOW') === -1) { flags.push('#define USE_UNLIT_WORKFLOW') } } else if (material.useSpecularGlossinessWorkflow) { flags.push('#define USE_SPECULAR_GLOSSINESS_WORKFLOW') if (material.diffuseMap) { flags.push('#define USE_DIFFUSE_MAP') flags.push( `#define DIFFUSE_MAP_TEX_COORD_INDEX ${material.diffuseMap.texCoord || 0}` ) if (material.diffuseMap.texCoordTransformMatrix) { flags.push('#define USE_DIFFUSE_MAP_TEX_COORD_TRANSFORM') } } if (material.specularGlossinessMap) { flags.push('#define USE_SPECULAR_GLOSSINESS_MAP') flags.push( `#define SPECULAR_GLOSSINESS_MAP_TEX_COORD_INDEX ${material .specularGlossinessMap.texCoord || 0}` ) if (material.specularGlossinessMap.texCoordTransformMatrix) { flags.push('#define USE_SPECULAR_GLOSSINESS_MAP_TEX_COORD_TRANSFORM') } } } else { flags.push('#define USE_METALLIC_ROUGHNESS_WORKFLOW') if (material.metallicMap) { flags.push('#define USE_METALLIC_MAP') flags.push( `#define METALLIC_MAP_TEX_COORD_INDEX ${material.metallicMap.texCoord || 0}` ) if (material.metallicMap.texCoordTransformMatrix) { flags.push('#define USE_METALLIC_MAP_TEX_COORD_TRANSFORM') } } if (material.roughnessMap) { flags.push('#define USE_ROUGHNESS_MAP') flags.push( `#define ROUGHNESS_MAP_TEX_COORD_INDEX ${material.roughnessMap .texCoord || 0}` ) if (material.roughnessMap.texCoordTransformMatrix) { flags.push('#define USE_ROUGHNESS_MAP_TEX_COORD_TRANSFORM') } } if (material.metallicRoughnessMap) { flags.push('#define USE_METALLIC_ROUGHNESS_MAP') flags.push( `#define METALLIC_ROUGHNESS_MAP_TEX_COORD_INDEX ${material .metallicRoughnessMap.texCoord || 0}` ) if (material.metallicRoughnessMap.texCoordTransformMatrix) { flags.push('#define USE_METALLIC_ROUGHNESS_MAP_TEX_COORD_TRANSFORM') } } } if (material.occlusionMap) { flags.push('#define USE_OCCLUSION_MAP') flags.push( `#define OCCLUSION_MAP_TEX_COORD_INDEX ${material.occlusionMap.texCoord || 0}` ) if (material.occlusionMap.texCoordTransformMatrix) { flags.push('#define USE_OCCLUSION_MAP_TEX_COORD_TRANSFORM') } } if (material.normalMap) { flags.push('#define USE_NORMAL_MAP') flags.push( `#define NORMAL_MAP_TEX_COORD_INDEX ${material.normalMap.texCoord || 0}` ) if (material.normalMap.texCoordTransformMatrix) { flags.push('#define USE_NORMAL_MAP_TEX_COORD_TRANSFORM') } } if (material.emissiveColorMap) { flags.push('#define USE_EMISSIVE_COLOR_MAP') flags.push( `#define EMISSIVE_COLOR_MAP_TEX_COORD_INDEX ${material.emissiveColorMap .texCoord || 0}` ) if (material.emissiveColorMap.texCoordTransformMatrix) { flags.push('#define USE_EMISSIVE_COLOR_MAP_TEX_COORD_TRANSFORM') } } if (!isNil(material.emissiveColor)) { flags.push('#define USE_EMISSIVE_COLOR') } if (!isNil(material.clearCoat)) { flags.push('#define USE_CLEAR_COAT') } if (material.clearCoatNormalMap) { flags.push('#define USE_CLEAR_COAT_NORMAL_MAP') flags.push( `#define CLEAR_COAT_NORMAL_MAP_TEX_COORD_INDEX ${material .clearCoatNormalMap.texCoord || 0}` ) if (material.clearCoatNormalMap.texCoordTransformMatrix) { flags.push('#define USE_CLEAR_COAT_NORMAL_MAP_TEX_COORD_TRANSFORM') } } if (material.blend) { flags.push('#define USE_BLEND') } flags.push('#define NUM_AMBIENT_LIGHTS ' + (options.numAmbientLights || 0)) flags.push( '#define NUM_DIRECTIONAL_LIGHTS ' + (options.numDirectionalLights || 0) ) flags.push('#define NUM_POINT_LIGHTS ' + (options.numPointLights || 0)) flags.push('#define NUM_SPOT_LIGHTS ' + (options.numSpotLights || 0)) flags.push('#define NUM_AREA_LIGHTS ' + (options.numAreaLights || 0)) if (options.useReflectionProbes) { flags.push('#define USE_REFLECTION_PROBES') } if (options.useTonemapping) { flags.push('#define USE_TONEMAPPING') } return { flags: flags, vert: material.vert || PBR_VERT, frag: material.frag || PBR_FRAG } } Renderer.prototype.buildProgram = function(vertSrc, fragSrc) { var ctx = this._ctx let program = null try { program = ctx.program({ vert: vertSrc, frag: fragSrc }) } catch (e) { console.error('pex-renderer glsl error', e) program = ctx.program({ vert: ERROR_VERT, frag: ERROR_FRAG }) throw e } return program } Renderer.prototype.getMaterialProgram = function( geometry, material, skin, options ) { var { flags, vert, frag } = this.getMaterialProgramAndFlags( geometry, material, skin, options ) var flagsStr = flags.join('\n') + '\n' var vertSrc = flagsStr + vert var fragSrc = flagsStr + frag var program = this._programCacheMap.getValue(flags, vert, frag) if (!program) { program = this.buildProgram( this.parseShader(vertSrc, options), this.parseShader(fragSrc, options) ) this._programCacheMap.setValue(flags, vert, frag, program) } return program } Renderer.prototype.traverseTransformTree = function( transform, beforeCallback, afterCallback ) { if (!transform.enabled) return beforeCallback(transform) transform.children.forEach((child) => { this.traverseTransformTree(child, beforeCallback, afterCallback) }) if (afterCallback) afterCallback(transform) } Renderer.prototype.update = function() { this.entities = [] this.traverseTransformTree( this.root.transform, (transform) => { this.entities.push(transform.entity) transform.entity.components.forEach((component) => { if (component.update) component.update() }) }, (transform) => { transform.entity.components.forEach((component) => { if (component.afterUpdate) component.afterUpdate() }) } ) } Renderer.prototype.getGeometryPipeline = function( geometry, material, skin, opts ) { const ctx = this._ctx const program = this.getMaterialProgram(geometry, material, skin, opts) if (!this._pipelineCache) { this._pipelineCache = {} } // TODO: better pipeline caching const hash = material.id + '_' + program.id + '_' + geometry.primitive let pipeline = this._pipelineCache[hash] if (!pipeline || material.needsPipelineUpdate) { material.needsPipelineUpdate = false pipeline = ctx.pipeline({ program: program, depthTest: material.depthTest, depthWrite: material.depthWrite, depthFunc: material.depthFunc, blend: material.blend, blendSrcRGBFactor: material.blendSrcRGBFactor, blendSrcAlphaFactor: material.blendSrcAlphaFactor, blendDstRGBFactor: material.blendDstRGBFactor, blendDstAlphaFactor: material.blendDstAlphaFactor, cullFace: material.cullFace, cullFaceMode: material.cullFaceMode, primitive: geometry.primitive }) this._pipelineCache[hash] = pipeline } return pipeline } Renderer.prototype.getOverlayCommand = function() { const ctx = this._ctx if (!this._drawOverlayCmd) { const program = ctx.program({ vert: OVERLAY_VERT, frag: OVERLAY_FRAG }) this._drawOverlayCmd = { name: 'DrawOverlayCmd', attributes: { aPosition: ctx.vertexBuffer([[-1, -1], [1, -1], [1, 1], [-1, 1]]), aTexCoord0: ctx.vertexBuffer([[0, 0], [1, 0], [1, 1], [0, 1]]) }, indices: ctx.indexBuffer([[0, 1, 2], [0, 2, 3]]), pipeline: ctx.pipeline({ program: program, depthTest: false, depthWrite: false, blend: true, blendSrcRGBFactor: ctx.BlendFactor.One, blendDstRGBFactor: ctx.BlendFactor.OneMinusSrcAlpha, blendSrcAlphaFactor: ctx.BlendFactor.One, blendDstAlphaFactor: ctx.BlendFactor.OneMinusSrcAlpha, cullFace: true, cullFaceMode: ctx.Face.Back, primitive: ctx.Primitive.Triangles }) } } return this._drawOverlayCmd } Renderer.prototype.getComponents = function(type) { const result = [] for (let i = 0; i < this.entities.length; i++) { const entity = this.entities[i] const component = entity.getComponent(type) if (component) { result.push(component) } } return result } // sort meshes by material // do material search by props not string concat // set global uniforms like lights once // set update transforms once per frame // draw + shadowmap @ 1000 objects x 30 uniforms = 60'000 setters / frame!! // transform feedback? Renderer.prototype.drawMeshes = function( camera, shadowMapping, shadowMappingLight, geometries, skybox, forward ) { const ctx = this._ctx function byEnabledAndCameraTags(component) { if (!component.enabled) return false if (!camera || !camera.entity) return true if (!camera.entity.tags.length) return true if (!component.entity.tags.length) return true return component.entity.tags[0] === camera.entity.tags[0] } geometries = geometries || this.getComponents('Geometry').filter(byEnabledAndCameraTags) const ambientLights = this.getComponents('AmbientLight').filter( byEnabledAndCameraTags ) const directionalLights = this.getComponents('DirectionalLight').filter( byEnabledAndCameraTags ) const pointLights = this.getComponents('PointLight').filter( byEnabledAndCameraTags ) const spotLights = this.getComponents('SpotLight').filter( byEnabledAndCameraTags ) const areaLights = this.getComponents('AreaLight').filter( byEnabledAndCameraTags ) const reflectionProbes = this.getComponents('ReflectionProbe').filter( byEnabledAndCameraTags ) if (!shadowMapping && !shadowMappingLight) { directionalLights.forEach((light) => { if (light.castShadows) { const shadowCasters = geometries.filter((geometry) => { const material = geometry.entity.getComponent('Material') return material && material.castShadows }) this.updateDirectionalLightShadowMap(light, shadowCasters) } }) pointLights.forEach((light) => { if (light.castShadows) { const shadowCasters = geometries.filter((geometry) => { const material = geometry.entity.getComponent('Material') return material && material.castShadows }) this.updatePointLightShadowMap(light, shadowCasters) } }) spotLights.forEach((light) => { if (light.castShadows) { const shadowCasters = geometries.filter((geometry) => { const material = geometry.entity.getComponent('Material') return material && material.castShadows }) this.updateSpotLightShadowMap(light, shadowCasters) } }) } var sharedUniforms = (this._sharedUniforms = this._sharedUniforms || {}) sharedUniforms.uOutputEncoding = State.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear // TODO: State.postprocess if (forward) { sharedUniforms.uOutputEncoding = ctx.Encoding.Gamma } // TODO: find nearest reflection probe if (reflectionProbes.length > 0) { sharedUniforms.uReflectionMap = reflectionProbes[0]._reflectionMap sharedUniforms.uReflectionMapEncoding = reflectionProbes[0]._reflectionMap.encoding } if (shadowMappingLight) { sharedUniforms.uProjectionMatrix = shadowMappingLight._projectionMatrix sharedUniforms.uViewMatrix = shadowMappingLight._viewMatrix } else { sharedUniforms.uCameraPosition = camera.entity.transform.worldPosition sharedUniforms.uProjectionMatrix = camera.projectionMatrix sharedUniforms.uViewMatrix = camera.viewMatrix sharedUniforms.uInverseViewMatrix = camera.inverseViewMatrix } if (camera) { const postProcessingCmp = camera.entity.getComponent('PostProcessing') if (postProcessingCmp && postProcessingCmp.ssao) { sharedUniforms.uAO = postProcessingCmp._frameAOTex sharedUniforms.uScreenSize = [camera.viewport[2], camera.viewport[3]] // TODO: should this be camera viewport size? } if (!(postProcessingCmp && postProcessingCmp.enabled)) { sharedUniforms.uExposure = camera.exposure } } ambientLights.forEach((light, i) => { sharedUniforms['uAmbientLights[' + i + '].color'] = light.color }) directionalLights.forEach((light, i) => { const dir4 = [0, 0, 1, 0] // TODO: GC const dir = [0, 0, 0] vec4.multMat4(dir4, light.entity.transform.modelMatrix) vec3.set(dir, dir4) sharedUniforms['uDirectionalLights[' + i + '].direction'] = dir sharedUniforms['uDirectionalLights[' + i + '].color'] = light.color sharedUniforms['uDirectionalLights[' + i + '].castShadows'] = light.castShadows sharedUniforms['uDirectionalLights[' + i + '].projectionMatrix'] = light._projectionMatrix sharedUniforms['uDirectionalLights[' + i + '].viewMatrix'] = light._viewMatrix sharedUniforms['uDirectionalLights[' + i + '].near'] = light._near sharedUniforms['uDirectionalLights[' + i + '].far'] = light._far sharedUniforms['uDirectionalLights[' + i + '].bias'] = light.bias sharedUniforms[ 'uDirectionalLights[' + i + '].shadowMapSize' ] = light.castShadows ? [light._shadowMap.width, light._shadowMap.height] : [0, 0] sharedUniforms['uDirectionalLightShadowMaps[' + i + ']'] = light.castShadows ? light._shadowMap : this._dummyTexture2D }) pointLights.forEach((light, i) => { sharedUniforms['uPointLights[' + i + '].position'] = light.entity.transform.worldPosition sharedUniforms['uPointLights[' + i + '].color'] = light.color sharedUniforms['uPointLights[' + i + '].range'] = light.range sharedUniforms['uPointLights[' + i + '].castShadows'] = light.castShadows sharedUniforms['uPointLightShadowMaps[' + i + ']'] = light.castShadows ? light._shadowCubemap : this._dummyTextureCube }) spotLights.forEach((light, i) => { const dir4 = [0, 0, 1, 0] // TODO: GC const dir = [0, 0, 0] vec4.multMat4(dir4, light.entity.transform.modelMatrix) vec3.set(dir, dir4) sharedUniforms['uSpotLights[' + i + '].position'] = light.entity.transform.position sharedUniforms['uSpotLights[' + i + '].direction'] = dir sharedUniforms['uSpotLights[' + i + '].color'] = light.color sharedUniforms['uSpotLights[' + i + '].angle'] = light.angle sharedUniforms['uSpotLights[' + i + '].innerAngle'] = light.innerAngle sharedUniforms['uSpotLights[' + i + '].range'] = light.range sharedUniforms['uSpotLights[' + i + '].castShadows'] = light.castShadows sharedUniforms['uSpotLights[' + i + '].projectionMatrix'] = light._projectionMatrix sharedUniforms['uSpotLights[' + i + '].viewMatrix'] = light._viewMatrix sharedUniforms['uSpotLights[' + i + '].near'] = light._near sharedUniforms['uSpotLights[' + i + '].far'] = light._far sharedUniforms['uSpotLights[' + i + '].bias'] = light.bias sharedUniforms['uSpotLights[' + i + '].shadowMapSize'] = light.castShadows ? [light._shadowMap.width, light._shadowMap.height] : [0, 0] sharedUniforms['uSpotLightShadowMaps[' + i + ']'] = light.castShadows ? light._shadowMap : this._dummyTexture2D }) areaLights.forEach((light, i) => { sharedUniforms.ltc_mat = light.ltc_mat_texture sharedUniforms.ltc_mag = light.ltc_mag_texture sharedUniforms['uAreaLights[' + i + '].position'] = light.entity.transform.position sharedUniforms['uAreaLights[' + i + '].color'] = light.color sharedUniforms['uAreaLights[' + i + '].intensity'] = light.intensity // FIXME: why area light has intensity and other lights don't? sharedUniforms['uAreaLights[' + i + '].rotation'] = light.entity.transform.rotation sharedUniforms['uAreaLights[' + i + '].size'] = [ light.entity.transform.scale[0] / 2, light.entity.transform.scale[1] / 2 ] }) geometries.sort((a, b) => { var matA = a.entity.getComponent('Material') || this._defaultMaterial var matB = b.entity.getComponent('Material') || this._defaultMaterial var transparentA = matA.blend ? 1 : 0 var transparentB = matB.blend ? 1 : 0 return transparentA - transparentB }) var firstTransparent = geometries.findIndex( (g) => (g.entity.getComponent('Material') || this._defaultMaterial).blend ) for (let i = 0; i < geometries.length; i++) { // also drawn below if transparent objects don't exist if (firstTransparent === i && skybox && skybox.enabled) { skybox.draw(camera, { outputEncoding: sharedUniforms.uOutputEncoding, backgroundMode: true }) } const geometry = geometries[i] const transform = geometry.entity.transform if (!transform.enabled) { continue } // don't draw uninitialized geometries if (!geometry._attributes.aPosition) { continue } const material = geometry.entity.getComponent('Material') || this._defaultMaterial if (!material.enabled || (material.blend && shadowMapping)) { continue } const skin = geometry.entity.getComponent('Skin') const cachedUniforms = material._uniforms if (material.baseColorMap) { cachedUniforms.uBaseColorMap = material.baseColorMap.texture || material.baseColorMap if (material.baseColorMap.texCoordTransformMatrix) { cachedUniforms.uBaseColorMapTexCoordTransform = material.baseColorMap.texCoordTransformMatrix } } cachedUniforms.uBaseColor = material.baseColor if (material.emissiveColorMap) { cachedUniforms.uEmissiveColorMap = material.emissiveColorMap.texture || material.emissiveColorMap if (material.emissiveColorMap.texCoordTransformMatrix) { cachedUniforms.uEmissiveColorMapTexCoordTransform = material.emissiveColorMap.texCoordTransformMatrix } } if (!isNil(material.emissiveColor)) { cachedUniforms.uEmissiveColor = material.emissiveColor cachedUniforms.uEmissiveIntensity = material.emissiveIntensity } if (material.useSpecularGlossinessWorkflow) { if (material.diffuse) cachedUniforms.uDiffuse = material.diffuse if (material.specular) cachedUniforms.uSpecular = material.specular if (!isNil(material.glossiness)) cachedUniforms.uGlossiness = material.glossiness if (material.diffuseMap) { cachedUniforms.uDiffuseMap = material.diffuseMap.texture || material.diffuseMap if (material.diffuseMap.texCoordTransformMatrix) { cachedUniforms.uDiffuseMapTexCoordTransform = material.diffuseMap.texCoordTransformMatrix } } if (material.specularGlossinessMap) { cachedUniforms.uSpecularGlossinessMap = material.specularGlossinessMap.texture || material.specularGlossinessMap if (material.specularGlossinessMap.texCoordTransformMatrix) { cachedUniforms.uSpecularGlossinessMapTexCoordTransform = material.specularGlossinessMap.texCoordTransformMatrix } } } else if (!material.unlit) { if (material.metallicMap) { cachedUniforms.uMetallicMap = material.metallicMap.texture || material.metallicMap if (material.metallicMap.texCoordTransformMatrix) { cachedUniforms.uMetallicMapTexCoordTransform = material.metallicMap.texCoordTransformMatrix } } if (!isNil(material.metallic)) cachedUniforms.uMetallic = material.metallic if (material.roughnessMap) { cachedUniforms.uRoughnessMap = material.roughnessMap.texture || material.roughnessMap if (material.roughnessMap.texCoordTransformMatrix) { cachedUniforms.uRoughnessMapTexCoordTransform = material.roughnessMap.texCoordTransformMatrix } } if (!isNil(material.roughness)) cachedUniforms.uRoughness = material.roughness if (material.metallicRoughnessMap) { cachedUniforms.uMetallicRoughnessMap = material.metallicRoughnessMap.texture || material.metallicRoughnessMap if (material.metallicRoughnessMap.texCoordTransformMatrix) { cachedUniforms.uMetallicRoughnessMapTexCoordTransform = material.metallicRoughnessMap.texCoordTransformMatrix } } } cachedUniforms.uReflectance = material.reflectance if (!isNil(material.clearCoat)) { cachedUniforms.uClearCoat = material.clearCoat cachedUniforms.uClearCoatRoughness = material.clearCoatRoughness || 0.04 } if (material.clearCoatNormalMap) { cachedUniforms.uClearCoatNormalMap = material.clearCoatNormalMap.texture || material.clearCoatNormalMap if (material.clearCoatNormalMap.texCoordTransformMatrix) { cachedUniforms.uClearCoatNormalMapTexCoordTransform = material.clearCoatNormalMap.texCoordTransformMatrix } cachedUniforms.uClearCoatNormalMapScale = material.clearCoatNormalMapScale } if (material.normalMap) { cachedUniforms.uNormalMap = material.normalMap.texture || material.normalMap cachedUniforms.uNormalScale = material.normalScale if (material.normalMap.texCoordTransformMatrix) { cachedUniforms.uNormalMapTexCoordTransform = material.normalMap.texCoordTransformMatrix } } if (material.occlusionMap) { cachedUniforms.uOcclusionMap = material.occlusionMap.texture || material.occlusionMap if (material.occlusionMap.texCoordTransformMatrix) { cachedUniforms.uOcclusionMapTexCoordTransform = material.occlusionMap.texCoordTransformMatrix } } if (material.displacementMap) { cachedUniforms.uDisplacementMap = material.displacementMap cachedUniforms.uDisplacement = material.displacement } if (material.alphaMap) { cachedUniforms.uAlphaMap = material.alphaMap.texture || material.alphaMap if (material.alphaMap.texCoordTransformMatrix) { cachedUniforms.uAlphaMapTexCoordTransform = material.alphaMap.texCoordTransformMatrix } } if (material.uniforms) { for (var uniformName in material.uniforms) { sharedUniforms[uniformName] = material.uniforms[uniformName] } } if (skin) { cachedUniforms.uJointMat = skin.jointMatrices } let pipeline = null if (shadowMapping && !shadowMappingLight) { pipeline = this.getGeometryPipeline(geometry, material, skin, { depthPrePassOnly: true }) } else if (shadowMapping) { pipeline = this.getGeometryPipeline(geometry, material, skin, { depthPassOnly: true }) } else { const postProcessingCmp = camera.entity.getComponent('PostProcessing') pipeline = this.getGeometryPipeline(geometry, material, skin, { numAmbientLights: ambientLights.length, numDirectionalLights: directionalLights.length, numPointLights: pointLights.length, numSpotLights: spotLights.length, numAreaLights: areaLights.length, useReflectionProbes: reflectionProbes.length, // TODO: reflection probes true useSSAO: postProcessingCmp && postProcessingCmp.enabled && postProcessingCmp.ssao, useTonemapping: !(postProcessingCmp && postProcessingCmp.enabled) }) } if (material.alphaTest !== undefined) { sharedUniforms.uAlphaTest = material.alphaTest } sharedUniforms.uPointSize = material.pointSize // TODO: shared uniforms HUH? // if (meshProgram !== prevProgram) { // prevProgram = meshProgram // // this is a bit hacky but prevents checking the same uniforms over and over again // // this would be even better if we sort meshes by material Object.assign(cachedUniforms, sharedUniforms) // } cachedUniforms.uModelMatrix = transform.modelMatrix // FIXME: this is expensive and not cached var viewMatrix if (shadowMappingLight) { viewMatrix = shadowMappingLight._viewMatrix } else { viewMatrix = camera.viewMatrix } var normalMat = mat4.copy(viewMatrix) mat4.mult(normalMat, transform.modelMatrix) mat4.invert(normalMat) mat4.transpose(normalMat) cachedUniforms.uNormalMatrix = mat3.fromMat4(mat3.create(), normalMat) ctx.submit({ name: 'drawGeometry', attributes: geometry._attributes, indices: geometry._indices, count: geometry.count, pipeline: pipeline, uniforms: cachedUniforms, instances: geometry.instances }) } // also drawn above if transparent objects exist if (firstTransparent === -1 && skybox && skybox.enabled) { skybox.draw(camera, { outputEncoding: sharedUniforms.uOutputEncoding, backgroundMode: true }) } } Renderer.prototype.drawHelpers = function(camera, postprocessing, ctx) { function byEnabledAndCameraTags(component) { if (!component.enabled) return false if (!camera || !camera.entity) return true if (!camera.entity.tags.length) return true if (!component.entity.tags.length) return true return component.entity.tags[0] === camera.entity.tags[0] } let draw = false let geomBuilder = createGeomBuilder({ colors: 1, positions: 1 }) // bounding box helper let thisHelpers = this.getComponents('BoundingBoxHelper').filter( byEnabledAndCameraTags ) thisHelpers.forEach((thisHelper) => { thisHelper.draw(geomBuilder) draw = true }) //light helper thisHelpers = this.getComponents('LightHelper').filter(byEnabledAndCameraTags) thisHelpers.forEach((thisHelper) => { thisHelper.draw(geomBuilder) draw = true }) //camera helper thisHelpers = this.getComponents('CameraHelper').filter( byEnabledAndCameraTags ) thisHelpers.forEach((thisHelper) => { thisHelper.draw(geomBuilder, camera) draw = true }) //grid helper thisHelpers = this.getComponents('GridHelper').filter(byEnabledAndCameraTags) thisHelpers.forEach((thisHelper) => { thisHelper.draw(geomBuilder, camera) draw = true }) //axis helper thisHelpers = this.getComponents('AxisHelper').filter(byEnabledAndCameraTags) thisHelpers.forEach((thisHelper) => { thisHelper.draw(geomBuilder) draw = true }) if (draw) { const outputEncoding = State.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear // TODO: State.postprocess ctx.update(this.helperPositionVBuffer, { data: geomBuilder.positions }) ctx.update(this.helperColorVBuffer, { data: geomBuilder.colors }) const cmd = postprocessing ? this.drawHelperLinesPostProcCmd : this.drawHelperLinesCmd cmd.count = geomBuilder.count ctx.submit(cmd, { uniforms: { uProjectionMatrix: camera.projectionMatrix, uViewMatrix: camera.viewMatrix, uOutputEncoding: outputEncoding }, viewport: camera.viewport }) } } Renderer.prototype.draw = function() { const ctx = this._ctx if (State.paused) return this.update() if (State.profiler) State.profiler.startFrame() var cameras = this.getComponents('Camera') var overlays = this.getComponents('Overlay') var skyboxes = this.getComponents('Skybox') var reflectionProbes = this.getComponents('ReflectionProbe') // TODO: update light probes /* if (!vec3.equals(State.prevSunPosition, State.sunPosition)) { vec3.set(State.prevSunPosition, State.sunPosition) // TODO: update sky only if it's used // TODO: implement if (State.skyEnvMap) { this._skybox.setEnvMap(State.skyEnvMap) } else { this._skyEnvMapTex.setSunPosition(State.sunPosition) this._skybox.setEnvMap(this._skyEnvMapTex.texture) } } */ reflectionProbes.forEach((probe) => { // TODO: this should be just node.reflectionProbe if (probe.dirty) { probe.update((camera, encoding) => { if (skyboxes.length > 0) { skyboxes[0].draw(camera, { outputEncoding: encoding, backgroundMode: false }) } }) } }) // draw scene cameras .filter((camera) => camera.enabled) .forEach((camera) => { const screenSize = [camera.viewport[2], camera.viewport[3]] const halfScreenSize = [ Math.floor(camera.viewport[2] / 2), Math.floor(camera.viewport[3] / 2) ] const halfViewport = [ 0, 0, Math.floor(camera.viewport[2] / 2), Math.floor(camera.viewport[3] / 2) ] const postProcessingCmp = camera.entity.getComponent('PostProcessing') if (postProcessingCmp && postProcessingCmp.enabled) { if (State.profiler) State.profiler.time('depthPrepass', true) ctx.submit(postProcessingCmp._drawFrameNormalsFboCommand, () => { const far = camera.far // TODO: Far clipping plane scaling fixes depth buffer precision artifacts // but breaks shadows on large scale scenes (eg maps) camera.set({ far: far * 0.99 }) this.drawMeshes(camera, true) camera.set({ far: far }) }) if (State.profiler) State.profiler.timeEnd('depthPrepass') } if ( postProcessingCmp && postProcessingCmp.enabled && postProcessingCmp.ssao ) { if (State.profiler) State.profiler.time('ssao', true) ctx.submit(postProcessingCmp._ssaoCmd, { uniforms: { uNear: camera.near, uFar: camera.far, uFov: camera.fov, viewMatrix: camera.viewMatrix, uInverseViewMatrix: mat4.invert(mat4.copy(camera.viewMatrix)), viewProjectionInverseMatrix: mat4.invert( mat4.mult(mat4.copy(camera.viewMatrix), camera.projectionMatrix) ), // TODO: GC cameraPositionWorldSpace: camera.entity.transform.worldPosition, uIntensity: postProcessingCmp.ssaoIntensity, uNoiseScale: [10, 10], uSampleRadiusWS: postProcessingCmp.ssaoRadius, uBias: postProcessingCmp.ssaoBias, uScreenSize: screenSize } }) if (State.profiler) State.profiler.timeEnd('ssao') if (State.profiler) State.profiler.time('ssao-blur', true) ctx.submit(postProcessingCmp._bilateralBlurHCmd, { uniforms: { near: camera.near, far: camera.far, sharpness: postProcessingCmp.ssaoBlurSharpness, imageSize: screenSize, depthMapSize: screenSize, direction: [postProcessingCmp.ssaoBlurRadius, 0] } }) ctx.submit(postProcessingCmp._bilateralBlurVCmd, { uniforms: { near: camera.near, far: camera.far, sharpness: postProcessingCmp.ssaoBlurSharpness, imageSize: screenSize, depthMapSize: screenSize, direction: [0, postProcessingCmp.ssaoBlurRadius] } }) if (State.profiler) State.profiler.timeEnd('ssao-blur') } if (State.profiler) State.profiler.time('drawFrame', true) if (postProcessingCmp && postProcessingCmp.enabled) { ctx.submit(postProcessingCmp._drawFrameFboCommand, () => { this.drawMeshes(camera, false, null, null, skyboxes[0], false) this.drawHelpers(camera, true, ctx) }) } else { ctx.submit({ viewport: camera.viewport }, () => { this.drawMeshes(camera, false, null, null, skyboxes[0], true) this.drawHelpers(camera, false, ctx) }) } if (State.profiler) State.profiler.timeEnd('drawFrame') if (State.profiler) State.profiler.time('postprocess') if ( postProcessingCmp && postProcessingCmp.enabled && postProcessingCmp.bloom ) { ctx.submit(postProcessingCmp._thresholdCmd, { uniforms: { uExposure: camera.exposure, uBloomThreshold: postProcessingCmp.bloomThreshold, imageSize: screenSize } }) for (let i = 0; i < postProcessingCmp._downSampleCmds.length; i++) { ctx.submit(postProcessingCmp._downSampleCmds[i], { uniforms: { intensity: postProcessingCmp.bloomRadius } }) } for (let i = 0; i < postProcessingCmp._bloomCmds.length; i++) { ctx.submit(postProcessingCmp._bloomCmds[i], { uniforms: { imageSize: screenSize } }) } } if ( postProcessingCmp && postProcessingCmp.enabled && postProcessingCmp.dof ) { if (State.profiler) State.profiler.time('dof', true) ctx.submit(postProcessingCmp._dofCmd, { uniforms: { uFar: camera.far, uNear: camera.near, imageSize: screenSize, depthMapSize: screenSize, uPixelSize: [1 / screenSize[0], 1 / screenSize[1]], uFocusDistance: postProcessingCmp.dofFocusDistance, uSensorHeight: camera.actualSensorHeight, uFocalLength: camera.focalLength, uFStop: camera.fStop, uDOFDebug: postProcessingCmp.dofDebug } }) if (State.profiler) State.profiler.timeEnd('dof') } if (postProcessingCmp && postProcessingCmp.enabled) { ctx.submit(postProcessingCmp._blitCmd, { uniforms: { uNear: camera.near, uFar: camera.far, uFov: camera.fov, uExposure: camera.exposure, uFXAA: postProcessingCmp.fxaa, uFog: postProcessingCmp.fog, uBloom: postProcessingCmp.bloom, uBloomIntensity: postProcessingCmp.bloomIntensity, uSunDispertion: postProcessingCmp.sunDispertion, uSunIntensity: postProcessingCmp.sunIntensity, uSunColor: postProcessingCmp.sunColor, uInscatteringCoeffs: postProcessingCmp.inscatteringCoeffs, uFogColor: postProcessingCmp.fogColor, uFogStart: postProcessingCmp.fogStart, uFogDensity: postProcessingCmp.fogDensity, uSunPosition: postProcessingCmp.sunPosition, uOutputEncoding: ctx.Encoding.Gamma, uOverlay: postProcessingCmp.dof ? postProcessingCmp._frameDofBlurTex : postProcessingCmp._frameColorTex, uScreenSize: screenSize }, viewport: camera.viewport }) if (State.profiler) State.profiler.timeEnd('postprocess') } }) overlays .filter((overlay) => overlay.enabled) .forEach((overlay) => { const bounds = [overlay.x, overlay.y, overlay.width, overlay.height] if ( overlay.x > 1 || overlay.y > 1 || overlay.width > 1 || overlay.height > 1 ) { bounds[0] /= ctx.gl.drawingBufferWidth bounds[1] /= ctx.gl.drawingBufferHeight bounds[2] /= ctx.gl.drawingBufferWidth bounds[3] /= ctx.gl.drawingBufferHeight } // overlay coordinates are from top left corner so we need to flip y bounds[1] = 1.0 - bounds[1] - bounds[3] ctx.submit(this.getOverlayCommand(), { uniforms: { uBounds: bounds, uTexture: overlay.texture } }) }) if (State.profiler) State.profiler.endFrame() } Renderer.prototype.entity = function(components, tags) { return createEntity(components, tags, this) } Renderer.prototype.add = function(entity, parent) { if (entity === this.root) { return entity } entity.transform.set({ parent: parent ? parent.transform : entity.transform.parent || this.root.transform }) return entity } Renderer.prototype.remove = function(entity) { entity.transform.set({ parent: null }) } Renderer.prototype.transform = function(opts) { return createTransform(Object.assign({ ctx: this._ctx }, opts)) } Renderer.prototype.skin = function(opts) { return createSkin(Object.assign({ ctx: this._ctx }, opts)) } Renderer.prototype.morph = function(opts) { return createMorph(Object.assign({ ctx: this._ctx }, opts)) } Renderer.prototype.animation = function(opts) { return createAnimation(Object.assign({ ctx: this._ctx }, opt