623 lines
15 KiB
JavaScript
623 lines
15 KiB
JavaScript
var check = require('./lib/util/check')
|
|
var extend = require('./lib/util/extend')
|
|
var dynamic = require('./lib/dynamic')
|
|
var raf = require('./lib/util/raf')
|
|
var clock = require('./lib/util/clock')
|
|
var createStringStore = require('./lib/strings')
|
|
var initWebGL = require('./lib/webgl')
|
|
var wrapExtensions = require('./lib/extension')
|
|
var wrapLimits = require('./lib/limits')
|
|
var wrapBuffers = require('./lib/buffer')
|
|
var wrapElements = require('./lib/elements')
|
|
var wrapTextures = require('./lib/texture')
|
|
var wrapRenderbuffers = require('./lib/renderbuffer')
|
|
var wrapFramebuffers = require('./lib/framebuffer')
|
|
var wrapAttributes = require('./lib/attribute')
|
|
var wrapShaders = require('./lib/shader')
|
|
var wrapRead = require('./lib/read')
|
|
var createCore = require('./lib/core')
|
|
var createStats = require('./lib/stats')
|
|
var createTimer = require('./lib/timer')
|
|
|
|
var GL_COLOR_BUFFER_BIT = 16384
|
|
var GL_DEPTH_BUFFER_BIT = 256
|
|
var GL_STENCIL_BUFFER_BIT = 1024
|
|
|
|
var GL_ARRAY_BUFFER = 34962
|
|
|
|
var CONTEXT_LOST_EVENT = 'webglcontextlost'
|
|
var CONTEXT_RESTORED_EVENT = 'webglcontextrestored'
|
|
|
|
var DYN_PROP = 1
|
|
var DYN_CONTEXT = 2
|
|
var DYN_STATE = 3
|
|
|
|
function find (haystack, needle) {
|
|
for (var i = 0; i < haystack.length; ++i) {
|
|
if (haystack[i] === needle) {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
module.exports = function wrapREGL (args) {
|
|
var config = initWebGL(args)
|
|
if (!config) {
|
|
return null
|
|
}
|
|
|
|
var gl = config.gl
|
|
var glAttributes = gl.getContextAttributes()
|
|
var contextLost = gl.isContextLost()
|
|
|
|
var extensionState = wrapExtensions(gl, config)
|
|
if (!extensionState) {
|
|
return null
|
|
}
|
|
|
|
var stringStore = createStringStore()
|
|
var stats = createStats()
|
|
var extensions = extensionState.extensions
|
|
var timer = createTimer(gl, extensions)
|
|
|
|
var START_TIME = clock()
|
|
var WIDTH = gl.drawingBufferWidth
|
|
var HEIGHT = gl.drawingBufferHeight
|
|
|
|
var contextState = {
|
|
tick: 0,
|
|
time: 0,
|
|
viewportWidth: WIDTH,
|
|
viewportHeight: HEIGHT,
|
|
framebufferWidth: WIDTH,
|
|
framebufferHeight: HEIGHT,
|
|
drawingBufferWidth: WIDTH,
|
|
drawingBufferHeight: HEIGHT,
|
|
pixelRatio: config.pixelRatio
|
|
}
|
|
var uniformState = {}
|
|
var drawState = {
|
|
elements: null,
|
|
primitive: 4, // GL_TRIANGLES
|
|
count: -1,
|
|
offset: 0,
|
|
instances: -1
|
|
}
|
|
|
|
var limits = wrapLimits(gl, extensions)
|
|
var bufferState = wrapBuffers(
|
|
gl,
|
|
stats,
|
|
config,
|
|
destroyBuffer)
|
|
var elementState = wrapElements(gl, extensions, bufferState, stats)
|
|
var attributeState = wrapAttributes(
|
|
gl,
|
|
extensions,
|
|
limits,
|
|
stats,
|
|
bufferState,
|
|
elementState,
|
|
drawState)
|
|
function destroyBuffer (buffer) {
|
|
return attributeState.destroyBuffer(buffer)
|
|
}
|
|
var shaderState = wrapShaders(gl, stringStore, stats, config)
|
|
var textureState = wrapTextures(
|
|
gl,
|
|
extensions,
|
|
limits,
|
|
function () { core.procs.poll() },
|
|
contextState,
|
|
stats,
|
|
config)
|
|
var renderbufferState = wrapRenderbuffers(gl, extensions, limits, stats, config)
|
|
var framebufferState = wrapFramebuffers(
|
|
gl,
|
|
extensions,
|
|
limits,
|
|
textureState,
|
|
renderbufferState,
|
|
stats)
|
|
var core = createCore(
|
|
gl,
|
|
stringStore,
|
|
extensions,
|
|
limits,
|
|
bufferState,
|
|
elementState,
|
|
textureState,
|
|
framebufferState,
|
|
uniformState,
|
|
attributeState,
|
|
shaderState,
|
|
drawState,
|
|
contextState,
|
|
timer,
|
|
config)
|
|
var readPixels = wrapRead(
|
|
gl,
|
|
framebufferState,
|
|
core.procs.poll,
|
|
contextState,
|
|
glAttributes, extensions, limits)
|
|
|
|
var nextState = core.next
|
|
var canvas = gl.canvas
|
|
|
|
var rafCallbacks = []
|
|
var lossCallbacks = []
|
|
var restoreCallbacks = []
|
|
var destroyCallbacks = [config.onDestroy]
|
|
|
|
var activeRAF = null
|
|
function handleRAF () {
|
|
if (rafCallbacks.length === 0) {
|
|
if (timer) {
|
|
timer.update()
|
|
}
|
|
activeRAF = null
|
|
return
|
|
}
|
|
|
|
// schedule next animation frame
|
|
activeRAF = raf.next(handleRAF)
|
|
|
|
// poll for changes
|
|
poll()
|
|
|
|
// fire a callback for all pending rafs
|
|
for (var i = rafCallbacks.length - 1; i >= 0; --i) {
|
|
var cb = rafCallbacks[i]
|
|
if (cb) {
|
|
cb(contextState, null, 0)
|
|
}
|
|
}
|
|
|
|
// flush all pending webgl calls
|
|
gl.flush()
|
|
|
|
// poll GPU timers *after* gl.flush so we don't delay command dispatch
|
|
if (timer) {
|
|
timer.update()
|
|
}
|
|
}
|
|
|
|
function startRAF () {
|
|
if (!activeRAF && rafCallbacks.length > 0) {
|
|
activeRAF = raf.next(handleRAF)
|
|
}
|
|
}
|
|
|
|
function stopRAF () {
|
|
if (activeRAF) {
|
|
raf.cancel(handleRAF)
|
|
activeRAF = null
|
|
}
|
|
}
|
|
|
|
function handleContextLoss (event) {
|
|
event.preventDefault()
|
|
|
|
// set context lost flag
|
|
contextLost = true
|
|
|
|
// pause request animation frame
|
|
stopRAF()
|
|
|
|
// lose context
|
|
lossCallbacks.forEach(function (cb) {
|
|
cb()
|
|
})
|
|
}
|
|
|
|
function handleContextRestored (event) {
|
|
// clear error code
|
|
gl.getError()
|
|
|
|
// clear context lost flag
|
|
contextLost = false
|
|
|
|
// refresh state
|
|
extensionState.restore()
|
|
shaderState.restore()
|
|
bufferState.restore()
|
|
textureState.restore()
|
|
renderbufferState.restore()
|
|
framebufferState.restore()
|
|
attributeState.restore()
|
|
if (timer) {
|
|
timer.restore()
|
|
}
|
|
|
|
// refresh state
|
|
core.procs.refresh()
|
|
|
|
// restart RAF
|
|
startRAF()
|
|
|
|
// restore context
|
|
restoreCallbacks.forEach(function (cb) {
|
|
cb()
|
|
})
|
|
}
|
|
|
|
if (canvas) {
|
|
canvas.addEventListener(CONTEXT_LOST_EVENT, handleContextLoss, false)
|
|
canvas.addEventListener(CONTEXT_RESTORED_EVENT, handleContextRestored, false)
|
|
}
|
|
|
|
function destroy () {
|
|
rafCallbacks.length = 0
|
|
stopRAF()
|
|
|
|
if (canvas) {
|
|
canvas.removeEventListener(CONTEXT_LOST_EVENT, handleContextLoss)
|
|
canvas.removeEventListener(CONTEXT_RESTORED_EVENT, handleContextRestored)
|
|
}
|
|
|
|
shaderState.clear()
|
|
framebufferState.clear()
|
|
renderbufferState.clear()
|
|
attributeState.clear()
|
|
textureState.clear()
|
|
elementState.clear()
|
|
bufferState.clear()
|
|
|
|
if (timer) {
|
|
timer.clear()
|
|
}
|
|
|
|
destroyCallbacks.forEach(function (cb) {
|
|
cb()
|
|
})
|
|
}
|
|
|
|
function compileProcedure (options) {
|
|
check(!!options, 'invalid args to regl({...})')
|
|
check.type(options, 'object', 'invalid args to regl({...})')
|
|
|
|
function flattenNestedOptions (options) {
|
|
var result = extend({}, options)
|
|
delete result.uniforms
|
|
delete result.attributes
|
|
delete result.context
|
|
delete result.vao
|
|
|
|
if ('stencil' in result && result.stencil.op) {
|
|
result.stencil.opBack = result.stencil.opFront = result.stencil.op
|
|
delete result.stencil.op
|
|
}
|
|
|
|
function merge (name) {
|
|
if (name in result) {
|
|
var child = result[name]
|
|
delete result[name]
|
|
Object.keys(child).forEach(function (prop) {
|
|
result[name + '.' + prop] = child[prop]
|
|
})
|
|
}
|
|
}
|
|
merge('blend')
|
|
merge('depth')
|
|
merge('cull')
|
|
merge('stencil')
|
|
merge('polygonOffset')
|
|
merge('scissor')
|
|
merge('sample')
|
|
|
|
if ('vao' in options) {
|
|
result.vao = options.vao
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function separateDynamic (object, useArrays) {
|
|
var staticItems = {}
|
|
var dynamicItems = {}
|
|
Object.keys(object).forEach(function (option) {
|
|
var value = object[option]
|
|
if (dynamic.isDynamic(value)) {
|
|
dynamicItems[option] = dynamic.unbox(value, option)
|
|
return
|
|
} else if (useArrays && Array.isArray(value)) {
|
|
for (var i = 0; i < value.length; ++i) {
|
|
if (dynamic.isDynamic(value[i])) {
|
|
dynamicItems[option] = dynamic.unbox(value, option)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
staticItems[option] = value
|
|
})
|
|
return {
|
|
dynamic: dynamicItems,
|
|
static: staticItems
|
|
}
|
|
}
|
|
|
|
// Treat context variables separate from other dynamic variables
|
|
var context = separateDynamic(options.context || {}, true)
|
|
var uniforms = separateDynamic(options.uniforms || {}, true)
|
|
var attributes = separateDynamic(options.attributes || {}, false)
|
|
var opts = separateDynamic(flattenNestedOptions(options), false)
|
|
|
|
var stats = {
|
|
gpuTime: 0.0,
|
|
cpuTime: 0.0,
|
|
count: 0
|
|
}
|
|
|
|
var compiled = core.compile(opts, attributes, uniforms, context, stats)
|
|
|
|
var draw = compiled.draw
|
|
var batch = compiled.batch
|
|
var scope = compiled.scope
|
|
|
|
// FIXME: we should modify code generation for batch commands so this
|
|
// isn't necessary
|
|
var EMPTY_ARRAY = []
|
|
function reserve (count) {
|
|
while (EMPTY_ARRAY.length < count) {
|
|
EMPTY_ARRAY.push(null)
|
|
}
|
|
return EMPTY_ARRAY
|
|
}
|
|
|
|
function REGLCommand (args, body) {
|
|
var i
|
|
if (contextLost) {
|
|
check.raise('context lost')
|
|
}
|
|
if (typeof args === 'function') {
|
|
return scope.call(this, null, args, 0)
|
|
} else if (typeof body === 'function') {
|
|
if (typeof args === 'number') {
|
|
for (i = 0; i < args; ++i) {
|
|
scope.call(this, null, body, i)
|
|
}
|
|
} else if (Array.isArray(args)) {
|
|
for (i = 0; i < args.length; ++i) {
|
|
scope.call(this, args[i], body, i)
|
|
}
|
|
} else {
|
|
return scope.call(this, args, body, 0)
|
|
}
|
|
} else if (typeof args === 'number') {
|
|
if (args > 0) {
|
|
return batch.call(this, reserve(args | 0), args | 0)
|
|
}
|
|
} else if (Array.isArray(args)) {
|
|
if (args.length) {
|
|
return batch.call(this, args, args.length)
|
|
}
|
|
} else {
|
|
return draw.call(this, args)
|
|
}
|
|
}
|
|
|
|
return extend(REGLCommand, {
|
|
stats: stats,
|
|
destroy: function () {
|
|
compiled.destroy()
|
|
}
|
|
})
|
|
}
|
|
|
|
var setFBO = framebufferState.setFBO = compileProcedure({
|
|
framebuffer: dynamic.define.call(null, DYN_PROP, 'framebuffer')
|
|
})
|
|
|
|
function clearImpl (_, options) {
|
|
var clearFlags = 0
|
|
core.procs.poll()
|
|
|
|
var c = options.color
|
|
if (c) {
|
|
gl.clearColor(+c[0] || 0, +c[1] || 0, +c[2] || 0, +c[3] || 0)
|
|
clearFlags |= GL_COLOR_BUFFER_BIT
|
|
}
|
|
if ('depth' in options) {
|
|
gl.clearDepth(+options.depth)
|
|
clearFlags |= GL_DEPTH_BUFFER_BIT
|
|
}
|
|
if ('stencil' in options) {
|
|
gl.clearStencil(options.stencil | 0)
|
|
clearFlags |= GL_STENCIL_BUFFER_BIT
|
|
}
|
|
|
|
check(!!clearFlags, 'called regl.clear with no buffer specified')
|
|
gl.clear(clearFlags)
|
|
}
|
|
|
|
function clear (options) {
|
|
check(
|
|
typeof options === 'object' && options,
|
|
'regl.clear() takes an object as input')
|
|
if ('framebuffer' in options) {
|
|
if (options.framebuffer &&
|
|
options.framebuffer_reglType === 'framebufferCube') {
|
|
for (var i = 0; i < 6; ++i) {
|
|
setFBO(extend({
|
|
framebuffer: options.framebuffer.faces[i]
|
|
}, options), clearImpl)
|
|
}
|
|
} else {
|
|
setFBO(options, clearImpl)
|
|
}
|
|
} else {
|
|
clearImpl(null, options)
|
|
}
|
|
}
|
|
|
|
function frame (cb) {
|
|
check.type(cb, 'function', 'regl.frame() callback must be a function')
|
|
rafCallbacks.push(cb)
|
|
|
|
function cancel () {
|
|
// FIXME: should we check something other than equals cb here?
|
|
// what if a user calls frame twice with the same callback...
|
|
//
|
|
var i = find(rafCallbacks, cb)
|
|
check(i >= 0, 'cannot cancel a frame twice')
|
|
function pendingCancel () {
|
|
var index = find(rafCallbacks, pendingCancel)
|
|
rafCallbacks[index] = rafCallbacks[rafCallbacks.length - 1]
|
|
rafCallbacks.length -= 1
|
|
if (rafCallbacks.length <= 0) {
|
|
stopRAF()
|
|
}
|
|
}
|
|
rafCallbacks[i] = pendingCancel
|
|
}
|
|
|
|
startRAF()
|
|
|
|
return {
|
|
cancel: cancel
|
|
}
|
|
}
|
|
|
|
// poll viewport
|
|
function pollViewport () {
|
|
var viewport = nextState.viewport
|
|
var scissorBox = nextState.scissor_box
|
|
viewport[0] = viewport[1] = scissorBox[0] = scissorBox[1] = 0
|
|
contextState.viewportWidth =
|
|
contextState.framebufferWidth =
|
|
contextState.drawingBufferWidth =
|
|
viewport[2] =
|
|
scissorBox[2] = gl.drawingBufferWidth
|
|
contextState.viewportHeight =
|
|
contextState.framebufferHeight =
|
|
contextState.drawingBufferHeight =
|
|
viewport[3] =
|
|
scissorBox[3] = gl.drawingBufferHeight
|
|
}
|
|
|
|
function poll () {
|
|
contextState.tick += 1
|
|
contextState.time = now()
|
|
pollViewport()
|
|
core.procs.poll()
|
|
}
|
|
|
|
function refresh () {
|
|
textureState.refresh()
|
|
pollViewport()
|
|
core.procs.refresh()
|
|
if (timer) {
|
|
timer.update()
|
|
}
|
|
}
|
|
|
|
function now () {
|
|
return (clock() - START_TIME) / 1000.0
|
|
}
|
|
|
|
refresh()
|
|
|
|
function addListener (event, callback) {
|
|
check.type(callback, 'function', 'listener callback must be a function')
|
|
|
|
var callbacks
|
|
switch (event) {
|
|
case 'frame':
|
|
return frame(callback)
|
|
case 'lost':
|
|
callbacks = lossCallbacks
|
|
break
|
|
case 'restore':
|
|
callbacks = restoreCallbacks
|
|
break
|
|
case 'destroy':
|
|
callbacks = destroyCallbacks
|
|
break
|
|
default:
|
|
check.raise('invalid event, must be one of frame,lost,restore,destroy')
|
|
}
|
|
|
|
callbacks.push(callback)
|
|
return {
|
|
cancel: function () {
|
|
for (var i = 0; i < callbacks.length; ++i) {
|
|
if (callbacks[i] === callback) {
|
|
callbacks[i] = callbacks[callbacks.length - 1]
|
|
callbacks.pop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var regl = extend(compileProcedure, {
|
|
// Clear current FBO
|
|
clear: clear,
|
|
|
|
// Short cuts for dynamic variables
|
|
prop: dynamic.define.bind(null, DYN_PROP),
|
|
context: dynamic.define.bind(null, DYN_CONTEXT),
|
|
this: dynamic.define.bind(null, DYN_STATE),
|
|
|
|
// executes an empty draw command
|
|
draw: compileProcedure({}),
|
|
|
|
// Resources
|
|
buffer: function (options) {
|
|
return bufferState.create(options, GL_ARRAY_BUFFER, false, false)
|
|
},
|
|
elements: function (options) {
|
|
return elementState.create(options, false)
|
|
},
|
|
texture: textureState.create2D,
|
|
cube: textureState.createCube,
|
|
renderbuffer: renderbufferState.create,
|
|
framebuffer: framebufferState.create,
|
|
framebufferCube: framebufferState.createCube,
|
|
vao: attributeState.createVAO,
|
|
|
|
// Expose context attributes
|
|
attributes: glAttributes,
|
|
|
|
// Frame rendering
|
|
frame: frame,
|
|
on: addListener,
|
|
|
|
// System limits
|
|
limits: limits,
|
|
hasExtension: function (name) {
|
|
return limits.extensions.indexOf(name.toLowerCase()) >= 0
|
|
},
|
|
|
|
// Read pixels
|
|
read: readPixels,
|
|
|
|
// Destroy regl and all associated resources
|
|
destroy: destroy,
|
|
|
|
// Direct GL state manipulation
|
|
_gl: gl,
|
|
_refresh: refresh,
|
|
|
|
poll: function () {
|
|
poll()
|
|
if (timer) {
|
|
timer.update()
|
|
}
|
|
},
|
|
|
|
// Current time
|
|
now: now,
|
|
|
|
// regl Statistics Information
|
|
stats: stats
|
|
})
|
|
|
|
config.onDone(null, regl)
|
|
|
|
return regl
|
|
}
|