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 cachedCode = config.cachedCode || {}; 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, cachedCode, 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 } } } } } function getCachedCode() { return cachedCode } function preloadCachedCode(moreCache) { Object.entries(moreCache).forEach(function (kv) { cachedCode[kv[0]] = kv[1] }) } 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, // cache generated code getCachedCode: getCachedCode, preloadCachedCode: preloadCachedCode }) config.onDone(null, regl) return regl }