raymarch
Version:
Volume ray marching renderer
191 lines (168 loc) • 5.22 kB
JavaScript
"use strict"
var bits = require("bit-twiddle")
var createTexture = require("gl-texture2d")
var createShader = require("gl-shader")
var createVAO = require("gl-vao")
var createBuffer = require("gl-buffer")
var ndarray = require("ndarray")
var pool = require("typedarray-pool")
var ops = require("ndarray-ops")
var fs = require("fs")
var glm = require("gl-matrix")
var mat4 = glm.mat4
module.exports = createVolumeRenderer
var VERT_SRC = fs.readFileSync(__dirname + "/lib/shaders/raymarch.vs")
var FRAG_SRC = fs.readFileSync(__dirname + "/lib/shaders/raymarch.fs")
function VolumeRenderer(gl, vao, faceBuf, vertBuf, shader, texture, shape, zsplit, tshape) {
this._gl = gl
this._vao = vao
this._faceBuf = faceBuf
this._vertBuf = vertBuf
this._shader = shader
this._texture = texture
this._shape = shape
this._model = new Float32Array(16)
this._view = new Float32Array(16)
this._projection = new Float32Array(16)
this._worldToClip = new Float32Array(16)
this._zsplit = zsplit
this._tshape = tshape
for(var i=0; i<4; ++i) {
this._model[i+4*i] = this._view[i+4*i] = this._projection[i+4*i] = 1.0
}
}
var proto = VolumeRenderer.prototype
;(function(){
var props = ["model", "view", "projection"]
for(var i=0; i<props.length; ++i) {
var p = props[i]
Object.defineProperty(proto, p, {
get: new Function("return this._" + p)
, set: new Function("d", ["var m=this._",p,";for(var i=0;i<16;++i){m[i]=d[i]}return m"].join(""))
, enumerable: true
})
}
})()
proto.draw = function() {
var gl = this._gl
var shader = this._shader
//Save WebGL state
var depthMask = gl.getParameter(gl.DEPTH_WRITEMASK)
var blend = gl.getParameter(gl.BLEND)
var cullFace = gl.getParameter(gl.CULL_FACE)
var frontFace = gl.getParameter(gl.FRONT_FACE)
var cullFaceMode = gl.getParameter(gl.CULL_FACE_MODE)
//Set up WebGL state
gl.depthMask(false)
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
gl.frontFace(gl.CCW)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
//Calculate camera matrices
mat4.multiply(this._worldToClip, this._view, this._model)
mat4.multiply(this._worldToClip, this._projection, this._worldToClip)
//Set up shader
shader.bind()
shader.uniforms.worldToClip = this._worldToClip
shader.uniforms.clipToWorld = mat4.invert(this._worldToClip, this._worldToClip)
shader.uniforms.voxels = this._texture.bind(0)
shader.uniforms.shape = this._shape
shader.uniforms.zshape = this._zsplit
shader.uniforms.tshape = this._tshape
//Draw
this._vao.bind()
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0)
this._vao.unbind()
//Restore WebGL state
if(!blend) {
gl.disable(gl.BLEND)
}
gl.depthMask(depthMask)
if(!cullFace) {
gl.disable(gl.CULL_FACE)
}
gl.cullFace(cullFaceMode)
gl.frontFace(frontFace)
}
proto.dispose = function() {
this._vao.dispose()
this._faceBuf.dispose()
this._vertBuf.dispose()
this._shader.dispose()
this._texture.dispose()
}
function factorZ(x, y, z) {
var a = Math.ceil(Math.sqrt(x * z / y))|0
var b = Math.ceil(z / a)|0
return [a,b]
}
function createVolumeRenderer(gl, array) {
if(array.shape.length !== 3) {
throw new Error("Invalid array shape")
}
if(array.shape[0] === 0 || array.shape[1] === 0 || array.shape[2] === 0) {
throw new Error("Invalid array shape")
}
var maxTexSize = gl.getParameter(gl.MAX_TEXTURE_SIZE)
//Unpack texture into square array
var zsplit = factorZ(array.shape[2], array.shape[1], array.shape[0])
var gridW = bits.nextPow2(array.shape[2] * zsplit[0])
var gridH = bits.nextPow2(array.shape[1] * zsplit[1])
if(gridW > maxTexSize || gridH > maxTexSize) {
throw new Error("Insufficient texture memory for voxel model")
}
var data = pool.mallocUint8(gridW * gridH)
var s0 = ndarray(data,
[zsplit[1], array.shape[1], zsplit[0], array.shape[2]],
[gridW * array.shape[1], gridW, array.shape[2], 1 ],
0)
for(var i=0; i<array.shape[0]; ++i) {
var s = i % zsplit[0]
var t = (i/zsplit[0])|0
ops.assign(s0.pick(t, -1, s, -1), array.pick(i))
}
var s1 = ndarray(data, [gridH, gridW])
var texture = createTexture(gl, s1)
pool.free(data)
//Create buffers for cube
var cubeVerts = []
var cubeFacets = []
for(var i=0; i<8; ++i) {
for(var j=0; j<3; ++j) {
if(i & (1<<j)) {
cubeVerts.push( 1)
} else {
cubeVerts.push(-1)
}
}
}
for(var d=0; d<3; ++d) {
var u = 1<<((d + 1) % 3)
var v = 1<<((d + 2) % 3)
for(var s=0; s<2; ++s) {
var m = s << d
cubeFacets.push(m, m+v, m+u, m+u, m+v, m+u+v)
var t = u
u = v
v = t
}
}
//Create cube VAO
var faceBuf = createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeFacets))
var vertBuf = createBuffer(gl, new Float32Array(cubeVerts))
var vao = createVAO(gl, faceBuf, [
{ "buffer": vertBuf,
"type": gl.FLOAT,
"size": 3,
"stride": 0,
"offset": 0,
"normalized": false
}
])
//Create shader
var shader = createShader(gl, VERT_SRC, FRAG_SRC)
shader.attributes.position.location = 0
//Return the volume renderer object
return new VolumeRenderer(gl, vao, faceBuf, vertBuf, shader, texture, [array.shape[2], array.shape[1], array.shape[0]], [zsplit[1], zsplit[0]], [gridW, gridH])
}