pex-renderer
Version:
Physically Based Renderer for Pex
344 lines (308 loc) • 9.77 kB
JavaScript
const Signal = require('signals')
const mat4 = require('pex-math/mat4')
const hammersley = require('hammersley')
const FULLSCREEN_QUAD = require('./shaders/reflection-probe/fullscreen-quad.vert.js')
const CUBEMAP_TO_OCTMAP = require('./shaders/reflection-probe/cubemap-to-octmap.frag.js')
const CONVOLVE_OCT_MAP_ATLAS_TO_OCT_MAP = require('./shaders/reflection-probe/convolve-oct-map-atlas-to-oct-map.frag.js')
const BLIT_TO_OCT_MAP_ATLAS = require('./shaders/reflection-probe/blit-to-oct-map-atlas.frag.js')
const DOWNSAMPLE_FROM_OCT_MAP_ATLAS = require('./shaders/reflection-probe/downsample-from-oct-map-atlas.frag.js')
const PREFILTER_FROM_OCT_MAP_ATLAS = require('./shaders/reflection-probe/prefilter-from-oct-map-atlas.frag.js')
function ReflectionProbe(opts) {
this.type = 'ReflectionProbe'
this.enabled = true
this.changed = new Signal()
this.rgbm = false
this.set(opts)
const ctx = opts.ctx
this._ctx = ctx
this.dirty = true
const CUBEMAP_SIZE = 512
const dynamicCubemap = (this._dynamicCubemap = ctx.textureCube({
width: CUBEMAP_SIZE,
height: CUBEMAP_SIZE,
pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F,
encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear
}))
const sides = [
{ eye: [0, 0, 0], target: [1, 0, 0], up: [0, -1, 0] },
{ eye: [0, 0, 0], target: [-1, 0, 0], up: [0, -1, 0] },
{ eye: [0, 0, 0], target: [0, 1, 0], up: [0, 0, 1] },
{ eye: [0, 0, 0], target: [0, -1, 0], up: [0, 0, -1] },
{ eye: [0, 0, 0], target: [0, 0, 1], up: [0, -1, 0] },
{ eye: [0, 0, 0], target: [0, 0, -1], up: [0, -1, 0] }
].map((side, i) => {
side.projectionMatrix = mat4.perspective(
mat4.create(),
Math.PI / 2,
1,
0.1,
100
) // TODO: change this to radians
side.viewMatrix = mat4.lookAt(mat4.create(), side.eye, side.target, side.up)
side.drawPassCmd = {
name: 'ReflectionProbe.sidePass',
pass: ctx.pass({
name: 'ReflectionProbe.sidePass',
color: [
{
texture: dynamicCubemap,
target: ctx.gl.TEXTURE_CUBE_MAP_POSITIVE_X + i
}
],
clearColor: [0, 0, 0, 1],
clearDepth: 1
})
}
return side
})
const quadPositions = [[-1, -1], [1, -1], [1, 1], [-1, 1]]
const quadTexCoords = [[0, 0], [1, 0], [1, 1], [0, 1]]
const quadFaces = [[0, 1, 2], [0, 2, 3]]
const attributes = {
aPosition: ctx.vertexBuffer(quadPositions),
aTexCoord: ctx.vertexBuffer(quadTexCoords)
}
const indices = ctx.indexBuffer(quadFaces)
const octMap = (this._octMap = ctx.texture2D({
width: 1024,
height: 1024,
pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F,
encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear
}))
const irradianceOctMapSize = 64
const octMapAtlas = (this._reflectionMap = ctx.texture2D({
width: 2 * 1024,
height: 2 * 1024,
min: ctx.Filter.Linear,
mag: ctx.Filter.Linear,
pixelFormat: this.rgbm ? ctx.PixelFormat.RGBA8 : ctx.PixelFormat.RGBA16F,
encoding: this.rgbm ? ctx.Encoding.RGBM : ctx.Encoding.Linear
}))
const cubemapToOctMap = {
name: 'ReflectionProbe.cubemapToOctMap',
pass: ctx.pass({
name: 'ReflectionProbe.cubemapToOctMap',
color: [octMap]
}),
pipeline: ctx.pipeline({
vert: FULLSCREEN_QUAD,
frag: CUBEMAP_TO_OCTMAP
}),
attributes: attributes,
indices: indices,
uniforms: {
uTextureSize: octMap.width,
uCubemap: dynamicCubemap
}
}
const convolveOctmapAtlasToOctMap = {
name: 'ReflectionProbe.convolveOctmapAtlasToOctMap',
pass: ctx.pass({
name: 'ReflectionProbe.convolveOctmapAtlasToOctMap',
color: [octMap]
}),
pipeline: ctx.pipeline({
vert: FULLSCREEN_QUAD,
frag: CONVOLVE_OCT_MAP_ATLAS_TO_OCT_MAP
}),
attributes: attributes,
indices: indices,
uniforms: {
uTextureSize: irradianceOctMapSize,
uSource: octMapAtlas,
uSourceSize: octMapAtlas.width,
uSourceEncoding: octMapAtlas.encoding,
uOutputEncoding: octMap.encoding
}
}
const clearOctMapAtlasCmd = {
name: 'ReflectionProbe.clearOctMapAtlas',
pass: ctx.pass({
name: 'ReflectionProbe.clearOctMapAtlas',
color: [octMapAtlas],
clearColor: [0, 0, 0, 0]
})
}
const blitToOctMapAtlasCmd = {
name: 'ReflectionProbe.blitToOctMapAtlasCmd',
pass: ctx.pass({
name: 'ReflectionProbe.blitToOctMapAtlasCmd',
color: [octMapAtlas]
}),
pipeline: ctx.pipeline({
vert: FULLSCREEN_QUAD,
frag: BLIT_TO_OCT_MAP_ATLAS
}),
uniforms: {
uSource: octMap,
uSourceSize: octMap.width
},
attributes: attributes,
indices: indices
}
const downsampleFromOctMapAtlasCmd = {
name: 'ReflectionProbe.downsampleFromOctMapAtlasCmd',
pass: ctx.pass({
name: 'ReflectionProbe.downsampleFromOctMapAtlasCmd',
color: [octMap],
clearColor: [0, 1, 0, 1]
}),
pipeline: ctx.pipeline({
vert: FULLSCREEN_QUAD,
frag: DOWNSAMPLE_FROM_OCT_MAP_ATLAS
}),
uniforms: {
uSource: octMapAtlas,
uSourceSize: octMapAtlas.width
},
attributes: attributes,
indices: indices
}
const prefilterFromOctMapAtlasCmd = {
name: 'ReflectionProbe.prefilterFromOctMapAtlasCmd',
pass: ctx.pass({
name: 'ReflectionProbe.prefilterFromOctMapAtlasCmd',
color: [octMap],
clearColor: [0, 1, 0, 1]
}),
pipeline: ctx.pipeline({
vert: FULLSCREEN_QUAD,
frag: PREFILTER_FROM_OCT_MAP_ATLAS
}),
uniforms: {
uSource: octMapAtlas,
uSourceSize: octMapAtlas.width,
uSourceEncoding: octMapAtlas.encoding,
uOutputEncoding: octMap.encoding
},
attributes: attributes,
indices: indices
}
const numSamples = 128
const hammersleyPointSet = new Float32Array(4 * numSamples)
for (let i = 0; i < numSamples; i++) {
const p = hammersley(i, numSamples)
hammersleyPointSet[i * 4] = p[0]
hammersleyPointSet[i * 4 + 1] = p[1]
hammersleyPointSet[i * 4 + 2] = 0
hammersleyPointSet[i * 4 + 3] = 0
}
const hammersleyPointSetMap = ctx.texture2D({
data: hammersleyPointSet,
width: 1,
height: numSamples,
pixelFormat: ctx.PixelFormat.RGBA32F,
encoding: ctx.Encoding.Linear
})
function blitToOctMapAtlasLevel(
mipmapLevel,
roughnessLevel,
sourceRegionSize
) {
const width = octMapAtlas.width
const levelSize = Math.max(
64,
width / (2 << (mipmapLevel + roughnessLevel))
)
const roughnessLevelWidth = width / (2 << roughnessLevel)
const vOffset = width - Math.pow(2, Math.log2(width) - roughnessLevel)
const hOffset =
2 * roughnessLevelWidth -
Math.pow(2, Math.log2(2 * roughnessLevelWidth) - mipmapLevel)
ctx.submit(blitToOctMapAtlasCmd, {
viewport: [hOffset, vOffset, levelSize, levelSize],
uniforms: {
uLevelSize: levelSize,
uSourceRegionSize: sourceRegionSize
}
})
}
function downsampleFromOctMapAtlasLevel(
mipmapLevel,
roughnessLevel,
targetRegionSize
) {
ctx.submit(downsampleFromOctMapAtlasCmd, {
viewport: [0, 0, targetRegionSize, targetRegionSize],
uniforms: {
uMipmapLevel: mipmapLevel,
uRoughnessLevel: roughnessLevel
}
})
}
function prefilterFromOctMapAtlasLevel(
sourceMipmapLevel,
sourceRoughnessLevel,
roughnessLevel,
targetRegionSize
) {
ctx.submit(prefilterFromOctMapAtlasCmd, {
viewport: [0, 0, targetRegionSize, targetRegionSize],
uniforms: {
uSourceMipmapLevel: sourceMipmapLevel,
uSourceRoughnessLevel: sourceRoughnessLevel,
uRoughnessLevel: roughnessLevel,
uNumSamples: numSamples,
uHammersleyPointSetMap: hammersleyPointSetMap
}
})
}
this.update = function(drawScene) {
if (!drawScene) return
this.dirty = false
sides.forEach((side) => {
ctx.submit(side.drawPassCmd, () =>
drawScene(side, dynamicCubemap.encoding)
)
})
ctx.submit(cubemapToOctMap)
ctx.submit(clearOctMapAtlasCmd)
// mipmap levels go horizontally
// roughness levels go vertically
const maxLevel = 5
blitToOctMapAtlasLevel(0, 0, octMap.width)
for (let i = 0; i < maxLevel - 1; i++) {
downsampleFromOctMapAtlasLevel(i, 0, octMap.width / Math.pow(2, i + 1))
blitToOctMapAtlasLevel(i + 1, 0, octMap.width / Math.pow(2, 1 + i))
}
blitToOctMapAtlasLevel(maxLevel, 0, 64)
for (let i = 1; i <= maxLevel; i++) {
// prefilterFromOctMapAtlasLevel(i, 0, i, Math.max(64, octMap.width / Math.pow(2, i + 1)))
prefilterFromOctMapAtlasLevel(
0,
Math.max(0, i - 1),
i,
Math.max(64, octMap.width / Math.pow(2, i + 1))
)
blitToOctMapAtlasLevel(
0,
i,
Math.max(64, octMap.width / Math.pow(2, 1 + i))
)
}
ctx.submit(convolveOctmapAtlasToOctMap, {
viewport: [0, 0, irradianceOctMapSize, irradianceOctMapSize]
})
ctx.submit(blitToOctMapAtlasCmd, {
viewport: [
octMapAtlas.width - irradianceOctMapSize,
octMapAtlas.height - irradianceOctMapSize,
irradianceOctMapSize,
irradianceOctMapSize
],
uniforms: {
uSourceRegionSize: irradianceOctMapSize
}
})
}
}
ReflectionProbe.prototype.init = function(entity) {
this.entity = entity
}
ReflectionProbe.prototype.set = function(opts) {
Object.assign(this, opts)
Object.keys(opts).forEach((prop) => this.changed.dispatch(prop))
}
module.exports = function(opts) {
return new ReflectionProbe(opts)
}