UNPKG

pex-renderer

Version:

Physically Based Renderer for Pex

468 lines (425 loc) 12.7 kB
const Signal = require('signals') const random = require('pex-random') const vec3 = require('pex-math/vec3') const mat4 = require('pex-math/mat4') const utils = require('pex-math/utils') const POSTPROCESS_VERT = require('./shaders/post-processing/post-processing.vert.js') const POSTPROCESS_FRAG = require('./shaders/post-processing/post-processing.frag.js') const SAO_FRAG = require('./shaders/post-processing/sao.frag.js') const BILATERAL_BLUR_FRAG = require('./shaders/post-processing/bilateral-blur.frag.js') const THRESHOLD_FRAG = require('./shaders/post-processing/threshold.frag.js') const BLOOM_FRAG = require('./shaders/post-processing/bloom.frag.js') const DOWN_SAMPLE_FRAG = require('./shaders/post-processing/down-sample.frag.js') const DOF_FRAG = require('./shaders/post-processing/dof.frag.js') const ssaoKernelData = new Float32Array(64 * 4) for (let i = 0; i < 64; i++) { var sample = [ random.float() * 2 - 1, random.float() * 2 - 1, random.float(), 1 ] vec3.normalize(sample) var scale = random.float() scale = utils.lerp(0.1, 1.0, scale * scale) vec3.scale(sample, scale) ssaoKernelData[i * 4 + 0] = sample[0] ssaoKernelData[i * 4 + 1] = sample[1] ssaoKernelData[i * 4 + 2] = sample[2] ssaoKernelData[i * 4 + 3] = sample[3] } const ssaoNoiseData = new Float32Array(128 * 128 * 4) for (let i = 0; i < 128 * 128; i++) { // let noiseSample = [ // random.float() * 2 - 1, // random.float() * 2 - 1, // 0, // 1 // ] ssaoNoiseData[i * 4 + 0] = sample[0] ssaoNoiseData[i * 4 + 1] = sample[1] ssaoNoiseData[i * 4 + 2] = sample[2] ssaoNoiseData[i * 4 + 3] = sample[3] } function PostProcessing(opts) { const gl = opts.ctx.gl this.type = 'PostProcessing' this.enabled = true this.changed = new Signal() this.entity = null this.rgbm = false this.depthPrepass = true this.backgroundColor = [0, 0, 0, 1] this.viewMatrix = mat4.create() this.viewport = [0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight] this.fxaa = false this.fog = false this.fogColor = [0.5, 0.5, 0.5] this.fogStart = 5 this.fogDensity = 0.15 this.inscatteringCoeffs = [0.3, 0.3, 0.3] this.ssao = false this.ssaoIntensity = 5 this.ssaoRadius = 12 this.ssaoBias = 0.01 this.ssaoBlurRadius = 2 this.ssaoBlurSharpness = 10 this.dof = false this.dofDebug = false this.dofFocusDistance = 5 this.dofAperture = 1 this.bloom = false this.bloomRadius = 1 this.bloomThreshold = 1 this.bloomIntensity = 0.1 this.sunPosition = [1, 1, 1] this.sunColor = [0.98, 0.98, 0.7] this.sunDispertion = 0.2 this.sunIntensity = 0.1 this._textures = [] this.set(opts) } PostProcessing.prototype.init = function(entity) { this.entity = entity const camera = this.entity && this.entity.getComponent('Camera') if (camera) { this.set({ viewport: camera.viewport, viewMatrix: camera.viewMatrix }) } if (this.enabled && this.ctx.capabilities.maxColorAttachments < 2) { this.enabled = false console.log( `pex-renderer disabling postprocess as MAX_COLOR_ATTACHMENTS=${ this.ctx.capabilities.maxColorAttachments }` ) console.log('pex-renderer ctx', this.ctx.capabilities) } if (this.enabled && !this._fsqMesh) { this.initPostproces() } } PostProcessing.prototype.set = function(opts) { Object.assign(this, opts) // Update textures if (opts.viewport) { this._textures.forEach((texture) => { const expectedWidth = Math.floor( opts.viewport[2] * (texture.sizeScale || 1) ) const expectedHeight = Math.floor( opts.viewport[3] * (texture.sizeScale || 1) ) if ( texture.width !== expectedWidth || texture.height !== expectedHeight ) { this.ctx.update(texture, { width: Math.max(expectedWidth, 1), height: Math.max(expectedHeight, 1) }) } }) } Object.keys(opts).forEach((prop) => this.changed.dispatch(prop)) } PostProcessing.prototype.initPostproces = function() { const ctx = this.ctx const fsqPositions = [[-1, -1], [1, -1], [1, 1], [-1, 1]] const fsqFaces = [[0, 1, 2], [0, 2, 3]] const W = this.viewport[2] const H = this.viewport[3] this._fsqMesh = { attributes: { aPosition: ctx.vertexBuffer(fsqPositions) }, indices: ctx.indexBuffer(fsqFaces) } // Init resizable textures this._frameColorTex = ctx.texture2D({ name: 'frameColorTex', width: W, height: H, pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F, encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear }) this._frameEmissiveTex = ctx.texture2D({ name: 'frameColorTex', width: W, height: H, pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F, encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear }) this._frameNormalTex = ctx.texture2D({ name: 'frameNormalTex', width: W, height: H, pixelFormat: ctx.PixelFormat.RGBA8, encoding: ctx.Encoding.Linear }) this._frameDepthTex = ctx.texture2D({ name: 'frameDepthTex', width: W, height: H, pixelFormat: ctx.PixelFormat.Depth24, encoding: ctx.Encoding.Linear }) this._frameAOTex = ctx.texture2D({ name: 'frameAOTex', width: W, height: H, pixelFormat: ctx.PixelFormat.RGBA8, encoding: ctx.Encoding.Linear }) this._frameAOBlurTex = ctx.texture2D({ name: 'frameAOBlurTex', width: W, height: H, pixelFormat: ctx.PixelFormat.RGBA8, encoding: ctx.Encoding.Linear }) this._frameDofBlurTex = ctx.texture2D({ name: 'frameDofBlurTex', width: W, height: H, pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F, encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear }) this._frameBloomTex = ctx.texture2D({ name: 'frameBloomHTex', width: W, height: H, pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F, encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear }) this._frameDownSampleTextures = Array.from({ length: 9 }, (v, k) => k).map( (i) => { const sampleSize = Math.pow(2, i + 1) const tex = ctx.texture2D({ width: Math.max(Math.floor(W / sampleSize), 1), height: Math.max(Math.floor(H / sampleSize), 1), pixelFormat: ctx.PixelFormat.RGBA16F, encoding: ctx.Encoding.Linear, min: ctx.Filter.Linear, mag: ctx.Filter.Linear }) tex.sizeScale = 1 / sampleSize return tex } ) this._textures = [ this._frameColorTex, this._frameEmissiveTex, this._frameNormalTex, this._frameDepthTex, this._frameAOTex, this._frameAOBlurTex, this._frameDofBlurTex, this._frameBloomTex, ...this._frameDownSampleTextures ] // Init fixed sizes textures ctx.gl.getExtension('OES_texture_float ') this._ssaoKernelMap = ctx.texture2D({ width: 8, height: 8, data: ssaoKernelData, pixelFormat: ctx.PixelFormat.RGBA32F, encoding: ctx.Encoding.Linear, wrap: ctx.Wrap.Repeat }) this._ssaoNoiseMap = ctx.texture2D({ width: 128, height: 128, data: ssaoNoiseData, pixelFormat: ctx.PixelFormat.RGBA32F, encoding: ctx.Encoding.Linear, wrap: ctx.Wrap.Repeat, mag: ctx.Filter.Linear, min: ctx.Filter.Linear }) // Init commands this._drawFrameNormalsFboCommand = { name: 'PostProcessing.drawFrameNormals', pass: ctx.pass({ name: 'PostProcessing.drawFrameNormals', color: [this._frameNormalTex], depth: this._frameDepthTex, clearColor: [0, 0, 0, 0], clearDepth: 1 }) } this._drawFrameFboCommand = { name: 'PostProcessing.drawFrame', pass: ctx.pass({ name: 'PostProcessing.drawFrame', color: [this._frameColorTex, this._frameEmissiveTex], depth: this._frameDepthTex, clearColor: this.backgroundColor }) } this._ssaoCmd = { name: 'PostProcessing.ssao', pass: ctx.pass({ name: 'PostProcessing.ssao', color: [this._frameAOTex], clearColor: [0, 0, 0, 1] // clearDepth: 1 }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: SAO_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { uDepthMap: this._frameDepthTex, uNormalMap: this._frameNormalTex, uNoiseMap: this._ssaoNoiseMap } } this._bilateralBlurHCmd = { name: 'PostProcessing.bilateralBlurH', pass: ctx.pass({ name: 'PostProcessing.bilateralBlurH', color: [this._frameAOBlurTex], clearColor: [1, 1, 0, 1] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: BILATERAL_BLUR_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { depthMap: this._frameDepthTex, image: this._frameAOTex, // direction: [State.bilateralBlurRadius, 0], // TODO: direction: [0.5, 0] } } this._bilateralBlurVCmd = { name: 'PostProcessing.bilateralBlurV', pass: ctx.pass({ name: 'PostProcessing.bilateralBlurV', color: [this._frameAOTex], clearColor: [1, 1, 0, 1] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: BILATERAL_BLUR_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { depthMap: this._frameDepthTex, image: this._frameAOBlurTex, // direction: [0, State.bilateralBlurRadius], // TODO: direction: [0, 0.5] } } this._dofCmd = { name: 'PostProcessing.dof', pass: ctx.pass({ name: 'PostProcessing.dof', color: [this._frameDofBlurTex], clearColor: [1, 1, 1, 1] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: DOF_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { depthMap: this._frameDepthTex, image: this._frameColorTex } } this._thresholdCmd = { name: 'PostProcessing.threshold', pass: ctx.pass({ name: 'PostProcessing.threshold', color: [this._frameBloomTex], clearColor: [1, 1, 1, 1] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: THRESHOLD_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { image: this._frameColorTex, emissiveTex: this._frameEmissiveTex, // TODO: this should be called screenSize as it's used to calculate uv imageSize: [this._frameBloomTex.width, this._frameBloomTex.height] } } this._downSampleCmds = this._frameDownSampleTextures.map((texture, i) => { const srcTexture = i === 0 ? this._frameBloomTex : this._frameDownSampleTextures[i - 1] return { name: `PostProcessing.downSample[${i}]`, pass: ctx.pass({ name: `PostProcessing.downSample[${i}]`, color: [texture] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: DOWN_SAMPLE_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { image: srcTexture, imageSize: [srcTexture.width, srcTexture.height], intensity: this.bloomRadius } } }) this._bloomCmds = this._frameDownSampleTextures.slice(1).map((texture, i) => { return { name: `PostProcessing.bloom[${i}]`, pass: ctx.pass({ name: `PostProcessing.bloom[${i}]`, color: [this._frameBloomTex] }), pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: BLOOM_FRAG, blend: true }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { image: texture, imageSize: [texture.width, texture.height] } } }) // this._overlayProgram = ctx.program({ vert: POSTPROCESS_VERT, frag: POSTPROCESS_FRAG }) // TODO this._blitCmd = { name: 'PostProcessing.blit', pipeline: ctx.pipeline({ vert: POSTPROCESS_VERT, frag: POSTPROCESS_FRAG }), attributes: this._fsqMesh.attributes, indices: this._fsqMesh.indices, uniforms: { uOverlay: this._frameColorTex, uOverlayEncoding: this._frameColorTex.encoding, uViewMatrix: this.viewMatrix, depthMap: this._frameDepthTex, depthMapSize: [W, H], uBloomMap: this._frameBloomTex, uEmissiveMap: this._frameEmissiveTex } } } module.exports = function createPostProcessing(opts) { return new PostProcessing(opts) }