UNPKG

beam-gl

Version:
557 lines (490 loc) 15.4 kB
import { SchemaTypes, GLTypes as GL } from '../consts.js' import * as miscUtils from './misc-utils.js' const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) /** * @param {HTMLCanvasElement} canvas */ export const getWebGLInstance = (canvas, config) => { const { contextAttributes, contextId = 'webgl2' } = config const gl = canvas.getContext(contextId, contextAttributes) if (gl) return gl return canvas.getContext('webgl', contextAttributes) } export const getExtensions = (gl, config) => { const extensions = {} config.extensions.forEach((name) => { extensions[name] = gl.getExtension(name) }) return extensions } const compileShader = (gl, type, source) => { const shader = gl.createShader(type) gl.shaderSource(shader, source) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Error compiling shaders', gl.getShaderInfoLog(shader)) gl.deleteShader(shader) return null } return shader } const initShaderProgram = (gl, defines, vs, fs) => { const defineStr = Object.keys(defines).reduce( (str, key) => defines[key] ? str + `#define ${key} ${defines[key]}\n` : '', '' ) const vertexShader = compileShader(gl, gl.VERTEX_SHADER, defineStr + vs) const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, defineStr + fs) const shaderProgram = gl.createProgram() gl.attachShader(shaderProgram, vertexShader) gl.attachShader(shaderProgram, fragmentShader) gl.linkProgram(shaderProgram) if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { console.error('Error initing program', gl.getProgramInfoLog(shaderProgram)) return null } return shaderProgram } export const initShaderRefs = (gl, defines, schema, vs, fs) => { const program = initShaderProgram(gl, defines, vs, fs) // map to { pos: { type, location } } const attributes = miscUtils.mapValue(schema.buffers, (attributes, key) => ({ type: attributes[key].type, location: gl.getAttribLocation(program, key), })) const uniforms = miscUtils.mapValue( { ...schema.uniforms, ...schema.textures, }, (uniforms, key) => ({ type: uniforms[key].type, location: gl.getUniformLocation(program, key), }) ) return { program, attributes, uniforms } } export const clear = (gl, color) => { const [r, g, b, a] = color gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) gl.clearColor(r, g, b, a) gl.clearDepth(1) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) gl.enable(gl.DEPTH_TEST) } export const initVertexBuffers = (gl, state) => { const buffers = {} const bufferKeys = Object.keys(state) bufferKeys.forEach((key) => { const buffer = gl.createBuffer() buffers[key] = buffer updateVertexBuffer(gl, buffers[key], state[key]) }) return buffers } export const updateVertexBuffer = (gl, buffer, array) => { const data = array instanceof Float32Array ? array : new Float32Array(array) gl.bindBuffer(gl.ARRAY_BUFFER, buffer) gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW) } export const destroyVertexBuffer = (gl, buffer) => { gl.deleteBuffer(buffer) } export const initIndexBuffer = (gl, state) => { const { array } = state const buffer = gl.createBuffer() updateIndexBuffer(gl, buffer, array) return buffer } export const updateIndexBuffer = (gl, buffer, array) => { const data = array instanceof Uint32Array ? array : new Uint32Array(array) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW) } export const destroyIndexBuffer = (gl, buffer) => { gl.deleteBuffer(buffer) } const compatSRGB = (gl) => { const { extensions } = gl return !isSafari && extensions.EXT_SRGB ? extensions.EXT_SRGB.SRGB_EXT : gl.RGBA } const compatSRGBA = (gl) => { const { extensions } = gl return !isSafari && extensions.EXT_SRGB ? extensions.EXT_SRGB.SRGB_ALPHA_EXT : gl.RGBA } // Hard coded for faster lookup const nativeTypeHOF = (gl) => (type) => { const map = { [GL.Repeat]: gl.REPEAT, [GL.MirroredRepeat]: gl.MIRRORED_REPEAT, [GL.ClampToEdge]: gl.CLAMP_TO_EDGE, [GL.Linear]: gl.LINEAR, [GL.Nearest]: gl.NEAREST, [GL.NearestMipmapNearest]: gl.NEAREST_MIPMAP_NEAREST, [GL.LinearMipmapNearest]: gl.LINEAR_MIPMAP_NEAREST, [GL.NearestMipmapLinear]: gl.NEAREST_MIPMAP_LINEAR, [GL.LinearMipmapLinear]: gl.LINEAR_MIPMAP_LINEAR, [GL.RGB]: gl.RGB, [GL.RGBA]: gl.RGBA, [GL.SRGB]: compatSRGB(gl), [GL.SRGBA]: compatSRGBA(gl), } return map[type] } export const init2DTexture = (gl, val) => { const texture = gl.createTexture() update2DTexture(gl, texture, val) return texture } export const initCubeTexture = (gl, val) => { const texture = gl.createTexture() updateCubeTexture(gl, texture, val) return texture } export const initTextures = (gl, state) => { const textures = {} Object.keys(state).forEach((key) => { const stateField = state[key] stateField.type = stateField.type || SchemaTypes.tex2D const texture = stateField.type === SchemaTypes.tex2D ? init2DTexture(gl, stateField) : initCubeTexture(gl, stateField) textures[key] = texture }) return textures } const supportMipmap = (image) => image && miscUtils.isPowerOf2(image.width) && miscUtils.isPowerOf2(image.height) && image.nodeName !== 'VIDEO' export const update2DTexture = (gl, texture, val) => { const native = nativeTypeHOF(gl) const { image, flip, space } = val let { wrapS, wrapT, minFilter, magFilter, premultiplyAlpha = false } = val gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, texture) gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiplyAlpha) // Image may not be provided when updating texture params if (image) { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!flip) const s = native(space || GL.RGBA) gl.texImage2D(gl.TEXTURE_2D, 0, s, s, gl.UNSIGNED_BYTE, image) if (supportMipmap(image)) gl.generateMipmap(gl.TEXTURE_2D) // Default workaround for non-mipmap 2D image if (!supportMipmap(image)) { if (!wrapS) wrapS = GL.ClampToEdge if (!wrapT) wrapT = GL.ClampToEdge if (!minFilter) minFilter = GL.Linear } } // Lazily set texture params if (wrapS) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, native(wrapS)) if (wrapT) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, native(wrapT)) if (minFilter) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, native(minFilter)) } if (magFilter) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, native(magFilter)) } return texture } export const updateCubeTexture = (gl, texture, val) => { const native = nativeTypeHOF(gl) const { images, level, flip, wrapS, wrapT, minFilter, magFilter, space } = val gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture) if (wrapS) { gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, native(wrapS)) } if (wrapT) { gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, native(wrapT)) } if (minFilter) { gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, native(minFilter) ) } if (magFilter) { gl.texParameteri( gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, native(magFilter) ) } // Image may not be provided when updating texture params if (images) { if (flip) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) else gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false) const faces = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, ] let count = 0 const s = native(space || GL.RGBA) for (let i = 0; i < faces.length; i++) { for (let j = 0; j <= level; j++) { const face = faces[i] gl.texImage2D(face, j, s, s, gl.UNSIGNED_BYTE, images[count]) count++ } } } return texture } export const destroyTexture = (gl, texture) => { gl.deleteTexture(texture) } /** * @param {WebGLRenderingContext} gl * @param {*} state */ const initColorOffscreen = (gl, state) => { const fbo = gl.createFramebuffer() const rbo = gl.createRenderbuffer() const colorTexture = gl.createTexture() const depthTexture = null const { width, height, debug = false } = state gl.bindTexture(gl.TEXTURE_2D, colorTexture) gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null ) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.bindRenderbuffer(gl.RENDERBUFFER, rbo) gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height) gl.bindFramebuffer(gl.FRAMEBUFFER, fbo) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0 ) gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rbo ) if (debug) { const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER) if (gl.FRAMEBUFFER_COMPLETE !== e) { console.error('Frame buffer object is incomplete: ' + e.toString()) } } gl.bindFramebuffer(gl.FRAMEBUFFER, null) gl.bindTexture(gl.TEXTURE_2D, null) gl.bindRenderbuffer(gl.RENDERBUFFER, null) return { fbo, rbo, colorTexture, depthTexture } } const initDepthOffscreen = (gl, state) => { const { width, height, debug = false } = state const fbo = gl.createFramebuffer() const rbo = null const colorTexture = gl.createTexture() const depthTexture = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, colorTexture) gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null ) gl.bindTexture(gl.TEXTURE_2D, depthTexture) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.texImage2D( gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null ) gl.bindFramebuffer(gl.FRAMEBUFFER, fbo) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0 ) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0 ) if (debug) { const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER) if (e !== gl.FRAMEBUFFER_COMPLETE) { console.error('framebuffer not complete', e.toString()) } } gl.bindTexture(gl.TEXTURE_2D, null) gl.bindFramebuffer(gl.FRAMEBUFFER, null) return { fbo, rbo, colorTexture, depthTexture } } export const initOffscreen = (gl, state) => { if (state.depth) return initDepthOffscreen(gl, state) else return initColorOffscreen(gl, state) } /** * @param {WebGLRenderingContext} gl * @param {*} target */ export const resetOffscreen = (gl, target) => { gl.deleteFramebuffer(target.fbo) gl.deleteRenderbuffer(target.rbo) gl.deleteTexture(target.colorTexture) gl.deleteTexture(target.depthTexture) } const padDefault = (schema, key, val) => { return val !== undefined ? val : schema.uniforms[key].default } let lastProgram = null export const draw = ( gl, shader, vertexBuffers, indexResource, uniforms, textures ) => { const { schema, shaderRefs } = shader const { program } = shaderRefs if (!lastProgram || lastProgram !== program) { gl.useProgram(program) lastProgram = program } Object.keys(shaderRefs.attributes).forEach((key) => { if ( !schema.buffers[key] || schema.buffers[key].type === SchemaTypes.index || !vertexBuffers[key] ) { return } const { location } = shaderRefs.attributes[key] const { n, type } = schema.buffers[key] const numComponents = n || miscUtils.getNumComponents(type) gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffers[key]) gl.vertexAttribPointer(location, numComponents, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(location) }) const { buffer, state } = indexResource const { offset, count } = state if (buffer) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer) } let unit = -1 Object.keys(shaderRefs.uniforms).forEach((key) => { const { type, location } = shaderRefs.uniforms[key] let val const isTexture = type === SchemaTypes.tex2D || type === SchemaTypes.texCube if (!isTexture) { val = padDefault(schema, key, uniforms[key]) } const uniformSetterMapping = { [SchemaTypes.vec4]: () => gl.uniform4fv(location, val), [SchemaTypes.vec3]: () => gl.uniform3fv(location, val), [SchemaTypes.vec2]: () => gl.uniform2fv(location, val), [SchemaTypes.int]: () => { !val || typeof val === 'number' || typeof val === 'string' ? gl.uniform1i(location, val) : gl.uniform1iv(location, val) }, [SchemaTypes.float]: () => { !val || typeof val === 'number' || typeof val === 'string' ? gl.uniform1f(location, val) : gl.uniform1fv(location, val) }, [SchemaTypes.mat4]: () => gl.uniformMatrix4fv(location, false, val), [SchemaTypes.mat3]: () => gl.uniformMatrix3fv(location, false, val), [SchemaTypes.mat2]: () => gl.uniformMatrix2fv(location, false, val), [SchemaTypes.tex2D]: () => { unit++ const texture = textures[key] if (!texture) { console.warn(`Missing texture ${key} at unit ${unit}`) return } gl.uniform1i(location, unit) gl.activeTexture(gl.TEXTURE0 + unit) gl.bindTexture(gl.TEXTURE_2D, texture) }, [SchemaTypes.texCube]: () => { unit++ const texture = textures[key] if (!texture) { console.warn(`Missing texture ${key} at unit ${unit}`) return } gl.uniform1i(location, unit) gl.activeTexture(gl.TEXTURE0 + unit) gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture) }, } // FIXME uniform keys padded by default are always re-uploaded. if (val !== undefined || isTexture) uniformSetterMapping[type]() }) const drawMode = schema.mode === GL.Triangles ? gl.TRIANGLES : gl.LINES gl.drawElements(drawMode, count, gl.UNSIGNED_INT, offset * 4) } /** * @param {WebGLRenderingContext} gl * @param {*} target */ export const beforeDrawToColor = (gl, target) => { const { state, colorTexture, fbo, rbo } = target const { width, height } = state gl.viewport(0, 0, width, height) gl.bindFramebuffer(gl.FRAMEBUFFER, fbo) gl.bindRenderbuffer(gl.RENDERBUFFER, rbo) gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height) gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0 ) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) } export const beforeDrawToDepth = (gl, target) => { const { state, fbo } = target const { width, height } = state gl.viewport(0, 0, width, height) gl.bindFramebuffer(gl.FRAMEBUFFER, fbo) gl.clear(gl.DEPTH_BUFFER_BIT) }