diff --git "a/ffi/opengl.js" "b/ffi/opengl.js" new file mode 100644--- /dev/null +++ "b/ffi/opengl.js" @@ -0,0 +1,3093 @@ +// This is an OpenGL implementation for SqueakJS using WebGL. + +// It is very much incomplete and currently only implements +// the subset of OpenGL that is used by Croquet Jasmine, +// but could be extended to support more. + +// The functions are invoked via FFI, which takes care of +// converting the arguments and return values between JS +// and Smalltalk. + +// The OpenGL context is global and created by B3DAcceleratorPlugin. +// Context switching is done by B3DAcceleratorPlugin.makeCurrent(). + +// helpful constant lookup: +// https://javagl.github.io/GLConstantsTranslator/GLConstantsTranslator.html + +// TODO +// [X] implement draw arrays +// [X] implement draw elements +// [ ] implement vertex buffer objects +// [X] implement material + lighting +// [X] implement clip planes +// [X] implement fog +// [ ] implement tex coord gen +// [ ] fix glBitmap for size other than 640x480 (also, make pixel perfect) +// [ ] optimize list compilation glBegin/glEnd +// [ ] implement light attenuation +// [ ] implement spot lights +// [ ] implement color material +// [ ] emulate glLineWidth (WebGL usually only supports 1px lines) +// e.g. using https://wwwtyro.net/2019/11/18/instanced-lines.html +// [ ] full OpenGL 1.0 support +// [ ] full OpenGL 1.1 support +// [ ] full OpenGL 1.2 support +// [ ] full OpenGL 1.3 support +// [ ] full OpenGL 1.4 support +// [ ] full OpenGL 1.5 support +// [ ] some extensions? + +// OpenGL constants (many missing in WebGL) +var GL; + +function OpenGL() { + "use strict"; + + var DEBUG = 0; + // 0 = off + // 1 = some (errors, warnings) + // 2 = lots (function calls) + // 3 = all (call details) + + var identity = new Float32Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + ]); + + // Primitive attributes for glBegin/glEnd + var flagCounter = 0; + var HAS_NORMAL = 1 << flagCounter++; + var HAS_COLOR = 1 << flagCounter++; + var HAS_TEXCOORD = 1 << flagCounter++; + + // additional flags for selecting shader + var USE_TEXTURE = 1 << flagCounter++; + var USE_ALPHA_TEST = 1 << flagCounter++; + var USE_POINT_SIZE = 1 << flagCounter++; + var NUM_LIGHTS_MASK = (1 << flagCounter++) // 3 bits for number of lights (0-7) + + (1 << flagCounter++) + + (1 << flagCounter++); + var NUM_CLIP_PLANES_MASK = (1 << flagCounter++) // 3 bits for number of clip planes (0-5) + + (1 << flagCounter++) + + (1 << flagCounter++); + var FOG_MASK = (1 << flagCounter++) // 2 bits for fog mode (off, linear, exp, exp2) + + (1 << flagCounter++); + + // this math is silly but fun ... + var NUM_LIGHTS_SHIFT = Math.floor(Math.log2(NUM_LIGHTS_MASK)) - 2; + var MAX_LIGHTS = (NUM_LIGHTS_MASK >> NUM_LIGHTS_SHIFT) + 1; + var ANY_LIGHTS = (MAX_LIGHTS-1) << NUM_LIGHTS_SHIFT; + if (ANY_LIGHTS !== NUM_LIGHTS_MASK) throw Error("OpenGL: bad NUM_LIGHTS_MASK"); + + var NUM_CLIP_PLANES_SHIFT = Math.floor(Math.log2(NUM_CLIP_PLANES_MASK)) - 2; + var MAX_CLIP_PLANES = (NUM_CLIP_PLANES_MASK >> NUM_CLIP_PLANES_SHIFT) + 1; + var ANY_CLIP_PLANES = (MAX_CLIP_PLANES-1) << NUM_CLIP_PLANES_SHIFT; + if (ANY_CLIP_PLANES !== NUM_CLIP_PLANES_MASK) throw Error("OpenGL: bad NUM_CLIP_PLANES_MASK"); + + var FOG_SHIFT = Math.floor(Math.log2(FOG_MASK)) - 1; + var MAX_FOG = (FOG_MASK >> FOG_SHIFT) + 1; + var ANY_FOG = (MAX_FOG-1) << FOG_SHIFT; + if (ANY_FOG !== FOG_MASK) throw Error("OpenGL: bad ANY_FOG"); + var NO_FOG = 0; + var LINEAR_FOG = 1; + var EXP_FOG = 2; + var EXP2_FOG = 3; + + var gl; // the emulated OpenGL state + var webgl; // the actual WebGL context + + return { + getModuleName: function() { return 'libGL.so (SqueakJS)'; }, + + setInterpreter: function(anInterpreterProxy) { + this.vm = anInterpreterProxy.vm; + this.ffi = this.vm.primHandler; + return true; + }, + + ffiFunctionNotFoundHandler: function(name, args) { + this.vm.warnOnce("OpenGL: UNIMPLEMENTED (missing) " + name); + if (DEBUG > 0) debugger; + return null; // do not fail but return nil + }, + + initialiseModule: function() { + DEBUG > 1 && console.log("OpenGL: initialiseModule"); + if (!GL) initGLConstants(); + // connect to B3DAcceleratorPlugin to get WebGL context + var modules = SqueakJS.vm.primHandler.loadedModules; + var B3DAcceleratorPlugin = modules['B3DAcceleratorPlugin']; + if (!B3DAcceleratorPlugin) throw Error("OpenGL: B3DAcceleratorPlugin not loaded"); + this.GL = GL; + B3DAcceleratorPlugin.setOpenGL(this); // will call makeCurrent() + }, + + makeCurrent: function(renderer) { + if (webgl === renderer.webgl) return; // already current + DEBUG > 1 && console.log("OpenGL: makeCurrent", renderer); + webgl = renderer.webgl; + gl = renderer.opengl; + if (!gl) renderer.opengl = this.initGL(); + }, + + initGL: function() { + DEBUG > 0 && console.log("OpenGL: initGL"); + // if webgl-lint is loaded, configure it + const ext = webgl.getExtension('GMAN_debug_helper'); + if (ext) ext.setConfiguration({ + throwOnError: false, + }); + // if Spector script is loaded, capture WebGL calls + if (typeof SPECTOR !== "undefined") { + var spector = new SPECTOR.Spector(); + spector.captureContext(webgl); + spector.displayUI(); + } + // initialize emulated OpenGL state + gl = { + alphaTest: false, + alphaFunc: null, + alphaRef: 0, + extensions: "GL_ARB_texture_non_power_of_two GL_SGIS_generate_mipmap GL_ARB_transpose_matrix", + color: new Float32Array(4), + normal: new Float32Array([0, 0, 1]), + texCoord: new Float32Array(2), + primitive: null, // for glBegin/glEnd + primitiveAttrs: 0, // for glVertex + clipPlanes: [], // clip plane equations + clientState: {}, // enabled arrays by attr + fogMode: GL.EXP, // fog mode + fogEnabled: false, // fog enabled + fogDensity: 1, // fog density + fogStart: 0, // fog start + fogEnd: 1, // fog end + fogColor: new Float32Array([0, 0, 0, 0]), // fog color + fogHint: GL.DONT_CARE, // fog hint + shaders: {}, // shader programs by attr/flags + matrixMode: 0, // current matrix mode + matrices: {}, // matrix stacks by mode + matrix: null, // current matrix (matrices[mode][0]) + lightingEnabled: false, + lights: [], // light states + lightModelAmbient: null, // scene ambient color + material: null, // material state + textureIdGen: 0, // texture id generator + textures: {}, // webgl texture objects by id + texture: null, // texture + textureEnabled: false, // texture enabled + textureEnvMode: GL.MODULATE, // texture environment mode + listIdGen: 0, // display list id generator + lists: {}, // display lists by id + list: null, // current display list + listMode: 0, // current display list mode + listBase: 0, // base for glCallLists + pixelStoreUnpackRowLength: 0, + pixelStoreUnpackSkipRows: 0, + pixelStoreUnpackSkipPixels: 0, + rasterPos: new Float32Array(4), + rasterColor: new Float32Array([1, 1, 1, 1]), + bitmapTexture: null, // texture for glBitmap + bitmapVertexBuffer: null, // vertex buffer for glBitmap + bitmapShader: { // shader program for glBitmap + program: null, + locations: {}, + }, // shader for glBitmap + vendor: "Codefrau", + renderer: "SqueakJS", + version: "1.0", + viewport: new Int32Array([0, 0, 0, 0]), + depthRange: new Float32Array([0, 1]), + }; + + // set initial state + gl.matrices[GL.MODELVIEW] = [new Float32Array(identity)]; + gl.matrices[GL.PROJECTION] = [new Float32Array(identity)]; + gl.matrices[GL.TEXTURE] = [new Float32Array(identity)]; + gl.matrixMode = GL.MODELVIEW; + gl.matrix = gl.matrices[gl.matrixMode][0]; + gl.color.set([1, 1, 1, 1]); + for (var i = 0; i < MAX_CLIP_PLANES; i++) { + gl.clipPlanes[i] = { + enabled: false, + equation: new Float32Array([0, 0, 0, 0]), + }; + } + for (var i = 0; i < MAX_LIGHTS; i++) { + gl.lights[i] = { + enabled: false, + ambient: new Float32Array([0, 0, 0, 1]), + diffuse: new Float32Array(i === 0 ? [1, 1, 1, 1] : [0, 0, 0, 1]), + specular: new Float32Array(i === 0 ? [1, 1, 1, 1] : [0, 0, 0, 1]), + position: new Float32Array([0, 0, 1, 0]), + }; + } + gl.lightModelAmbient = new Float32Array([0.2, 0.2, 0.2, 1]); + gl.material = { + ambient: new Float32Array([0.2, 0.2, 0.2, 1]), + diffuse: new Float32Array([0.8, 0.8, 0.8, 1]), + specular: new Float32Array([0, 0, 0, 1]), + emission: new Float32Array([0, 0, 0, 1]), + shininess: 0, + }; + var clientStates = ["vertexArray", "normalArray", "colorArray", "textureCoordArray"]; + for (var i = 0; i < clientStates.length; i++) { + var attr = clientStates[i]; + gl.clientState[attr] = { + enabled: false, + size: 0, + type: GL.FLOAT, + stride: 0, + pointer: null, + // binding: null, TODO: support VBOs + } + } + return gl; + }, + + destroyGL: function(renderer) { + DEBUG > 0 && console.log("OpenGL: destroyGL"); + // TODO: delete textures, arrays, shaders? + renderer.opengl = null; + webgl = null; + gl = null; + }, + + // FFI functions get JS args, return JS result + + addToList: function(name, args) { + if (!gl.list) return false; + gl.list.commands.push({name: name, args: args}); + if (gl.listMode === GL.COMPILE) { + DEBUG > 1 && console.log("[COMPILE]", name, args); + return true; + } + return false; + }, + + glAlphaFunc: function(func, ref) { + if (gl.listMode && this.addToList("glAlphaFunc", [func, ref])) return; + DEBUG > 1 && console.log("glAlphaFunc", GL_Symbol(func), ref); + gl.alphaFunc = func; + gl.alphaRef = ref; + }, + + glBegin: function(mode) { + if (gl.listMode && this.addToList("glBegin", [mode])) return; + DEBUG > 1 && console.log("glBegin", GL_Symbol(mode, 'POINTS')); + gl.primitive = { + mode: mode, + vertices: [], + vertexSize: 0, + vertexAttrs: 0, + } + gl.primitiveAttrs = 0; + }, + + glBindTexture: function(target, texture) { + if (gl.listMode && this.addToList("glBindTexture", [target, texture])) return; + DEBUG > 1 && console.log("glBindTexture", GL_Symbol(target), texture); + var textureObj = gl.textures[texture]; + if (!textureObj) throw Error("OpenGL: texture not found"); + webgl.bindTexture(target, textureObj); + gl.texture = textureObj; + }, + + glBitmap: function(width, height, xorig, yorig, xmove, ymove, bitmap) { + // bitmap is supposed to be declared as "GLubyte*" per OpenGL spec, + // which the FFI would convert to Uint8Array for us. However, the + // image FFI declaration uses "void*", probably because it makes no + // difference in C, a pointer is a pointer. In JS, we get an + // ArrayBuffer for "void*" so we need to convert it to Uint8Array + // ourselves. + if (!bitmap.buffer) bitmap = new Uint8Array(bitmap); + if (gl.listMode && this.addToList("glBitmap", [width, height, xorig, yorig, xmove, ymove, bitmap])) return; + DEBUG > 1 && console.log("glBitmap", width, height, xorig, yorig, xmove, ymove, bitmap); + if (width > 0 && height > 0) { + // we need to convert the 1-bit deep bitmap to a 1-byte + // per pixel texture in ALPHA format, with the bitmap + // mapping 0-bits to transparent, 1-bits to opaque, + // and then draw it as a textured quad covering the viewport + var texels = new Uint8Array(width * height); + var bytesPerRow = Math.ceil(width / 32) * 4; + for (var y = 0; y < height; y++) { + var byteIndex = y * bytesPerRow; + var bitIndex = 7; + for (var x = 0; x < width; x++) { + var bit = bitmap[byteIndex] & (1 << bitIndex); + if (bit) texels[y * width + x] = 255; + bitIndex--; + if (bitIndex < 0) { + byteIndex++; + bitIndex = 7; + } + } + } + // debug: print bitmap + // s=''; for (y = height -1 ; y >= 0; y--) { for (x = 0; x < width; x++) s += texels[y * width + x] ? '⬛️' : '⬜️'; s+='\n'}; console.log(s) + var texture = gl.bitmapTexture; + if (!texture) { + texture = gl.bitmapTexture = webgl.createTexture(); + webgl.bindTexture(webgl.TEXTURE_2D, texture); + webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MIN_FILTER, webgl.LINEAR); + webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MAG_FILTER, webgl.LINEAR); + webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_S, webgl.CLAMP_TO_EDGE); + webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_T, webgl.CLAMP_TO_EDGE); + } else { + webgl.bindTexture(webgl.TEXTURE_2D, texture); + } + webgl.pixelStorei(webgl.UNPACK_ALIGNMENT, 1); + webgl.texImage2D(webgl.TEXTURE_2D, 0, webgl.ALPHA, width, height, 0, webgl.ALPHA, webgl.UNSIGNED_BYTE, texels); + webgl.pixelStorei(webgl.UNPACK_ALIGNMENT, 4); + + webgl.disable(webgl.CULL_FACE); + webgl.disable(webgl.DEPTH_TEST); + webgl.disable(webgl.BLEND); + webgl.colorMask(true, true, true, true); + webgl.viewport(0, 0, webgl.drawingBufferWidth, webgl.drawingBufferHeight); + var vertexBuffer = gl.bitmapVertexBuffer; + if (!vertexBuffer) { + var vertices = new Float32Array([ + 0, 0, + 1, 0, + 0, 1, + 1, 1, + ]); + vertexBuffer = gl.bitmapVertexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, vertexBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, vertices, webgl.STATIC_DRAW); + } else { + webgl.bindBuffer(webgl.ARRAY_BUFFER, vertexBuffer); + } + var shader = gl.bitmapShader; + if (!shader.program) { + shader.program = webgl.createProgram(); + var vs = webgl.createShader(webgl.VERTEX_SHADER); + webgl.shaderSource(vs, ` + attribute vec2 a_position; + uniform vec3 u_raster; + uniform vec2 u_rasterOffset; + uniform vec2 u_rasterScale; + uniform vec2 u_translate; + uniform vec2 u_scale; + varying vec2 v_texcoord; + void main() { + vec2 raster = u_raster.xy * u_rasterScale + u_rasterOffset; + vec2 pos = raster + a_position * u_scale + u_translate; + gl_Position = vec4(pos, u_raster.z, 1); + v_texcoord = a_position; + } + `); + webgl.compileShader(vs); + if (!webgl.getShaderParameter(vs, webgl.COMPILE_STATUS)) { + console.error("OpenGL: vertex shader compile error: " + webgl.getShaderInfoLog(vs)); + debugger; + return; + } + var fs = webgl.createShader(webgl.FRAGMENT_SHADER); + webgl.shaderSource(fs, ` + precision mediump float; + uniform sampler2D u_texture; + uniform vec4 u_color; + varying vec2 v_texcoord; + void main() { + float alpha = texture2D(u_texture, v_texcoord).a; + if (alpha < 0.5) discard; + gl_FragColor = u_color; + } + `); + webgl.compileShader(fs); + if (!webgl.getShaderParameter(fs, webgl.COMPILE_STATUS)) { + console.error("OpenGL: fragment shader compile error: " + webgl.getShaderInfoLog(fs)); + debugger; + return; + } + webgl.attachShader(shader.program, vs); + webgl.attachShader(shader.program, fs); + webgl.linkProgram(shader.program); + if (!webgl.getProgramParameter(shader.program, webgl.LINK_STATUS)) { + console.error("OpenGL: shader link error: " + webgl.getProgramInfoLog(shader.program)); + debugger + return; + } + shader.locations = { + a_position: webgl.getAttribLocation(shader.program, "a_position"), + u_texture: webgl.getUniformLocation(shader.program, "u_texture"), + u_color: webgl.getUniformLocation(shader.program, "u_color"), + u_raster: webgl.getUniformLocation(shader.program, "u_raster"), + u_rasterOffset: webgl.getUniformLocation(shader.program, "u_rasterOffset"), + u_rasterScale: webgl.getUniformLocation(shader.program, "u_rasterScale"), + u_translate: webgl.getUniformLocation(shader.program, "u_translate"), + u_scale: webgl.getUniformLocation(shader.program, "u_scale"), + }; + } + webgl.useProgram(shader.program); + webgl.enableVertexAttribArray(shader.locations.a_position); + webgl.vertexAttribPointer(shader.locations.a_position, 2, webgl.FLOAT, false, 0, 0); + webgl.uniform1i(shader.locations.u_texture, 0); + webgl.uniform4fv(shader.locations.u_color, gl.rasterColor); + var w = webgl.drawingBufferWidth; + var h = webgl.drawingBufferHeight; + // this still isn't pixel perfect. Appears to work best for 640x480 + // but not when changing the extent?! Weird. Also, some letters are still + // cut off (like "m"). + if (!this.bitmapRasterScale) this.bitmapRasterScale = [2/w, 2/h]; + if (!this.bitmapScale) this.bitmapScale = [2*width/w, 2*height/h]; + if (!this.bitmapTranslate) this.bitmapTranslate = [0, 0]; + if (!this.bitmapRasterOffset) this.bitmapRasterOffset = [-1, -1]; + // the properties above are written to allow intereactive debugging + webgl.uniform3f(shader.locations.u_raster, gl.rasterPos[0] - xorig, gl.rasterPos[1] - yorig, gl.rasterPos[2]); + webgl.uniform2fv(shader.locations.u_rasterOffset, this.bitmapRasterOffset); + webgl.uniform2fv(shader.locations.u_rasterScale, this.bitmapRasterScale); + webgl.uniform2fv(shader.locations.u_translate, this.bitmapTranslate); + webgl.uniform2fv(shader.locations.u_scale, this.bitmapScale); + webgl.drawArrays(webgl.TRIANGLE_STRIP, 0, 4); + webgl.disableVertexAttribArray(shader.locations.a_position); + webgl.bindBuffer(webgl.ARRAY_BUFFER, null); + webgl.useProgram(null); + webgl.bindTexture(webgl.TEXTURE_2D, null); + webgl.enable(webgl.CULL_FACE); + webgl.enable(webgl.DEPTH_TEST); + webgl.enable(webgl.BLEND); + } + gl.rasterPos[0] += xmove; + gl.rasterPos[1] += ymove; + }, + + glBlendFunc: function(sfactor, dfactor) { + if (gl.listMode && this.addToList("glBlendFunc", [sfactor, dfactor])) return; + DEBUG > 1 && console.log("glBlendFunc", GL_Symbol(sfactor), GL_Symbol(dfactor)); + webgl.blendFunc(sfactor, dfactor); + }, + + glCallList: function(list) { + if (gl.listMode && this.addToList("glCallList", [list])) return; + DEBUG > 1 && console.log("glCallList", list, "START"); + this.executeList(list); + DEBUG > 1 && console.log("glCallList", list, "DONE"); + }, + + glCallLists: function(n, type, lists) { + if (gl.listMode && this.addToList("glCallLists", [n, type, lists])) return; + DEBUG > 1 && console.log("glCallLists", n, GL_Symbol(type), lists); + var array; + switch (type) { + case GL.BYTE: + array = new Int8Array(lists); + break; + case GL.UNSIGNED_BYTE: + array = new Uint8Array(lists); + break; + case GL.INT: + array = new Int32Array(lists); + break; + case GL.UNSIGNED_INT: + array = new Uint32Array(lists); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glCallLists type", GL_Symbol(type)) + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glCallLists type " + GL_Symbol(type)); + return; + } + for (var i = 0; i < n; i++) { + var list = gl.listBase + array[i]; + this.executeList(list); + } + }, + + glClear: function(mask) { + if (gl.listMode && this.addToList("glClear", [mask])) return; + var maskString = ""; + if (mask & webgl.COLOR_BUFFER_BIT) maskString += " COLOR"; + if (mask & webgl.DEPTH_BUFFER_BIT) maskString += " DEPTH"; + if (mask & webgl.STENCIL_BUFFER_BIT) maskString += " STENCIL"; + DEBUG > 1 && console.log("glClear"+ maskString); + webgl.clear(mask); + // B3DAcceleratorPlugin will call vm.breakNow() + // to emulate double buffering (which will return + // control to the browser which will flush the canvas). + // We discourage breaking until then to avoid flicker + // glClear is a good place for that since it's usually + // called at least once per frame + this.vm.breakAfter(500); + }, + + glClearColor: function(red, green, blue, alpha) { + if (gl.listMode && this.addToList("glClearColor", [red, green, blue, alpha])) return; + DEBUG > 1 && console.log("glClearColor", red, green, blue, alpha); + webgl.clearColor(red, green, blue, alpha); + }, + + glColor3f: function(red, green, blue) { + if (gl.listMode && this.addToList("glColor3f", [red, green, blue])) return; + DEBUG > 1 && console.log("glColor3f", red, green, blue); + gl.color[0] = red; + gl.color[1] = green; + gl.color[2] = blue; + gl.color[3] = 1; + gl.primitiveAttrs |= HAS_COLOR; + }, + + glColor3fv: function(v) { + if (gl.listMode && this.addToList("glColor3fv", [v.slice()])) return; + DEBUG > 1 && console.log("glColor3fv", Array.from(v)); + gl.color.set(v); + gl.color[3] = 1; + gl.primitiveAttrs |= HAS_COLOR; + }, + + glColor4d: function(red, green, blue, alpha) { + if (gl.listMode && this.addToList("glColor4d", [red, green, blue, alpha])) return; + DEBUG > 1 && console.log("glColor4d", red, green, blue, alpha); + gl.color[0] = red; + gl.color[1] = green; + gl.color[2] = blue; + gl.color[3] = alpha; + gl.primitiveAttrs |= HAS_COLOR; + }, + + glColor4f: function(red, green, blue, alpha) { + if (gl.listMode && this.addToList("glColor4f", [red, green, blue, alpha])) return; + DEBUG > 1 && console.log("glColor4f", red, green, blue, alpha); + gl.color[0] = red; + gl.color[1] = green; + gl.color[2] = blue; + gl.color[3] = alpha; + gl.primitiveAttrs |= HAS_COLOR; + }, + + glColor4fv: function(v) { + if (gl.listMode && this.addToList("glColor4fv", [v.slice()])) return; + DEBUG > 1 && console.log("glColor4fv", Array.from(v)); + gl.color.set(v); + gl.primitiveAttrs |= HAS_COLOR; + }, + + glColorPointer: function(size, type, stride, pointer) { + if (gl.listMode && this.addToList("glColorPointer", [size, type, stride, pointer])) return; + DEBUG > 1 && console.log("glColorPointer", size, GL_Symbol(type), stride, pointer); + gl.clientState.colorArray.size = size; + gl.clientState.colorArray.type = type; + gl.clientState.colorArray.stride = stride; + gl.clientState.colorArray.pointer = pointer; + }, + + glColorMask: function(red, green, blue, alpha) { + if (gl.listMode && this.addToList("glColorMask", [red, green, blue, alpha])) return; + DEBUG > 1 && console.log("glColorMask", red, green, blue, alpha); + webgl.colorMask(red, green, blue, alpha); + }, + + glClipPlane: function(plane, equation) { + if (gl.listMode && this.addToList("glClipPlane", [plane, equation])) return; + DEBUG > 1 && console.log("glClipPlane", GL_Symbol(plane), Array.from(equation)); + var clipPlane = gl.clipPlanes[plane - GL.CLIP_PLANE0]; + // multiply by inverse of modelview matrix + var m = new Float32Array(16); + invertMatrix(gl.matrices[GL.MODELVIEW][0], m); + transposeMatrix(m); + multVec4(m, equation, clipPlane.equation); + }, + + glDeleteLists: function(list, range) { + DEBUG > 1 && console.log("glDeleteLists", list, range); + for (var i = 0; i < range; i++) { + delete gl.lists[list + i]; + } + }, + + glDeleteTextures: function(n, textures) { + DEBUG > 1 && console.log("glDeleteTextures", n, Array.from(textures)); + for (var i = 0; i < n; i++) { + var id = textures[i]; + var texture = gl.textures[id]; + if (texture) { + webgl.deleteTexture(texture); + if (gl.texture === texture) gl.texture = null; + } + delete gl.textures[id]; + } + }, + + glDepthFunc: function(func) { + if (gl.listMode && this.addToList("glDepthFunc", [func])) return; + DEBUG > 1 && console.log("glDepthFunc", GL_Symbol(func)); + webgl.depthFunc(func); + }, + + glDepthMask: function(flag) { + if (gl.listMode && this.addToList("glDepthMask", [flag])) return; + DEBUG > 1 && console.log("glDepthMask", flag); + webgl.depthMask(flag); + }, + + glDepthRange: function(zNear, zFar) { + if (gl.listMode && this.addToList("glDepthRange", [zNear, zFar])) return; + DEBUG > 1 && console.log("glDepthRange", zNear, zFar); + webgl.depthRange(zNear, zFar); + }, + + glDisable: function(cap) { + if (gl.listMode && this.addToList("glDisable", [cap])) return; + switch (cap) { + case GL.ALPHA_TEST: + DEBUG > 1 && console.log("glDisable GL_ALPHA_TEST"); + gl.alphaTest = false; + break; + case webgl.BLEND: + DEBUG > 1 && console.log("glDisable GL_BLEND"); + webgl.disable(webgl.BLEND); + break; + case GL.CLIP_PLANE0: + case GL.CLIP_PLANE1: + case GL.CLIP_PLANE2: + case GL.CLIP_PLANE3: + case GL.CLIP_PLANE4: + case GL.CLIP_PLANE5: + case GL.CLIP_PLANE6: + case GL.CLIP_PLANE7: + DEBUG > 1 && console.log("glDisable GL_CLIP_PLANE" + (cap - GL.CLIP_PLANE0)); + gl.clipPlanes[cap - GL.CLIP_PLANE0].enabled = false; + break; + case webgl.CULL_FACE: + DEBUG > 1 && console.log("glDisable GL.CULL_FACE"); + webgl.disable(webgl.CULL_FACE); + break; + case webgl.DEPTH_TEST: + DEBUG > 1 && console.log("glDisable GL_DEPTH_TEST"); + webgl.disable(webgl.DEPTH_TEST); + break; + case GL.FOG: + DEBUG > 1 && console.log("glDisable GL_FOG"); + gl.fogEnabled = false; + break; + case GL.NORMALIZE: + DEBUG > 1 && console.log("glDisable GL_NORMALIZE"); + // we always normalize normals + break; + case GL.LIGHT0: + case GL.LIGHT1: + case GL.LIGHT2: + case GL.LIGHT3: + case GL.LIGHT4: + case GL.LIGHT5: + case GL.LIGHT6: + case GL.LIGHT7: + DEBUG > 1 && console.log("glDisable GL_LIGHT" + (cap - GL.LIGHT0)); + gl.lights[cap - GL.LIGHT0].enabled = false; + break; + case GL.LIGHTING: + DEBUG > 1 && console.log("glDisable GL_LIGHTING"); + gl.lightingEnabled = false; + break; + case webgl.POLYGON_OFFSET_FILL: + DEBUG > 1 && console.log("glDisable GL_POLYGON_OFFSET_FILL"); + webgl.disable(webgl.POLYGON_OFFSET_FILL); + break; + case webgl.STENCIL_TEST: + DEBUG > 1 && console.log("glDisable GL_STENCIL_TEST"); + webgl.disable(webgl.STENCIL_TEST); + break; + case webgl.TEXTURE_2D: + DEBUG > 1 && console.log("glDisable GL_TEXTURE_2D"); + gl.textureEnabled = false; + break; + case GL.TEXTURE_GEN_S: + case GL.TEXTURE_GEN_T: + case GL.TEXTURE_GEN_R: + case GL.TEXTURE_GEN_Q: + if (DEBUG) console.log("UNIMPLEMENTED glDisable GL_TEXTURE_GEN_" + (cap - GL.TEXTURE_GEN_S)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDisable GL_TEXTURE_GEN_" + (cap - GL.TEXTURE_GEN_S)); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glDisable", GL_Symbol(cap)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDisable " + GL_Symbol(cap)); + } + }, + + glDisableClientState: function(cap) { + if (gl.listMode && this.addToList("glDisableClientState", [cap])) return; + switch (cap) { + case GL.VERTEX_ARRAY: + DEBUG > 1 && console.log("glDisableClientState GL_VERTEX_ARRAY"); + gl.clientState.vertexArray.enabled = false; + return; + case GL.NORMAL_ARRAY: + DEBUG > 1 && console.log("glDisableClientState GL_NORMAL_ARRAY"); + gl.clientState.normalArray.enabled = false; + return; + case GL.COLOR_ARRAY: + DEBUG > 1 && console.log("glDisableClientState GL_COLOR_ARRAY"); + gl.clientState.colorArray.enabled = false; + return; + case GL.TEXTURE_COORD_ARRAY: + DEBUG > 1 && console.log("glDisableClientState GL_TEXTURE_COORD_ARRAY"); + gl.clientState.textureCoordArray.enabled = false; + return; + default: + if (DEBUG) console.log("UNIMPLEMENTED glDisableClientState", GL_Symbol(cap)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDisableClientState " + GL_Symbol(cap)); + } + }, + + glDrawArrays: function(mode, first, count) { + if (gl.listMode && this.addToList("glDrawArrays", [mode, first, count])) return; + var geometryFlags = 0; + if (gl.clientState.normalArray.enabled) geometryFlags |= HAS_NORMAL; + if (gl.clientState.colorArray.enabled) geometryFlags |= HAS_COLOR; + if (gl.clientState.textureCoordArray.enabled) geometryFlags |= HAS_TEXCOORD; + if (mode === GL.POINTS) geometryFlags |= USE_POINT_SIZE; + + var shader = this.getShader(geometryFlags); + if (!shader.program) { + if (DEBUG) console.warn("UNIMPLEMENTED glDrawArrays shader: " + shader.label); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDrawArrays shader: " + shader.label); + return; + } + + var vertexArray = gl.clientState.vertexArray; + if (!vertexArray.enabled || !vertexArray.pointer) { + DEBUG > 0 && console.warn("glDrawArrays: GL_VERTEX_ARRAY incomplete, skipping"); + return; + } + + DEBUG > 1 && console.log("glDrawArrays", GL_Symbol(mode, 'POINTS'), first, count, shader.label); + + webgl.useProgram(shader.program); + this.setShaderUniforms(shader); + var loc = shader.locations; + + var vertexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, vertexBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, vertexArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aPosition'], vertexArray.size, vertexArray.type, false, vertexArray.stride, 0); + webgl.enableVertexAttribArray(loc['aPosition']); + + var normalBuffer; + if (loc['aNormal'] >= 0) { + var normalArray = gl.clientState.normalArray; + normalBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, normalBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, normalArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aNormal'], normalArray.size, normalArray.type, false, normalArray.stride, 0); + webgl.enableVertexAttribArray(loc['aNormal']); + } + + var colorBuffer; + if (loc['aColor'] >= 0) { + var colorArray = gl.clientState.colorArray; + colorBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, colorBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, colorArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aColor'], colorArray.size, colorArray.type, false, colorArray.stride, 0); + webgl.enableVertexAttribArray(loc['aColor']); + } + + var texCoordBuffer; + if (loc['aTexCoord'] >= 0) { + var texCoordArray = gl.clientState.textureCoordArray; + texCoordBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, texCoordBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, texCoordArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aTexCoord'], texCoordArray.size, texCoordArray.type, false, texCoordArray.stride, 0); + webgl.enableVertexAttribArray(loc['aTexCoord']); + } + + webgl.drawArrays(mode, first, count); + + webgl.useProgram(null); + + webgl.bindBuffer(webgl.ARRAY_BUFFER, null); + + webgl.disableVertexAttribArray(loc['aPosition']); + webgl.deleteBuffer(vertexBuffer); + if (normalBuffer) { + webgl.disableVertexAttribArray(loc['aNormal']); + webgl.deleteBuffer(normalBuffer); + } + if (colorBuffer) { + webgl.disableVertexAttribArray(loc['aColor']); + webgl.deleteBuffer(colorBuffer); + } + if (texCoordBuffer) { + webgl.disableVertexAttribArray(loc['aTexCoord']); + webgl.deleteBuffer(texCoordBuffer); + } + }, + + glDrawElements: function(mode, count, type, indicesPtr) { + if (gl.listMode && this.addToList("glDrawElements", [mode, count, type, indicesPtr])) return; + var indices; + switch (type) { + case GL.UNSIGNED_BYTE: + indices = new Uint8Array(indicesPtr); + break; + case GL.UNSIGNED_SHORT: + indices = new Uint16Array(indicesPtr); + break; + case GL.UNSIGNED_INT: + // not directly supported by WebGL without OES_element_index_uint + var indices32 = new Uint32Array(indicesPtr); + var max = Math.max.apply(null, indices32); + if (max > 0xFFFF) console.warn("OpenGL: glDrawElements with indices > 65535 not supported, truncating", max); + if (max <= 0xFF) { + indices = new Uint8Array(count); + type = GL.UNSIGNED_BYTE; + } else { + indices = new Uint16Array(count); + type = GL.UNSIGNED_SHORT; + } + for (var i = 0; i < count; i++) indices[i] = indices32[i]; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glDrawElements type", GL_Symbol(type)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDrawElements type " + GL_Symbol(type)); + return; + } + + // convert quads to triangles + if (mode === GL.QUADS) { + var arrayClass = indices.constructor; + var newIndices = new arrayClass(count * 6 / 4); + var j = 0; + for (var i = 0; i < count; i += 4) { + newIndices[j++] = indices[i]; + newIndices[j++] = indices[i + 1]; + newIndices[j++] = indices[i + 2]; + newIndices[j++] = indices[i]; + newIndices[j++] = indices[i + 2]; + newIndices[j++] = indices[i + 3]; + } + indices = newIndices; + count = newIndices.length; + mode = GL.TRIANGLES; + } + + var geometryFlags = 0; + if (gl.clientState.normalArray.enabled) geometryFlags |= HAS_NORMAL; + if (gl.clientState.colorArray.enabled) geometryFlags |= HAS_COLOR; + if (gl.clientState.textureCoordArray.enabled) geometryFlags |= HAS_TEXCOORD; + if (mode === GL.POINTS) geometryFlags |= USE_POINT_SIZE; + + var shader = this.getShader(geometryFlags); + if (!shader.program) { + if (DEBUG) console.warn("UNIMPLEMENTED glDrawElements shader: " + shader.label); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glDrawElements shader: " + shader.label); + return; + } + + var vertexArray = gl.clientState.vertexArray; + if (!vertexArray.enabled || !vertexArray.pointer) { + DEBUG > 0 && console.warn("glDrawElements: GL_VERTEX_ARRAY incomplete, skipping"); + return; + } + + DEBUG > 1 && console.log("glDrawElements", GL_Symbol(mode, 'POINTS'), count, GL_Symbol(type), shader.label, Array.from(indices)); + + webgl.useProgram(shader.program); + this.setShaderUniforms(shader); + var loc = shader.locations; + + var vertexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, vertexBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, vertexArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aPosition'], vertexArray.size, vertexArray.type, false, vertexArray.stride, 0); + webgl.enableVertexAttribArray(loc['aPosition']); + + var normalBuffer; + if (loc['aNormal'] >= 0) { + var normalArray = gl.clientState.normalArray; + normalBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, normalBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, normalArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aNormal'], normalArray.size, normalArray.type, false, normalArray.stride, 0); + webgl.enableVertexAttribArray(loc['aNormal']); + } + + var colorBuffer; + if (loc['aColor'] >= 0) { + var colorArray = gl.clientState.colorArray; + colorBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, colorBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, colorArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aColor'], colorArray.size, colorArray.type, false, colorArray.stride, 0); + webgl.enableVertexAttribArray(loc['aColor']); + } + + var texCoordBuffer; + if (loc['aTexCoord'] >= 0) { + var texCoordArray = gl.clientState.textureCoordArray; + texCoordBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, texCoordBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, texCoordArray.pointer, webgl.DYNAMIC_DRAW); + webgl.vertexAttribPointer(loc['aTexCoord'], texCoordArray.size, texCoordArray.type, false, texCoordArray.stride, 0); + webgl.enableVertexAttribArray(loc['aTexCoord']); + } + + var indexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, indexBuffer); + webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, indices, webgl.DYNAMIC_DRAW); + + webgl.drawElements(mode, indices.length, type, 0); + + webgl.useProgram(null); + + webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, null); + webgl.bindBuffer(webgl.ARRAY_BUFFER, null); + + webgl.deleteBuffer(indexBuffer); + webgl.disableVertexAttribArray(loc['aPosition']); + webgl.deleteBuffer(vertexBuffer); + if (normalBuffer) { + webgl.disableVertexAttribArray(loc['aNormal']); + webgl.deleteBuffer(normalBuffer); + } + if (colorBuffer) { + webgl.disableVertexAttribArray(loc['aColor']); + webgl.deleteBuffer(colorBuffer); + } + if (texCoordBuffer) { + webgl.disableVertexAttribArray(loc['aTexCoord']); + webgl.deleteBuffer(texCoordBuffer); + } + }, + + glEnable: function(cap) { + if (gl.listMode && this.addToList("glEnable", [cap])) return; + switch (cap) { + case GL.ALPHA_TEST: + DEBUG > 1 && console.log("glEnable GL_ALPHA_TEST"); + gl.alphaTest = true; + break; + case webgl.BLEND: + DEBUG > 1 && console.log("glEnable GL_BLEND"); + webgl.enable(webgl.BLEND); + break; + case GL.CLIP_PLANE0: + case GL.CLIP_PLANE1: + case GL.CLIP_PLANE2: + case GL.CLIP_PLANE3: + case GL.CLIP_PLANE4: + case GL.CLIP_PLANE5: + case GL.CLIP_PLANE6: + case GL.CLIP_PLANE7: + DEBUG > 1 && console.log("glEnable GL_CLIP_PLANE" + (cap - GL.CLIP_PLANE0)); + gl.clipPlanes[cap - GL.CLIP_PLANE0].enabled = true; + break; + case webgl.CULL_FACE: + DEBUG > 1 && console.log("glEnable GL_CULL_FACE"); + webgl.enable(webgl.CULL_FACE); + break; + case webgl.DEPTH_TEST: + DEBUG > 1 && console.log("glEnable GL_DEPTH_TEST"); + webgl.enable(webgl.DEPTH_TEST); + break; + case GL.FOG: + DEBUG > 1 && console.log("glEnable GL_FOG"); + gl.fogEnabled = true; + break; + case GL.NORMALIZE: + DEBUG > 1 && console.log("glEnable GL_NORMALIZE"); + // we always normalize normals + break; + case GL.LIGHT0: + case GL.LIGHT1: + case GL.LIGHT2: + case GL.LIGHT3: + case GL.LIGHT4: + case GL.LIGHT5: + case GL.LIGHT6: + case GL.LIGHT7: + DEBUG > 1 && console.log("glEnable GL_LIGHT" + (cap - GL.LIGHT0)); + gl.lights[cap - GL.LIGHT0].enabled = true; + break; + case GL.LIGHTING: + DEBUG > 1 && console.log("glEnable GL_LIGHTING"); + gl.lightingEnabled = true; + break; + case webgl.POLYGON_OFFSET_FILL: + DEBUG > 1 && console.log("glEnable GL_POLYGON_OFFSET_FILL"); + webgl.enable(webgl.POLYGON_OFFSET_FILL); + break; + case webgl.STENCIL_TEST: + DEBUG > 1 && console.log("glEnable GL_STENCIL_TEST"); + webgl.enable(webgl.STENCIL_TEST); + break; + case webgl.TEXTURE_2D: + DEBUG > 1 && console.log("glEnable GL_TEXTURE_2D"); + gl.textureEnabled = true; + break; + case GL.TEXTURE_GEN_S: + case GL.TEXTURE_GEN_T: + case GL.TEXTURE_GEN_R: + case GL.TEXTURE_GEN_Q: + if (DEBUG) console.log("UNIMPLEMENTED glEnable GL_" + GL_Symbol(cap, "TEXTURE_GEN_S")); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnable GL_" + GL_Symbol(cap, "TEXTURE_GEN_S")); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glEnable", GL_Symbol(cap)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnable " + GL_Symbol(cap)); + } + }, + + glEnableClientState: function(cap) { + if (gl.listMode && this.addToList("glEnableClientState", [cap])) return; + switch (cap) { + case GL.VERTEX_ARRAY: + DEBUG > 1 && console.log("glEnableClientState GL_VERTEX_ARRAY"); + gl.clientState.vertexArray.enabled = true; + return; + case GL.NORMAL_ARRAY: + DEBUG > 1 && console.log("glEnableClientState GL_NORMAL_ARRAY"); + gl.clientState.normalArray.enabled = true; + return; + case GL.COLOR_ARRAY: + DEBUG > 1 && console.log("glEnableClientState GL_COLOR_ARRAY"); + gl.clientState.colorArray.enabled = true; + return; + case GL.TEXTURE_COORD_ARRAY: + DEBUG > 1 && console.log("glEnableClientState GL_TEXTURE_COORD_ARRAY"); + gl.clientState.textureCoordArray.enabled = true; + return; + default: + if (DEBUG) console.log("UNIMPLEMENTED glEnableClientState", GL_Symbol(cap)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnableClientState " + GL_Symbol(cap)); + } + }, + + glFog: function(pname, param) { + if (gl.listMode && this.addToList("glFog", [pname, param])) return; + switch (pname) { + case GL.FOG_MODE: + gl.fogMode = param; + DEBUG > 1 && console.log("glFog GL_FOG_MODE", GL_Symbol(param)); + break; + case GL.FOG_DENSITY: + DEBUG > 1 && console.log("glFog GL_FOG_DENSITY", param); + gl.fogDensity = param; + break; + case GL.FOG_START: + DEBUG > 1 && console.log("glFog GL_FOG_START", param); + gl.fogStart = param; + break; + case GL.FOG_END: + DEBUG > 1 && console.log("glFog GL_FOG_END", param); + gl.fogEnd = param; + break; + case GL.FOG_COLOR: + DEBUG > 1 && console.log("glFog GL_FOG_COLOR", Array.from(param)); + gl.fogColor.set(param); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glFog", GL_Symbol(pname), param); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glFog " + GL_Symbol(pname)); + } + }, + + glFogi: function(pname, param) { + this.glFog(pname, param); + }, + + glFogf: function(pname, param) { + this.glFog(pname, param); + }, + + glFogiv: function(pname, params) { + // FOG_COLOR integer values are mapped linearly such that the most positive representable value maps to 1.0, + // and the most negative representable value maps to -1.0 + this.glFog(pname, pname === GL.FOG_COLOR + ? params.map(function(x) { return (x + 0.5) / (0x7FFFFFFF + 0.5); }) + : params[0]); + }, + + glFogfv: function(pname, params) { + this.glFog(pname, pname === GL.FOG_COLOR ? params : params[0]); + }, + + getShader: function(geometryFlags) { + // geometryFlags: HAS_TEXCOORD, HAS_NORMAL, HAS_COLOR, USE_POINT_SIZE + + var shaderFlags = geometryFlags; + if (gl.textureEnabled && gl.texture) shaderFlags |= USE_TEXTURE; + if (gl.alphaTest) shaderFlags |= USE_ALPHA_TEST; // UNIMPLEMENTED + if (gl.fogEnabled) switch (gl.fogMode) { + case GL.EXP: shaderFlags |= EXP_FOG << FOG_SHIFT; break; + case GL.EXP2: shaderFlags |= EXP2_FOG << FOG_SHIFT; break; + case GL.LINEAR: shaderFlags |= LINEAR_FOG << FOG_SHIFT; break; + } + var numLights = 0; + if (gl.lightingEnabled) { + for (var i = 0; i < MAX_LIGHTS; i++) { + if (gl.lights[i].enabled) numLights++; + } + shaderFlags |= numLights << NUM_LIGHTS_SHIFT; + } + var numClipPlanes = 0; + for (var i = 0; i < MAX_CLIP_PLANES; i++) { + if (gl.clipPlanes[i].enabled) { + numClipPlanes++; + } + } + shaderFlags |= numClipPlanes << NUM_CLIP_PLANES_SHIFT; + + // create shader program + var shader = gl.shaders[shaderFlags]; + if (!shader) { + var flagString = "[POSITION"; + if (shaderFlags & HAS_NORMAL) flagString += ", NORMAL"; + if (shaderFlags & HAS_COLOR) flagString += ", COLOR"; + if (shaderFlags & HAS_TEXCOORD) flagString += ", TEXCOORD"; + flagString += "]"; + if (shaderFlags & USE_TEXTURE) flagString += ", TEXTURE"; + if (shaderFlags & ANY_LIGHTS) { flagString += ", "+ numLights +" LIGHT"; if (numLights !== 1) flagString += "S"; } + if (shaderFlags & ANY_CLIP_PLANES) { flagString += ", "+ numClipPlanes +" CLIP_PLANE"; if (numClipPlanes !== 1) flagString += "S"; } + if (shaderFlags & USE_ALPHA_TEST) flagString += ", ALPHA_TEST"; + if (shaderFlags & USE_POINT_SIZE) flagString += ", POINT_SIZE"; + if (shaderFlags & ANY_FOG) flagString += ", FOG"; + + shader = gl.shaders[shaderFlags] = { + flags: shaderFlags, + label: flagString, + program: null, + locations: null, + vsource: null, // for debugging + fsource: null, // for debugging + }; + var implemented = HAS_TEXCOORD | HAS_NORMAL | HAS_COLOR | USE_TEXTURE | NUM_LIGHTS_MASK | NUM_CLIP_PLANES_MASK | USE_POINT_SIZE | FOG_MASK; + if (shaderFlags & ~implemented) return shader; + + var program = webgl.createProgram() + var vs = webgl.createShader(webgl.VERTEX_SHADER); + shader.vsource = this.vertexShaderSource(shaderFlags); + webgl.shaderSource(vs, shader.vsource); + webgl.compileShader(vs); + if (!webgl.getShaderParameter(vs, webgl.COMPILE_STATUS)) { + console.error("OpenGL: vertex shader compile error: " + webgl.getShaderInfoLog(vs)); + debugger; + return shader; + } + var fs = webgl.createShader(webgl.FRAGMENT_SHADER); + shader.fsource = this.fragmentShaderSource(shaderFlags); + webgl.shaderSource(fs, shader.fsource); + webgl.compileShader(fs); + if (!webgl.getShaderParameter(fs, webgl.COMPILE_STATUS)) { + console.error("OpenGL: fragment shader compile error: " + webgl.getShaderInfoLog(fs)); + debugger; + return shader; + } + webgl.attachShader(program, vs); + webgl.attachShader(program, fs); + webgl.linkProgram(program); + if (!webgl.getProgramParameter(program, webgl.LINK_STATUS)) { + console.error("OpenGL: shader link error: " + webgl.getProgramInfoLog(program)); + debugger + return shader; + } + shader.program = program; + shader.locations = this.getLocations(program, shaderFlags); + } + return shader; + }, + + setShaderUniforms: function(shader) { + var loc = shader.locations; + DEBUG > 2 && console.log("uModelView", Array.from(gl.matrices[GL.MODELVIEW][0])); + webgl.uniformMatrix4fv(loc['uModelView'], false, gl.matrices[GL.MODELVIEW][0]); + DEBUG > 2 && console.log("uProjection", Array.from(gl.matrices[GL.PROJECTION][0])); + webgl.uniformMatrix4fv(loc['uProjection'], false, gl.matrices[GL.PROJECTION][0]); + if (loc['uNormalMatrix']) { + var normalMatrix = asNormalMatrix(gl.matrices[GL.MODELVIEW][0]); + DEBUG > 2 && console.log("uNormalMatrix", Array.from(normalMatrix)); + webgl.uniformMatrix3fv(loc['uNormalMatrix'], false, normalMatrix); + } + if (loc['uNormal']) { + DEBUG > 2 && console.log("uNormal", Array.from(gl.normal)); + webgl.uniform3fv(loc['uNormal'], gl.normal); + } + if (loc['uColor']) { + var color = gl.color; + if (gl.textureEnvMode === GL.REPLACE) color = [1, 1, 1, 1]; // HACK + DEBUG > 2 && console.log("uColor", Array.from(color)); + webgl.uniform4fv(loc['uColor'], color); + } + if (loc['uTextureMatrix']) { + DEBUG > 2 && console.log("uTextureMatrix", Array.from(gl.matrices[GL.TEXTURE][0])); + webgl.uniformMatrix4fv(loc['uTextureMatrix'], false, gl.matrices[GL.TEXTURE][0]); + } + if (loc['uTexCoord']) { + DEBUG > 2 && console.log("uTexCoord", Array.from(gl.texCoord)); + webgl.uniform2fv(loc['uTexCoord'], gl.texCoord); + } + if (loc['uSampler']) { + DEBUG > 2 && console.log("uSampler", gl.texture); + webgl.activeTexture(webgl.TEXTURE0); + webgl.bindTexture(webgl.TEXTURE_2D, gl.texture); + webgl.uniform1i(loc['uSampler'], 0); + } + if (loc['uPointSize']) { + DEBUG > 2 && console.log("uPointSize", gl.pointSize); + webgl.uniform1f(loc['uPointSize'], gl.pointSize); + } + var numLights = (shader.flags & NUM_LIGHTS_MASK) >> NUM_LIGHTS_SHIFT; + if (numLights > 0) { + DEBUG > 2 && console.log("uLightModelAmbient", Array.from(gl.lightModelAmbient)); + webgl.uniform4fv(loc['uLightModelAmbient'], gl.lightModelAmbient); + DEBUG > 2 && console.log("uMaterialAmbient", Array.from(gl.material.ambient)); + webgl.uniform4fv(loc['uMaterialAmbient'], gl.material.ambient); + DEBUG > 2 && console.log("uMaterialDiffuse", Array.from(gl.material.diffuse)); + webgl.uniform4fv(loc['uMaterialDiffuse'], gl.material.diffuse); + DEBUG > 2 && console.log("uMaterialSpecular", Array.from(gl.material.specular)); + webgl.uniform4fv(loc['uMaterialSpecular'], gl.material.specular); + DEBUG > 2 && console.log("uMaterialEmission", Array.from(gl.material.emission)); + webgl.uniform4fv(loc['uMaterialEmission'], gl.material.emission); + DEBUG > 2 && console.log("uMaterialShininess", gl.material.shininess); + webgl.uniform1f(loc['uMaterialShininess'], gl.material.shininess); + var index = 0; + for (var i = 0; i < MAX_LIGHTS; i++) { + var light = gl.lights[i]; + if (!light.enabled) continue; + DEBUG > 2 && console.log("uLights[" + index + "].ambient", Array.from(light.ambient)); + webgl.uniform4fv(loc['uLights'][index].ambient, light.ambient); + DEBUG > 2 && console.log("uLights[" + index + "].diffuse", Array.from(light.diffuse)); + webgl.uniform4fv(loc['uLights'][index].diffuse, light.diffuse); + DEBUG > 2 && console.log("uLights[" + index + "].specular", Array.from(light.specular)); + webgl.uniform4fv(loc['uLights'][index].specular, light.specular); + DEBUG > 2 && console.log("uLights[" + index + "].position", Array.from(light.position)); + webgl.uniform4fv(loc['uLights'][index].position, light.position); + index++; + } + } + var numClipPlanes = (shader.flags & NUM_CLIP_PLANES_MASK) >> NUM_CLIP_PLANES_SHIFT; + if (numClipPlanes > 0) { + var index = 0; + for (var i = 0; i < MAX_CLIP_PLANES; i++) { + var clipPlane = gl.clipPlanes[i]; + if (!clipPlane.enabled) continue; + DEBUG > 2 && console.log("uClipPlanes[" + index + "]", Array.from(clipPlane.equation)); + webgl.uniform4fv(loc['uClipPlanes'][index], clipPlane.equation); + index++; + } + } + if (loc['uFogColor']) { + DEBUG > 2 && console.log("uFogColor", Array.from(gl.fogColor)); + webgl.uniform4fv(loc['uFogColor'], gl.fogColor); + } + if (loc['uFogEnd']) { + DEBUG > 2 && console.log("uFogEnd", gl.fogEnd); + webgl.uniform1f(loc['uFogEnd'], gl.fogEnd); + } + if (loc['uFogRange']) { + DEBUG > 2 && console.log("uFogRange", gl.fogEnd - gl.fogStart); + webgl.uniform1f(loc['uFogRange'], gl.fogEnd - gl.fogStart); + } + if (loc['uFogDensity']) { + DEBUG > 2 && console.log("uFogDensity", gl.fogDensity); + webgl.uniform1f(loc['uFogDensity'], gl.fogDensity); + } + }, + + glEnd: function() { + if (gl.listMode && this.addToList("glEnd", [])) return; + var primitive = gl.primitive; + gl.primitive = null; + + // select shader + var geometryFlags = primitive.vertexAttrs; + if (primitive.mode === GL.POINTS) geometryFlags |= USE_POINT_SIZE; + + var shader = this.getShader(geometryFlags); + if (!shader.program) { + if (DEBUG) console.warn("UNIMPLEMENTED glEnd shader: " + shader.label); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnd shader: " + shader.label); + return; + } + + // create interleaved vertex buffer + var vertices = primitive.vertices; + var size = primitive.vertexSize; + var data = new Float32Array(vertices.length * size); + for (var i = 0, offset = 0; i < vertices.length; i++, offset += size) { + data.set(vertices[i], offset); + } + var vertexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ARRAY_BUFFER, vertexBuffer); + webgl.bufferData(webgl.ARRAY_BUFFER, data, webgl.DYNAMIC_DRAW); + + // set mode depending on primitive mode + // and create index buffer if needed + var mode; + var indices; + + switch (primitive.mode) { + // supported by WebGL, no index buffer needed + case webgl.POINTS: + case webgl.LINES: + case webgl.LINE_LOOP: + case webgl.LINE_STRIP: + case webgl.TRIANGLES: + case webgl.TRIANGLE_STRIP: + case webgl.TRIANGLE_FAN: + DEBUG > 1 && console.log("glEnd " + GL_Symbol(primitive.mode, "POINTS") + ":" + shader.label); + mode = primitive.mode; + break; + // not supported by WebGL, emulate + case GL.QUADS: + // use triangles and an index buffer to + // duplicate vertices as v0-v1-v2, v2-v1-v3 + // we assume that all attributes are floats + DEBUG > 1 && console.log("glEnd GL_QUADS:" + shader.label); + indices = vertices.length > 256 + ? new Uint16Array(vertices.length * 3 / 2) + : new Uint8Array(vertices.length * 3 / 2); + var offset = 0; + for (var i = 0; i < vertices.length; i += 4) { + indices[offset++] = i; + indices[offset++] = i+1; + indices[offset++] = i+2; + indices[offset++] = i; + indices[offset++] = i+2; + indices[offset++] = i+3; + } + mode = webgl.TRIANGLES; + break; + case GL.QUAD_STRIP: + if (DEBUG) console.log("UNIMPLEMENTED glEnd GL_QUAD_STRIP:" + shader.label); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnd GL_QUAD_STRIP"); + return; + case GL.POLYGON: + // use triangle fan, which works for convex polygons + DEBUG > 1 && console.log("glEnd GL_POLYGON:" + shader.label); + mode = webgl.TRIANGLE_FAN; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glEnd", primitive.mode); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glEnd " + primitive.mode); + return; + } + var indexBuffer; + if (indices) { + indexBuffer = webgl.createBuffer(); + webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, indexBuffer); + webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, indices, webgl.DYNAMIC_DRAW); + } + + // set up uniforms and vertex attributes + var stride = size * 4; + var offset = 0; + + webgl.useProgram(shader.program); + this.setShaderUniforms(shader); + var loc = shader.locations; + DEBUG > 2 && console.log("aPosition: @" + offset + "/" + stride); + webgl.vertexAttribPointer(loc['aPosition'], 3, webgl.FLOAT, false, stride, offset); + webgl.enableVertexAttribArray(loc['aPosition']); + offset += 12; + if (loc['aNormal'] >= 0) { + DEBUG > 2 && console.log("aNormal: @" + offset + "/" + stride); + webgl.vertexAttribPointer(loc['aNormal'], 3, webgl.FLOAT, false, stride, offset); + webgl.enableVertexAttribArray(loc['aNormal']); + } + if (geometryFlags & HAS_NORMAL) offset += 12; + if (loc['aColor'] >= 0) { + DEBUG > 2 && console.log("aColor: @" + offset + "/" + stride); + webgl.vertexAttribPointer(loc['aColor'], 4, webgl.FLOAT, false, stride, offset); + webgl.enableVertexAttribArray(loc['aColor']); + } + if (geometryFlags & HAS_COLOR) offset += 16; + if (loc['aTexCoord'] >= 0) { + DEBUG > 2 && console.log("aTexCoord: @" + offset + "/" + stride); + webgl.vertexAttribPointer(loc['aTexCoord'], 2, webgl.FLOAT, false, stride, offset); + webgl.enableVertexAttribArray(loc['aTexCoord']); + } + if (geometryFlags & HAS_TEXCOORD) offset += 8; + + // draw + if (indexBuffer) { + DEBUG > 1 && console.log("glEnd: draw indexed vertices", GL_Symbol(mode, 'POINTS'), Array.from(indices), vertices.map(function(v) { return ""+v; })); + webgl.drawElements(mode, indices.length, vertices.length > 256 ? webgl.UNSIGNED_SHORT : webgl.UNSIGNED_BYTE, 0); + } else { + DEBUG > 1 && console.log("glEnd: draw vertices", GL_Symbol(mode, 'POINTS'), 0, vertices.map(function(v) { return ""+v; })); + webgl.drawArrays(mode, 0, vertices.length); + } + webgl.useProgram(null); + webgl.disableVertexAttribArray(loc['aPosition']); + if (loc['aNormal'] >= 0) webgl.disableVertexAttribArray(loc['aNormal']); + if (loc['aColor'] >= 0) webgl.disableVertexAttribArray(loc['aColor']); + if (loc['aTexCoord'] >= 0) webgl.disableVertexAttribArray(loc['aTexCoord']); + webgl.bindBuffer(webgl.ARRAY_BUFFER, null); + webgl.deleteBuffer(vertexBuffer); + if (indexBuffer) { + webgl.deleteBuffer(indexBuffer); + webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, null); + } + }, + + glEndList: function() { + DEBUG > 1 && console.log("glEndList"); + var list = gl.list; + gl.list = null; + gl.lists[list.id] = list; + gl.listMode = 0; + }, + + glFinish: function() { + DEBUG > 1 && console.log("glFinish"); + webgl.finish(); + }, + + glFlush: function() { + DEBUG > 1 && console.log("glFlush"); + webgl.flush(); + }, + + glFrontFace: function(mode) { + if (gl.listMode && this.addToList("glFrontFace", [mode])) return; + DEBUG > 1 && console.log("glFrontFace", GL_Symbol(mode)); + webgl.frontFace(mode); + }, + + glFrustum: function(left, right, bottom, top, zNear, zFar) { + if (gl.listMode && this.addToList("glFrustum", [left, right, bottom, top, zNear, zFar])) return; + DEBUG > 1 && console.log("glFrustum", left, right, bottom, top, zNear, zFar); + var m = gl.matrix; + m[0] = 2 * zNear / (right - left); + m[1] = 0; + m[2] = 0; + m[3] = 0; + + m[4] = 0; + m[5] = 2 * zNear / (top - bottom); + m[6] = 0; + m[7] = 0; + + m[8] = (right + left) / (right - left); + m[9] = (top + bottom) / (top - bottom); + m[10] = -(zFar + zNear) / (zFar - zNear); + m[11] = -1; + + m[12] = 0; + m[13] = 0; + m[14] = -(2 * zFar * zNear) / (zFar - zNear); + m[15] = 0; + }, + + glGenLists: function(range) { + DEBUG > 1 && console.log("glGenLists", range); + var firstId = 0; + if (range > 0) { + firstId = gl.listIdGen + 1; + gl.listIdGen += range; + } + return firstId; + }, + + glGenTextures: function(n, textures) { + for (var i = 0; i < n; i++) { + var id = ++gl.textureIdGen; + gl.textures[id] = webgl.createTexture(); + textures[i] = id; + } + DEBUG > 1 && console.log("glGenTextures", n, Array.from(textures)); + }, + + glGetFloatv: function(pname, params) { + switch (pname) { + case GL.MODELVIEW_MATRIX: + DEBUG > 1 && console.log("glGetFloatv GL_MODELVIEW_MATRIX"); + params.set(gl.matrices[GL.MODELVIEW][0]); + break; + case GL.PROJECTION_MATRIX: + DEBUG > 1 && console.log("glGetFloatv GL_PROJECTION_MATRIX"); + params.set(gl.matrices[GL.PROJECTION][0]); + break; + case GL.TEXTURE_MATRIX: + DEBUG > 1 && console.log("glGetFloatv GL_TEXTURE_MATRIX"); + params.set(gl.matrices[GL.TEXTURE][0]); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glGetFloatv", GL_Symbol(pname)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glGetFloatv " + GL_Symbol(pname)); + } + }, + + glGetIntegerv(name, params) { + switch (name) { + case GL.LIST_INDEX: + DEBUG > 1 && console.log("glGetIntegerv GL_LIST_INDEX"); + params[0] = gl.list ? gl.list.id : 0; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glGetIntegerv", GL_Symbol(name)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glGetIntegerv " + GL_Symbol(name)); + } + }, + + glGetString: function(name) { + switch (name) { + case GL.VERSION: + DEBUG > 1 && console.log("glGetString GL_VERSION"); + return gl.version; + case GL.VENDOR: + DEBUG > 1 && console.log("glGetString GL_VENDOR"); + return gl.vendor; + case GL.RENDERER: + DEBUG > 1 && console.log("glGetString GL_RENDERER"); + return gl.renderer; + case GL.EXTENSIONS: + DEBUG > 1 && console.log("glGetString GL_EXTENSIONS"); + return gl.extensions; + default: + if (DEBUG) console.log("UNIMPLEMENTED glGetString", name); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glGetString " + name); + } + return ""; + }, + + glGetTexLevelParameteriv: function(target, level, pname, params) { + switch (pname) { + case GL.TEXTURE_COMPRESSED: + return false; + default: + if (DEBUG) console.log("UNIMPLEMENTED glGetTexLevelParameteriv", GL_Symbol(target), level, GL_Symbol(pname), params); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glGetTexLevelParameteriv " + GL_Symbol(target) + " " + GL_Symbol(pname)); + } + }, + + glHint: function(target, mode) { + if (gl.listMode && this.addToList("glHint", [target, mode])) return; + switch (target) { + case webgl.GENERATE_MIPMAP_HINT: + DEBUG > 1 && console.log("glHint GL_GENERATE_MIPMAP_HINT", GL_Symbol(mode)); + webgl.hint(target, mode); + break; + } + // webgl doesn't support any other hints + DEBUG > 1 && console.log("IGNORING glHint", GL_Symbol(target), GL_Symbol(mode)); + }, + + glIsEnabled: function(cap) { + switch (cap) { + case GL.LIGHTING: + DEBUG > 1 && console.log("glIsEnabled GL_LIGHTING"); + return gl.lightingEnabled; + default: + if (DEBUG) console.log("UNIMPLEMENTED glIsEnabled", cap); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glIsEnabled " + cap); + } + return false; + }, + + glIsTexture: function(texture) { + DEBUG > 1 && console.log("glIsTexture", texture); + return !!gl.textures[texture]; + }, + + glLightf: function(light, pname, param) { + if (gl.listMode && this.addToList("glLightf", [light, pname, param])) return; + var i = light - GL.LIGHT0; + switch (pname) { + case GL.SPOT_CUTOFF: + if (param === 180) { + DEBUG > 1 && console.log("glLightf", i, "GL_SPOT_CUTOFF", param); + return; + } + // fall through if not default + default: + if (DEBUG) console.log("UNIMPLEMENTED glLightf", i, GL_Symbol(pname), param); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glLightf " + GL_Symbol(pname)); + } + }, + + glLightfv: function(light, pname, param) { + if (gl.listMode && this.addToList("glLightfv", [light, pname, param])) return; + var i = light - GL.LIGHT0; + switch (pname) { + case GL.AMBIENT: + DEBUG > 1 && console.log("glLightfv", i, "GL_AMBIENT", Array.from(param)); + gl.lights[i].ambient = param; + break; + case GL.DIFFUSE: + DEBUG > 1 && console.log("glLightfv", i, "GL_DIFFUSE", Array.from(param)); + gl.lights[i].diffuse = param; + break; + case GL.SPECULAR: + DEBUG > 1 && console.log("glLightfv", i, "GL_SPECULAR", Array.from(param)); + gl.lights[i].specular = param; + break; + case GL.POSITION: + if (param[3] === 0) { + transformDirection(gl.matrices[GL.MODELVIEW][0], param, gl.lights[i].position); + } else { + transformPoint(gl.matrices[GL.MODELVIEW][0], param, gl.lights[i].position); + } + DEBUG > 1 && console.log("glLightfv", i, "GL_POSITION", Array.from(param), "=>", Array.from(gl.lights[i].position)); + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glLightfv", i, GL_Symbol(pname), Array.from(param)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glLightfv " + GL_Symbol(pname)); + } + }, + + glLightModelfv: function(pname, params) { + if (gl.listMode && this.addToList("glLightModelfv", [pname, params])) return; + switch (pname) { + case GL.LIGHT_MODEL_AMBIENT: + DEBUG > 1 && console.log("glLightModelfv GL_LIGHT_MODEL_AMBIENT", Array.from(params)); + gl.lightModelAmbient = params; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glLightModelfv", GL_Symbol(pname), Array.from(params)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glLightModelfv " + GL_Symbol(pname)); + } + }, + + glLineWidth: function(width) { + if (gl.listMode && this.addToList("glLineWidth", [width])) return; + DEBUG > 1 && console.log("glLineWidth", width); + webgl.lineWidth(width); + }, + + glListBase: function(base) { + DEBUG > 1 && console.log("glListBase", base); + gl.listBase = base; + }, + + glLoadIdentity: function() { + if (gl.listMode && this.addToList("glLoadIdentity", [])) return; + DEBUG > 1 && console.log("glLoadIdentity"); + gl.matrix.set(identity); + }, + + glLoadMatrixf: function(m) { + if (gl.listMode && this.addToList("glLoadMatrixf", [m])) return; + gl.matrix.set(m); + DEBUG > 1 && console.log("glLoadMatrixf", GL_Symbol(gl.matrixMode), Array.from(m)); + }, + + glLoadTransposeMatrixf: function(m) { + if (gl.listMode && this.addToList("glLoadTransposeMatrixf", [m])) return; + var t = new Float32Array(m); + transposeMatrix(t); + gl.matrix.set(t); + DEBUG > 1 && console.log("glLoadTransposeMatrixf", GL_Symbol(gl.matrixMode), Array.from(m)); + }, + + glMaterialf: function(face, pname, param) { + if (gl.listMode && this.addToList("glMaterialf", [face, pname, param])) return; + switch (pname) { + case GL.SHININESS: + DEBUG > 1 && console.log("glMaterialf GL_SHININESS", param); + gl.material.shininess = param; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glMaterialf", GL_Symbol(pname), param); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glMaterialf " + GL_Symbol(pname)); + } + }, + + glMaterialfv: function(face, pname, param) { + if (gl.listMode && this.addToList("glMaterialfv", [face, pname, param])) return; + switch (pname) { + case GL.AMBIENT: + DEBUG > 1 && console.log("glMaterialfv GL_AMBIENT", Array.from(param)); + gl.material.ambient = param; + break; + case GL.DIFFUSE: + DEBUG > 1 && console.log("glMaterialfv GL_DIFFUSE", Array.from(param)); + gl.material.diffuse = param; + break; + case GL.SPECULAR: + DEBUG > 1 && console.log("glMaterialfv GL_SPECULAR", Array.from(param)); + gl.material.specular = param; + break; + case GL.EMISSION: + DEBUG > 1 && console.log("glMaterialfv GL_EMISSION", Array.from(param)); + gl.material.emission = param; + break; + case GL.SHININESS: + DEBUG > 1 && console.log("glMaterialfv GL_SHININESS", Array.from(param)); + gl.material.shininess = param[0]; + break; + case GL.AMBIENT_AND_DIFFUSE: + DEBUG > 1 && console.log("glMaterialfv GL_AMBIENT_AND_DIFFUSE", Array.from(param)); + gl.material.ambient = param; + gl.material.diffuse = param; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glMaterialfv", GL_Symbol(pname), Array.from(param)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glMaterialfv " + GL_Symbol(pname)); + } + }, + + glMatrixMode: function(mode) { + if (gl.listMode && this.addToList("glMatrixMode", [mode])) return; + if (mode !== GL.MODELVIEW && mode !== GL.PROJECTION && mode !== GL.TEXTURE) { + if (DEBUG) console.warn("UNIMPLEMENTED glMatrixMode", GL_Symbol(mode)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glMatrixMode " + GL_Symbol(mode)); + } else DEBUG > 1 && console.log("glMatrixMode", GL_Symbol(mode)); + gl.matrixMode = mode; + if (!gl.matrices[mode]) gl.matrices[mode] = [new Float32Array(identity)]; + gl.matrix = gl.matrices[mode][0]; + }, + + glMultMatrixf: function(m) { + if (gl.listMode && this.addToList("glMultMatrixf", [m])) return; + multMatrix(gl.matrix, m); + DEBUG > 1 && console.log("glMultMatrixf", GL_Symbol(gl.matrixMode), Array.from(m), "=>", Array.from(gl.matrix)); + }, + + glMultTransposeMatrixf: function(m) { + if (gl.listMode && this.addToList("glMultTransposeMatrixf", [m])) return; + var t = new Float32Array(m); + transposeMatrix(t); + multMatrix(gl.matrix, t); + DEBUG > 1 && console.log("glMultTransposeMatrixf", GL_Symbol(gl.matrixMode), Array.from(m), "=>", Array.from(gl.matrix)); + }, + + glNewList: function(list, mode) { + DEBUG > 1 && console.log("glNewList", list, GL_Symbol(mode)); + var newList = { + id: list, + commands: [], + }; + gl.list = newList; + gl.listMode = mode; + }, + + glNormal3f: function(nx, ny, nz) { + if (gl.listMode && this.addToList("glNormal3f", [nx, ny, nz])) return; + DEBUG > 1 && console.log("glNormal3f", nx, ny, nz); + gl.normal[0] = nx; + gl.normal[1] = ny; + gl.normal[2] = nz; + gl.primitiveAttrs |= HAS_NORMAL; + }, + + glNormal3fv: function(v) { + if (gl.listMode && this.addToList("glNormal3fv", [v.slice()])) return; + DEBUG > 1 && console.log("glNormal3fv", Array.from(v)); + gl.normal.set(v); + gl.primitiveAttrs |= HAS_NORMAL; + }, + + glNormalPointer: function(type, stride, pointer) { + if (gl.listMode && this.addToList("glNormalPointer", [type, stride, pointer])) return; + DEBUG > 1 && console.log("glNormalPointer", GL_Symbol(type), stride, pointer); + gl.clientState.normalArray.size = 3; + gl.clientState.normalArray.type = type; + gl.clientState.normalArray.stride = stride; + gl.clientState.normalArray.pointer = pointer; + }, + + glPixelStorei: function(pname, param) { + switch (pname) { + case webgl.UNPACK_ALIGNMENT: + DEBUG > 1 && console.log("glPixelStorei GL_UNPACK_ALIGNMENT", param); + //@webgl.pixelStorei(webgl.UNPACK_ALIGNMENT, param); + break; + case GL.UNPACK_LSB_FIRST: + if (param !== 0) console.log("UNIMPLEMENTED glPixelStorei GL_UNPACK_LSB_FIRST", param); + break; + case GL.UNPACK_ROW_LENGTH: + DEBUG > 1 && console.log("glPixelStorei GL_UNPACK_ROW_LENGTH", param); + gl.pixelStoreUnpackRowLength = param; + break; + case GL.UNPACK_SKIP_ROWS: + DEBUG > 1 && console.log("glPixelStorei GL_UNPACK_SKIP_ROWS", param); + gl.pixelStoreUnpackSkipRows = param; + break; + case GL.UNPACK_SKIP_PIXELS: + DEBUG > 1 && console.log("glPixelStorei GL_UNPACK_SKIP_PIXELS", param); + gl.pixelStoreUnpackSkipPixels = param; + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glPixelStorei", pname, param); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glPixelStorei " + pname); + } + }, + + glPolygonOffset: function(factor, units) { + if (gl.listMode && this.addToList("glPolygonOffset", [factor, units])) return; + DEBUG > 1 && console.log("glPolygonOffset", factor, units); + webgl.polygonOffset(factor, units); + }, + + glPushAttrib: function(mask) { + if (gl.listMode && this.addToList("glPushAttrib", [mask])) return; + if (DEBUG > 0) { + var maskString = ''; + if (mask === GL.ALL_ATTRIB_BITS) { + maskString = 'GL_ALL_ATTRIB_BITS'; + } else { + if (mask & GL.CURRENT_BIT) { maskString += 'GL_CURRENT_BIT '; mask &= ~GL.CURRENT_BIT; } + if (mask & GL.ENABLE_BIT) { maskString += 'GL_ENABLE_BIT '; mask &= ~GL.ENABLE_BIT; } + if (mask & GL.LIGHTING_BIT) { maskString += 'GL_LIGHTING_BIT '; mask &= ~GL.LIGHTING_BIT; } + if (mask & GL.COLOR_BUFFER_BIT) { maskString += 'GL_COLOR_BUFFER_BIT '; mask &= ~GL.COLOR_BUFFER_BIT; } + if (mask & GL.DEPTH_BUFFER_BIT) { maskString += 'GL_DEPTH_BUFFER_BIT '; mask &= ~GL.DEPTH_BUFFER_BIT; } + if (mask & GL.POLYGON_BIT) { maskString += 'GL_POLYGON_BIT '; mask &= ~GL.POLYGON_BIT; } + if (mask & GL.TEXTURE_BIT) { maskString += 'GL_TEXTURE_BIT '; mask &= ~GL.TEXTURE_BIT; } + if (mask) for (var i = 0; i < 32; i++) { + if (mask & (1 << i)) maskString += " " + (1 << i); + } + } + console.log("UNIMPLEMENTED glPushAttrib", maskString); + } else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glPushAttrib"); + }, + + glPushMatrix: function() { + if (gl.listMode && this.addToList("glPushMatrix", [])) return; + gl.matrix = new Float32Array(gl.matrix); + gl.matrices[gl.matrixMode].unshift(gl.matrix); + DEBUG > 1 && console.log("glPushMatrix", GL_Symbol(gl.matrixMode), "=>", Array.from(gl.matrix)); + }, + + glPointSize: function(size) { + if (gl.listMode && this.addToList("glPointSize", [size])) return; + DEBUG > 1 && console.log("glPointSize", size); + gl.pointSize = size; + }, + + glPopAttrib: function() { + if (gl.listMode && this.addToList("glPopAttrib", [])) return; + if (DEBUG) console.log("UNIMPLEMENTED glPopAttrib"); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glPopAttrib"); + }, + + glPopMatrix: function() { + if (gl.listMode && this.addToList("glPopMatrix", [])) return; + if (gl.matrices[gl.matrixMode].length <= 1) + return DEBUG > 0 && console.warn("OpenGL: matrix stack underflow"); + gl.matrices[gl.matrixMode].shift(); + gl.matrix = gl.matrices[gl.matrixMode][0]; + DEBUG > 1 && console.log("glPopMatrix", GL_Symbol(gl.matrixMode), "=>", Array.from(gl.matrix)); + }, + + glRasterPos3f: function(x, y, z) { + if (gl.listMode && this.addToList("glRasterPos3f", [x, y, z])) return; + DEBUG > 1 && console.log("glRasterPos3f", x, y, z); + gl.rasterPos[0] = x; + gl.rasterPos[1] = y; + gl.rasterPos[2] = z; + gl.rasterPos[3] = 1; + // transform point via modelview and projection matrices + var m = gl.matrices[GL.PROJECTION][0]; + transformPoint(m, gl.rasterPos, gl.rasterPos); + m = gl.matrices[GL.MODELVIEW][0]; + transformPoint(m, gl.rasterPos, gl.rasterPos); + // transform to window coordinates + gl.rasterPos[0] = (gl.rasterPos[0] * 0.5 + 0.5) * gl.viewport[2] + gl.viewport[0]; + gl.rasterPos[1] = (gl.rasterPos[1] * 0.5 + 0.5) * gl.viewport[3] + gl.viewport[1]; + gl.rasterPos[2] = (gl.rasterPos[2] * 0.5 + 0.5) * (gl.depthRange[1] - gl.depthRange[0]) + gl.depthRange[0]; + // remember raster color + gl.rasterColor.set(gl.color); + }, + + glReadPixels: function(x, y, width, height, format, type, pixels) { + if (gl.listMode && this.addToList("glReadPixels", [x, y, width, height, format, type, pixels])) return; + var swizzle = false; + switch (format) { + case webgl.RGBA: + break; + case GL.BGRA: + format = webgl.RGBA; + swizzle = true; + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glReadPixels format " + GL_Symbol(format)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glReadPixels format " + GL_Symbol(format)); + return; + } + switch (type) { + case webgl.UNSIGNED_BYTE: + pixels = new Uint8Array(pixels); + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glReadPixels type " + GL_Symbol(type)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glReadPixels type " + GL_Symbol(type)); + return; + } + DEBUG > 1 && console.log("glReadPixels", x, y, width, height, GL_Symbol(format), GL_Symbol(type), pixels); + webgl.readPixels(x, y, width, height, format, type, pixels); + if (swizzle) { + for (var i = 0; i < pixels.length; i += 4) { + var r = pixels[i]; + var b = pixels[i+2]; + pixels[i] = b; + pixels[i+2] = r; + } + } + }, + + glTranslated: function(x, y, z) { + if (gl.listMode && this.addToList("glTranslated", [x, y, z])) return; + translateMatrix(gl.matrix, x, y, z); + DEBUG > 1 && console.log("glTranslated", GL_Symbol(gl.matrixMode), x, y, z, "=>", Array.from(gl.matrix)); + }, + + glTranslatef: function(x, y, z) { + if (gl.listMode && this.addToList("glTranslatef", [x, y, z])) return; + translateMatrix(gl.matrix, x, y, z); + DEBUG > 1 && console.log("glTranslatef", GL_Symbol(gl.matrixMode), x, y, z, "=>", Array.from(gl.matrix)); + }, + + glRotatef: function(angle, x, y, z) { + if (gl.listMode && this.addToList("glRotatef", [angle, x, y, z])) return; + rotateMatrix(gl.matrix, angle, x, y, z); + DEBUG > 1 && console.log("glRotatef", GL_Symbol(gl.matrixMode), angle, x, y, z, "=>", Array.from(gl.matrix)); + }, + + glScalef: function(x, y, z) { + if (gl.listMode && this.addToList("glScalef", [x, y, z])) return; + scaleMatrix(gl.matrix, x, y, z); + DEBUG > 1 && console.log("glScalef", GL_Symbol(gl.matrixMode), x, y, z, "=>", Array.from(gl.matrix)); + }, + + glScaled: function(x, y, z) { + if (gl.listMode && this.addToList("glScaled", [x, y, z])) return; + scaleMatrix(gl.matrix, x, y, z); + DEBUG > 1 && console.log("glScaled", GL_Symbol(gl.matrixMode), x, y, z, "=>", Array.from(gl.matrix)); + }, + + glShadeModel: function(mode) { + if (gl.listMode && this.addToList("glShadeModel", [mode])) return; + if (DEBUG) console.log("UNIMPLEMENTED glShadeModel", GL_Symbol(mode)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glShadeModel " + GL_Symbol(mode)); + }, + + glStencilFunc: function(func, ref, mask) { + if (gl.listMode && this.addToList("glStencilFunc", [func, ref, mask])) return; + DEBUG > 1 && console.log("glStencilFunc", GL_Symbol(func), ref, '0x'+(mask>>>0).toString(16)); + webgl.stencilFunc(func, ref, mask); + }, + + glStencilOp: function(fail, zfail, zpass) { + if (gl.listMode && this.addToList("glStencilOp", [fail, zfail, zpass])) return; + DEBUG > 1 && console.log("glStencilOp", GL_Symbol(fail), GL_Symbol(zfail), GL_Symbol(zpass)); + webgl.stencilOp(fail, zfail, zpass); + }, + + glTexEnv: function(target, pname, param) { + if (gl.listMode && this.addToList("glTexEnv", [target, pname, param])) return; + switch (pname) { + case GL.TEXTURE_ENV_MODE: + switch (param) { + case GL.MODULATE: + // Modulate means multiply the texture color with the fragment color + // which is what our fragment shader does by default + DEBUG > 1 && console.log("glTexEnv", GL_Symbol(target), "GL_TEXTURE_ENV_MODE", GL_Symbol(param)); + gl.textureEnvMode = param; + return; + case GL.REPLACE: + // Replace means use the texture color as the fragment color + // which we emulate by forcing the uniform color to white + DEBUG > 1 && console.log("glTexEnv", GL_Symbol(target), "GL_TEXTURE_ENV_MODE", GL_Symbol(param)); + gl.textureEnvMode = param; + return; + default: + if (DEBUG) console.log("UNIMPLEMENTED glTexEnv", GL_Symbol(target), "GL_TEXTURE_ENV_MODE", GL_Symbol(param)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexEnv " + GL_Symbol(target) + " GL_TEXTURE_ENV_MODE " + GL_Symbol(param)); + } + break; + default: + if (DEBUG) console.log("UNIMPLEMENTED glTexEnv", GL_Symbol(target), GL_Symbol(pname), GL_Symbol(param)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexEnv " + GL_Symbol(target) + " " + GL_Symbol(pname)); + } + }, + + glTexEnvi: function(target, pname, param) { + this.glTexEnv(target, pname, param); + }, + + glTexEnvf: function(target, pname, param) { + this.glTexEnv(target, pname, param); + }, + + glTexGen: function(coord, pname, param) { + if (gl.listMode && this.addToList("glTexGen", [coord, pname, param])) return; + if (DEBUG) console.log("UNIMPLEMENTED glTexGen", GL_Symbol(coord, "S"), GL_Symbol(pname), GL_Symbol(param)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexGen " + GL_Symbol(pname)); + }, + + glTexGeni: function(coord, pname, param) { + this.glTexGen(coord, pname, param); + }, + + glTexGenf: function(coord, pname, param) { + this.glTexGen(coord, pname, param); + }, + + glTexGend: function(coord, pname, params) { + this.glTexGen(coord, pname, param); + }, + + glTexGenfv: function(coord, pname, params) { + if (gl.listMode && this.addToList("glTexGenfv", [coord, pname, params])) return; + if (DEBUG) console.log("UNIMPLEMENTED glTexGenfv", GL_Symbol(coord, "S"), GL_Symbol(pname), Array.from(params)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexGenfv " + GL_Symbol(pname)); + }, + + glTexImage2D: function(target, level, internalformat, width, height, border, format, type, pixels) { + if (gl.listMode && this.addToList("glTexImage2D", [target, level, internalformat, width, height, border, format, type, pixels])) return; + DEBUG > 1 && console.log("glTexImage2D", GL_Symbol(target), level, GL_Symbol(internalformat), width, height, border, GL_Symbol(format), GL_Symbol(type), pixels); + gl.texture.width = width; + gl.texture.height = height; + // WebGL does not support GL_UNPACK_ROW_LENGTH, GL_UNPACK_SKIP_ROWS, GL_UNPACK_SKIP_PIXELS + if (gl.pixelStoreUnpackRowLength !== 0 && gl.pixelStoreUnpackRowLength !== gl.texture.width) { + if (DEBUG) console.warn("UNIMPLEMENTED glTexImage2D GL_UNPACK_ROW_LENGTH " + gl.pixelStoreUnpackRowLength); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D GL_UNPACK_ROW_LENGTH"); + } + if (gl.pixelStoreUnpackSkipRows !== 0) { + if (DEBUG) console.warn("UNIMPLEMENTED glTexImage2D GL_UNPACK_SKIP_ROWS " + gl.pixelStoreUnpackSkipRows); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D GL_UNPACK_SKIP_ROWS"); + } + if (gl.pixelStoreUnpackSkipPixels !== 0) { + if (DEBUG) console.warn("UNIMPLEMENTED glTexImage2D GL_UNPACK_SKIP_PIXELS " + gl.pixelStoreUnpackSkipPixels); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D GL_UNPACK_SKIP_PIXELS"); + } + // WebGL only supports GL_RGBA + switch (format) { + case webgl.RGBA: + if (DEBUG) console.warn("glTexImage2D GL_RGBA: need to not swizzle BGRA"); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D format GL_RGBA"); + break; + case GL.BGRA: + format = webgl.RGBA; + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glTexImage2D format " + GL_Symbol(format)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D format " + GL_Symbol(format)); + return; + } + // pixels are coming in via FFI as void* (ArrayBuffer) + // convert to appropriate typed array + // in OpenGL, pixels can be null to allocate texture storage + if (!pixels) pixels = new ArrayBuffer(width * height * 4); + switch (type) { + case webgl.UNSIGNED_BYTE: + pixels = new Uint8Array(pixels); + gl.texture.pixels = pixels; // for debugging + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glTexImage2D type " + GL_Symbol(type)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexImage2D type " + GL_Symbol(type)); + return; + } + webgl.texImage2D(target, level, internalformat, width, height, border, format, type, pixels); + if (gl.texture.generateMipmapSGIS) webgl.generateMipmap(target); + }, + + debugTexture: function(texture) { + if (texture === false) { + var canvas = document.getElementById("texDebug"); + if (canvas) canvas.remove(); + return; + } + if (!texture) texture = gl.texture; + var pixels = texture.pixels; + var width = texture.width; + var height = texture.height; + var data = new Uint8Array(pixels); + var canvas = document.getElementById("texDebug"); + if (!canvas) { + canvas = document.createElement('canvas'); + canvas.id = "texDebug"; + canvas.style.position = "absolute"; + canvas.style.zIndex = 1000; + document.body.appendChild(canvas); + } + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + var imageData = ctx.createImageData(width, height); + imageData.data.set(data); + ctx.putImageData(imageData, 0, 0); + }, + + glTexCoord2d: function(s, t) { + if (gl.listMode && this.addToList("glTexCoord2d", [s, t])) return; + DEBUG > 1 && console.log("glTexCoord2d", s, t); + gl.texCoord[0] = s; + gl.texCoord[1] = t; + gl.primitiveAttrs |= HAS_TEXCOORD; + }, + + glTexCoord2f: function(s, t) { + if (gl.listMode && this.addToList("glTexCoord2f", [s, t])) return; + DEBUG > 1 && console.log("glTexCoord2f", s, t); + gl.texCoord[0] = s; + gl.texCoord[1] = t; + gl.primitiveAttrs |= HAS_TEXCOORD; + }, + + glTexCoord2fv: function(v) { + if (gl.listMode && this.addToList("glTexCoord2fv", [v.slice()])) return; + DEBUG > 1 && console.log("glTexCoord2fv", Array.from(v)); + gl.texCoord.set(v); + gl.primitiveAttrs |= HAS_TEXCOORD; + }, + + glTexCoordPointer: function(size, type, stride, pointer) { + if (gl.listMode && this.addToList("glTexCoordPointer", [size, type, stride, pointer])) return; + DEBUG > 1 && console.log("glTexCoordPointer", size, GL_Symbol(type), stride, pointer); + gl.clientState.textureCoordArray.size = size; + gl.clientState.textureCoordArray.type = type; + gl.clientState.textureCoordArray.stride = stride; + gl.clientState.textureCoordArray.pointer = pointer; + }, + + glTexParameteri: function(target, pname, param) { + if (gl.listMode && this.addToList("glTexParameteri", [target, pname, param])) return; + DEBUG > 1 && console.log("glTexParameteri", GL_Symbol(target), GL_Symbol(pname), GL_Symbol(param)); + if (pname === GL.GENERATE_MIPMAP_SGIS) { + // WebGL does not support GL_GENERATE_MIPMAP_SGIS. Emulate it + gl.texture.generateMipmapSGIS = param; + // if an image has been uploaded already, generate mipmaps now + if (param && gl.texture.width > 1) webgl.generateMipmap(target); + return; + } + webgl.texParameteri(target, pname, param); + }, + + glTexSubImage2D: function(target, level, xoffset, yoffset, width, height, format, type, pixels) { + if (gl.listMode && this.addToList("glTexSubImage2D", [target, level, xoffset, yoffset, width, height, format, type, pixels])) return; + DEBUG > 1 && console.log("glTexSubImage2D", GL_Symbol(target), level, xoffset, yoffset, width, height, GL_Symbol(format), GL_Symbol(type), pixels); + // WebGL does not support GL_UNPACK_ROW_LENGTH, GL_UNPACK_SKIP_ROWS, GL_UNPACK_SKIP_PIXELS + // emulate GL_UNPACK_SKIP_ROWS + var pixelsOffset = gl.pixelStoreUnpackSkipRows * gl.texture.width; // to be multiplied by pixel size below + // assume GL_UNPACK_ROW_LENGTH is full width (which is the case when uploading part of a bitmap in Squeak) + if (gl.pixelStoreUnpackRowLength !== 0 && gl.pixelStoreUnpackRowLength !== gl.texture.width) { + if (DEBUG) console.warn("UNIMPLEMENTED glTexSubImage2D GL_UNPACK_ROW_LENGTH " + gl.pixelStoreUnpackRowLength); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexSubImage2D GL_UNPACK_ROW_LENGTH"); + } + // WebGL does not support GL_UNPACK_SKIP_PIXELS to allow different width + if (width !== gl.texture.width) { + // we could either + // 1. call texSubImage2D for each row + // 2. copy subimage pixels into a new buffer + // 3. call texSubImage2D for the whole width so we don't need to skip pixels + // we choose 3. for now + width = gl.texture.width; + xoffset = 0; + } + // WebGL only supports RGB not BGR + switch (format) { + case webgl.RGBA: + pixelsOffset *= 4; + break; + case GL.BGRA: + pixelsOffset *= 4; + format = webgl.RGBA; + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glTexSubImage2D format " + GL_Symbol(format)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexSubImage2D format " + GL_Symbol(format)); + return; + } + // pixels are coming in via FFI as void* (ArrayBuffer) + // convert to appropriate typed array + switch (type) { + case webgl.UNSIGNED_BYTE: + pixels = new Uint8Array(pixels, pixelsOffset); + break; + default: + if (DEBUG) console.warn("UNIMPLEMENTED glTexSubImage2D type " + GL_Symbol(type)); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED glTexSubImage2D type " + GL_Symbol(type)); + return; + } + webgl.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + if (gl.texture.generateMipmapSGIS) webgl.generateMipmap(target); + }, + + glVertex2f: function(x, y) { + if (gl.listMode && this.addToList("glVertex2f", [x, y])) return; + DEBUG > 1 && console.log("glVertex2f", x, y); + var position = [x, y]; + this.pushVertex(position); + }, + + glVertex3f: function(x, y, z) { + if (gl.listMode && this.addToList("glVertex3f", [x, y, z])) return; + DEBUG > 1 && console.log("glVertex3f", x, y, z); + var position = [x, y, z]; + this.pushVertex(position); + }, + + glVertex3fv: function(v) { + if (gl.listMode && this.addToList("glVertex3fv", [v.slice()])) return; + DEBUG > 1 && console.log("glVertex3fv", Array.from(v)); + this.pushVertex(v); + }, + + glVertex2i: function(x, y) { + if (gl.listMode && this.addToList("glVertex2i", [x, y])) return; + DEBUG > 1 && console.log("glVertex2i", x, y); + var position = [x, y]; + this.pushVertex(position); + }, + + glVertexPointer: function(size, type, stride, pointer) { + if (gl.listMode && this.addToList("glVertexPointer", [size, type, stride, pointer])) return; + DEBUG > 1 && console.log("glVertexPointer", size, GL_Symbol(type), stride, pointer); + gl.clientState.vertexArray.size = size; + gl.clientState.vertexArray.type = type; + gl.clientState.vertexArray.stride = stride; + gl.clientState.vertexArray.pointer = pointer; + }, + + glViewport: function(x, y, width, height) { + if (gl.listMode && this.addToList("glViewport", [x, y, width, height])) return; + DEBUG > 1 && console.log("glViewport", x, y, width, height); + webgl.viewport(x, y, width, height); + gl.viewport[0] = x; + gl.viewport[1] = y; + gl.viewport[2] = width; + gl.viewport[3] = height; + }, + + glXGetProcAddressARB: function(procName) { + procName = Squeak.bytesAsString(procName); + DEBUG > 1 && console.log("glXGetProcAddressARB", procName); + var handle = this.ffi.ffiLookupFunc(this, procName); + if (!handle) { + if (DEBUG) console.warn("UNIMPLEMENTED EXT FUNC", procName); + else this.vm.warnOnce("OpenGL: UNIMPLEMENTED EXT FUNC" + procName); + } + return handle; + }, + + pushVertex: function(position) { + var primitive = gl.primitive; + if (!primitive) throw Error("OpenGL: glBegin not called"); + if (!primitive.vertexSize) { + var vertexSize = 3; + if (gl.primitiveAttrs & HAS_NORMAL) vertexSize += 3; + if (gl.primitiveAttrs & HAS_COLOR) vertexSize += 4; + if (gl.primitiveAttrs & HAS_TEXCOORD) vertexSize += 2; + primitive.vertexSize = vertexSize; + primitive.vertexAttrs = gl.primitiveAttrs; + } + var vertex = new Float32Array(primitive.vertexSize); + var offset = 0; + vertex.set(position, offset); offset += 3; + if (primitive.vertexAttrs & HAS_NORMAL) { + vertex.set(gl.normal, offset); offset += 3; + } + if (primitive.vertexAttrs & HAS_COLOR) { + vertex.set(gl.color, offset); offset += 4; + } + if (primitive.vertexAttrs & HAS_TEXCOORD) { + vertex.set(gl.texCoord, offset); offset += 2; + } + primitive.vertices.push(vertex); + }, + + // Shader source code + + // Note: we could take a look at Emscripten's glemu to add some more features + // https://github.com/emscripten-core/emscripten/blob/cb99414efed02dc61d04315d3e3cf5ad3180e56f/src/library_glemu.js#L2170 + // The structure is a bit different but applicable + + vertexShaderSource: function(shaderFlags) { + var src = []; + src.push("uniform mat4 uModelView;"); + src.push("uniform mat4 uProjection;"); + src.push("attribute vec3 aPosition;"); + if (shaderFlags & HAS_COLOR) { + src.push("attribute vec4 aColor;"); + } else if (shaderFlags & ANY_LIGHTS) { + src.push("uniform vec4 uColor;"); + } + if (shaderFlags & (HAS_COLOR | ANY_LIGHTS)) { + src.push("varying vec4 vColor;"); + } + if (shaderFlags & HAS_TEXCOORD && shaderFlags & USE_TEXTURE) { + src.push("uniform mat4 uTextureMatrix;"); + src.push("attribute vec2 aTexCoord;"); + src.push("varying vec2 vTexCoord;"); + } + if (shaderFlags & USE_POINT_SIZE) { + src.push("uniform float uPointSize;"); + } + var numLights = (shaderFlags & NUM_LIGHTS_MASK) >> NUM_LIGHTS_SHIFT; + if (numLights > 0) { + if (shaderFlags & HAS_NORMAL) { + src.push("attribute vec3 aNormal;"); + } else { + src.push("uniform vec3 uNormal;"); + } + src.push("uniform mat3 uNormalMatrix;"); + src.push("uniform vec4 uLightModelAmbient;"); + src.push("uniform vec4 uMaterialAmbient;"); + src.push("uniform vec4 uMaterialDiffuse;"); + src.push("uniform vec4 uMaterialSpecular;"); + src.push("uniform vec4 uMaterialEmission;"); + src.push("uniform float uMaterialShininess;"); + src.push("struct Light {"); + src.push(" vec4 position;"); + src.push(" vec4 ambient;"); + src.push(" vec4 diffuse;"); + src.push(" vec4 specular;"); + src.push("};"); + src.push("uniform Light uLights[" + numLights + "];"); + } + var numClipPlanes = (shaderFlags & NUM_CLIP_PLANES_MASK) >> NUM_CLIP_PLANES_SHIFT; + if (numClipPlanes > 0) { + src.push("uniform vec4 uClipPlanes[" + numClipPlanes + "];"); + src.push("varying float vClipDist[" + numClipPlanes + "];"); + } + var fog = (shaderFlags & FOG_MASK) >> FOG_SHIFT; + if (fog !== NO_FOG) { + if (fog === LINEAR_FOG) { + src.push("uniform float uFogEnd;"); + src.push("uniform float uFogRange;"); + } else { + src.push("uniform float uFogDensity;"); + } + src.push("varying float vFogDist;"); + } + + src.push("void main(void) {"); + if (shaderFlags & HAS_COLOR) { + src.push(" vColor = aColor;"); + } else if (shaderFlags & ANY_LIGHTS) { + src.push(" vColor = uColor;"); + } + src.push(" vec4 position = uModelView * vec4(aPosition, 1.0);"); + if (numLights > 0) { + if (shaderFlags & HAS_NORMAL) { + src.push(" vec3 normal = normalize(uNormalMatrix * aNormal);"); + } else { + src.push(" vec3 normal = normalize(uNormalMatrix * uNormal);"); + } + src.push(" vec4 lighting = uMaterialEmission;"); + src.push(" lighting += uMaterialAmbient * uLightModelAmbient;"); + src.push(" vec3 eyeDir = normalize(-position.xyz);"); + src.push(" for (int i = 0; i < " + numLights + "; i++) {"); + src.push(" Light light = uLights[i];"); + src.push(" vec3 lightDir;"); + src.push(" if (light.position.w == 0.0) {"); + src.push(" lightDir = normalize(light.position.xyz);"); + src.push(" } else {"); + src.push(" lightDir = normalize(light.position.xyz - position.xyz);"); + src.push(" }"); + src.push(" float nDotL = max(dot(normal, lightDir), 0.0);"); + src.push(" vec4 ambient = uMaterialAmbient * light.ambient;"); + src.push(" vec4 diffuse = uMaterialDiffuse * light.diffuse * nDotL;"); + src.push(" vec4 specular = vec4(0.0);"); + src.push(" if (nDotL > 0.0) {"); + src.push(" vec3 halfVector = normalize(lightDir + eyeDir);"); + src.push(" float nDotHV = max(dot(normal, halfVector), 0.0);"); + src.push(" specular = uMaterialSpecular * light.specular * pow(nDotHV, uMaterialShininess);"); + src.push(" }"); + src.push(" lighting += ambient + diffuse + specular;"); + src.push(" }"); + src.push(" vColor = clamp(lighting, 0.0, 1.0);"); + src.push(" vColor.a = uMaterialDiffuse.a;"); + } + if (numClipPlanes > 0) { + src.push(" for (int i = 0; i < " + numClipPlanes + "; i++) {"); + src.push(" vClipDist[i] = dot(position, uClipPlanes[i]);"); + src.push(" }"); + } + if (shaderFlags & HAS_TEXCOORD && shaderFlags & USE_TEXTURE) { + src.push(" vec4 texCoord = uTextureMatrix * vec4(aTexCoord, 0.0, 1.0);"); + src.push(" vTexCoord = texCoord.xy / texCoord.w;"); + } + if (fog !== NO_FOG) { + src.push(" vFogDist = -position.z;"); + } + if (shaderFlags & USE_POINT_SIZE) { + src.push(" gl_PointSize = uPointSize;"); + } + src.push(" gl_Position = uProjection * position;"); + src.push("}"); + var src = src.join("\n"); + DEBUG > 1 && console.log(src); + return src; + }, + + fragmentShaderSource: function(shaderFlags) { + var src = []; + src.push("precision mediump float;"); + var numClipPlanes = (shaderFlags & NUM_CLIP_PLANES_MASK) >> NUM_CLIP_PLANES_SHIFT; + if (numClipPlanes > 0) { + src.push("varying float vClipDist[" + numClipPlanes + "];"); + } + if (shaderFlags & (HAS_COLOR | ANY_LIGHTS)) { + src.push("varying vec4 vColor;"); + } else { + src.push("uniform vec4 uColor;"); + } + if (shaderFlags & USE_TEXTURE) { + if (shaderFlags & HAS_TEXCOORD) { + src.push("varying vec2 vTexCoord;"); + } else { + src.push("uniform vec2 uTexCoord;"); + } + src.push("uniform sampler2D uSampler;"); + } + var fog = (shaderFlags & FOG_MASK) >> FOG_SHIFT; + if (fog !== NO_FOG) { + if (fog === LINEAR_FOG) { + src.push("uniform float uFogEnd;"); + src.push("uniform float uFogRange;"); + } else { + src.push("uniform float uFogDensity;"); + } + src.push("uniform vec4 uFogColor;"); + src.push("varying float vFogDist;"); + } + src.push("void main(void) {"); + if (numClipPlanes > 0) { + src.push(" bool clipped = false;"); + src.push(" for (int i = 0; i < " + numClipPlanes + "; i++) {"); + src.push(" if (vClipDist[i] < 0.0) clipped = true;"); + src.push(" }"); + src.push(" if (clipped) discard;"); + } + if (shaderFlags & (HAS_COLOR | ANY_LIGHTS)) { + src.push(" vec4 color = vColor;"); + } else { + src.push(" vec4 color = uColor;"); + } + if (shaderFlags & USE_TEXTURE) { + if (shaderFlags & HAS_TEXCOORD) { + src.push(" vec2 texCoord = vTexCoord;"); + } else { + src.push(" vec2 texCoord = uTexCoord;"); + } + src.push(" color *= texture2D(uSampler, texCoord).bgra;"); + } + if (fog !== NO_FOG) { + switch (fog) { + case LINEAR_FOG: + src.push(" float fogAmount = (uFogEnd - vFogDist) / uFogRange;"); + break; + case EXP_FOG: + src.push(" float fogAmount = exp(-uFogDensity * vFogDist);"); + break; + case EXP2_FOG: + src.push(" float fogAmount = exp(-uFogDensity * uFogDensity * vFogDist * vFogDist);"); + break; + } + src.push(" color.rgb = mix(uFogColor.rgb, color.rgb, clamp(fogAmount, 0.0, 1.0));"); + } + src.push(" gl_FragColor = color;"); + src.push("}"); + var src = src.join("\n"); + DEBUG > 1 && console.log(src); + return src; + }, + + getLocations: function(program, shaderFlags) { + var locations = {}; + locations.uModelView = webgl.getUniformLocation(program, "uModelView"); + locations.uProjection = webgl.getUniformLocation(program, "uProjection"); + locations.aPosition = webgl.getAttribLocation(program, "aPosition"); + if (shaderFlags & USE_TEXTURE) { + if (shaderFlags & HAS_TEXCOORD) { + locations.uTextureMatrix = webgl.getUniformLocation(program, "uTextureMatrix"); + locations.aTexCoord = webgl.getAttribLocation(program, "aTexCoord"); + } else { + locations.uTexCoord = webgl.getUniformLocation(program, "uTexCoord"); + } + locations.uSampler = webgl.getUniformLocation(program, "uSampler"); + } + if (shaderFlags & HAS_COLOR) { + locations.aColor = webgl.getAttribLocation(program, "aColor"); + } else { + locations.uColor = webgl.getUniformLocation(program, "uColor"); + } + if (shaderFlags & USE_POINT_SIZE) { + locations.uPointSize = webgl.getUniformLocation(program, "uPointSize"); + } + var numLights = (shaderFlags & NUM_LIGHTS_MASK) >> NUM_LIGHTS_SHIFT; + if (numLights > 0) { + if (shaderFlags & HAS_NORMAL) { + locations.aNormal = webgl.getAttribLocation(program, "aNormal"); + } else{ + locations.uNormal = webgl.getUniformLocation(program, "uNormal"); + } + locations.uNormalMatrix = webgl.getUniformLocation(program, "uNormalMatrix"); + locations.uLightModelAmbient = webgl.getUniformLocation(program, "uLightModelAmbient"); + locations.uMaterialAmbient = webgl.getUniformLocation(program, "uMaterialAmbient"); + locations.uMaterialDiffuse = webgl.getUniformLocation(program, "uMaterialDiffuse"); + locations.uMaterialSpecular = webgl.getUniformLocation(program, "uMaterialSpecular"); + locations.uMaterialEmission = webgl.getUniformLocation(program, "uMaterialEmission"); + locations.uMaterialShininess = webgl.getUniformLocation(program, "uMaterialShininess"); + locations.uLights = []; + for (var i = 0; i < numLights; i++) { + var light = {}; + light.position = webgl.getUniformLocation(program, "uLights[" + i + "].position"); + light.ambient = webgl.getUniformLocation(program, "uLights[" + i + "].ambient"); + light.diffuse = webgl.getUniformLocation(program, "uLights[" + i + "].diffuse"); + light.specular = webgl.getUniformLocation(program, "uLights[" + i + "].specular"); + locations.uLights.push(light); + } + } + var numClipPlanes = (shaderFlags & NUM_CLIP_PLANES_MASK) >> NUM_CLIP_PLANES_SHIFT; + if (numClipPlanes > 0) { + locations.uClipPlanes = []; + for (var i = 0; i < numClipPlanes; i++) { + locations.uClipPlanes.push(webgl.getUniformLocation(program, "uClipPlanes[" + i + "]")); + } + } + var fog = (shaderFlags & FOG_MASK) >> FOG_SHIFT; + if (fog !== NO_FOG) { + if (fog === LINEAR_FOG) { + locations.uFogEnd = webgl.getUniformLocation(program, "uFogEnd"); + locations.uFogRange = webgl.getUniformLocation(program, "uFogRange"); + } else { + locations.uFogDensity = webgl.getUniformLocation(program, "uFogDensity"); + } + locations.uFogColor = webgl.getUniformLocation(program, "uFogColor"); + } + + DEBUG > 1 && console.log(locations); + return locations; + }, + + executeList: function(listId) { + var list = gl.lists[listId]; + if (!list) { + DEBUG > 0 && console.warn("OpenGL: display list not found " + listId); + return; + } + for (var i = 0; i < list.commands.length; i++) { + var cmd = list.commands[i]; + this[cmd.name].apply(this, cmd.args); + } + }, + }; +} + +function transformDirection(matrix, src, dst) { + var x = src[0]; + var y = src[1]; + var z = src[2]; + var rx = matrix[0] * x + matrix[4] * y + matrix[8] * z; + var ry = matrix[1] * x + matrix[5] * y + matrix[9] * z; + var rz = matrix[2] * x + matrix[6] * y + matrix[10] * z; + dst[0] = rx; + dst[1] = ry; + dst[2] = rz; + dst[3] = src[3]; +} + +function transformPoint(matrix, src, dst) { + var x = src[0]; + var y = src[1]; + var z = src[2]; + var rx = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]; + var ry = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]; + var rz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]; + var rw = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15]; + if (rw === 1) { + dst[0] = rx; + dst[1] = ry; + dst[2] = rz; + } else { + if (rw !== 0) rw = 1 / rw; + dst[0] = rx * rw; + dst[1] = ry * rw; + dst[2] = rz * rw; + } + dst[3] = src[3]; +} + +function multVec4(matrix, src, dst) { + var x = src[0]; + var y = src[1]; + var z = src[2]; + var w = src[3]; + dst[0] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12] * w; + dst[1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13] * w; + dst[2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14] * w; + dst[3] = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15] * w; +} + +function multMatrix(m1, m2) { + var m00 = m1[0] * m2[0] + m1[4] * m2[1] + m1[8] * m2[2] + m1[12] * m2[3]; + var m01 = m1[1] * m2[0] + m1[5] * m2[1] + m1[9] * m2[2] + m1[13] * m2[3]; + var m02 = m1[2] * m2[0] + m1[6] * m2[1] + m1[10] * m2[2] + m1[14] * m2[3]; + var m03 = m1[3] * m2[0] + m1[7] * m2[1] + m1[11] * m2[2] + m1[15] * m2[3]; + var m10 = m1[0] * m2[4] + m1[4] * m2[5] + m1[8] * m2[6] + m1[12] * m2[7]; + var m11 = m1[1] * m2[4] + m1[5] * m2[5] + m1[9] * m2[6] + m1[13] * m2[7]; + var m12 = m1[2] * m2[4] + m1[6] * m2[5] + m1[10] * m2[6] + m1[14] * m2[7]; + var m13 = m1[3] * m2[4] + m1[7] * m2[5] + m1[11] * m2[6] + m1[15] * m2[7]; + var m20 = m1[0] * m2[8] + m1[4] * m2[9] + m1[8] * m2[10] + m1[12] * m2[11]; + var m21 = m1[1] * m2[8] + m1[5] * m2[9] + m1[9] * m2[10] + m1[13] * m2[11]; + var m22 = m1[2] * m2[8] + m1[6] * m2[9] + m1[10] * m2[10] + m1[14] * m2[11]; + var m23 = m1[3] * m2[8] + m1[7] * m2[9] + m1[11] * m2[10] + m1[15] * m2[11]; + var m30 = m1[0] * m2[12] + m1[4] * m2[13] + m1[8] * m2[14] + m1[12] * m2[15]; + var m31 = m1[1] * m2[12] + m1[5] * m2[13] + m1[9] * m2[14] + m1[13] * m2[15]; + var m32 = m1[2] * m2[12] + m1[6] * m2[13] + m1[10] * m2[14] + m1[14] * m2[15]; + var m33 = m1[3] * m2[12] + m1[7] * m2[13] + m1[11] * m2[14] + m1[15] * m2[15]; + m1[0] = m00; m1[1] = m01; m1[2] = m02; m1[3] = m03; + m1[4] = m10; m1[5] = m11; m1[6] = m12; m1[7] = m13; + m1[8] = m20; m1[9] = m21; m1[10] = m22; m1[11] = m23; + m1[12] = m30; m1[13] = m31; m1[14] = m32; m1[15] = m33; +} + +function translateMatrix(m, x, y, z) { + m[12] += x * m[0] + y * m[4] + z * m[8]; + m[13] += x * m[1] + y * m[5] + z * m[9]; + m[14] += x * m[2] + y * m[6] + z * m[10]; + m[15] += x * m[3] + y * m[7] + z * m[11]; +} + +function rotateMatrix(m, angle, x, y, z) { + // normalize axis vector + var len = Math.sqrt(x * x + y * y + z * z); + if (len === 0) return; + if (len !== 1) { + len = 1 / len; + x *= len; + y *= len; + z *= len; + } + // create rotation matrix + var degrees = Math.PI / 180; + var c = Math.cos(angle * degrees); + var s = Math.sin(angle * degrees); + var t = 1 - c; + var m00 = x * x * t + c; + var m01 = x * y * t - z * s; + var m02 = x * z * t + y * s; + var m10 = y * x * t + z * s; + var m11 = y * y * t + c; + var m12 = y * z * t - x * s; + var m20 = z * x * t - y * s; + var m21 = z * y * t + x * s; + var m22 = z * z * t + c; + // multiply rotation matrix + var m0 = m[0] * m00 + m[4] * m01 + m[8] * m02; + var m1 = m[1] * m00 + m[5] * m01 + m[9] * m02; + var m2 = m[2] * m00 + m[6] * m01 + m[10] * m02; + var m3 = m[3] * m00 + m[7] * m01 + m[11] * m02; + var m4 = m[0] * m10 + m[4] * m11 + m[8] * m12; + var m5 = m[1] * m10 + m[5] * m11 + m[9] * m12; + var m6 = m[2] * m10 + m[6] * m11 + m[10] * m12; + var m7 = m[3] * m10 + m[7] * m11 + m[11] * m12; + m[8] = m[0] * m20 + m[4] * m21 + m[8] * m22; + m[9] = m[1] * m20 + m[5] * m21 + m[9] * m22; + m[10] = m[2] * m20 + m[6] * m21 + m[10] * m22; + m[11] = m[3] * m20 + m[7] * m21 + m[11] * m22; + m[0] = m0; m[1] = m1; m[2] = m2; m[3] = m3; + m[4] = m4; m[5] = m5; m[6] = m6; m[7] = m7; +} + +function scaleMatrix(m, x, y, z) { + m[0] *= x; m[1] *= x; m[2] *= x; m[3] *= x; + m[4] *= y; m[5] *= y; m[6] *= y; m[7] *= y; + m[8] *= z; m[9] *= z; m[10] *= z; m[11] *= z; +} + +function transposeMatrix(m) { + var m01 = m[1], m02 = m[2], m03 = m[3], + m12 = m[6], m13 = m[7], + m23 = m[11]; + m[1] = m[4]; m[2] = m[8]; m[3] = m[12]; + m[4] = m01; m[6] = m[9]; m[7] = m[13]; + m[8] = m02; m[9] = m12; m[11] = m[14]; + m[12] = m03; m[13] = m13; m[14] = m23; +} + +function invertMatrix(src, dst) { + if (!dst) dst = src; + + var m00 = src[0], m01 = src[1], m02 = src[2], m03 = src[3], + m10 = src[4], m11 = src[5], m12 = src[6], m13 = src[7], + m20 = src[8], m21 = src[9], m22 = src[10], m23 = src[11], + m30 = src[12], m31 = src[13], m32 = src[14], m33 = src[15]; + + var t00 = m00 * m11 - m01 * m10, + t01 = m00 * m12 - m02 * m10, + t02 = m00 * m13 - m03 * m10, + t03 = m01 * m12 - m02 * m11, + t04 = m01 * m13 - m03 * m11, + t05 = m02 * m13 - m03 * m12, + t06 = m20 * m31 - m21 * m30, + t07 = m20 * m32 - m22 * m30, + t08 = m20 * m33 - m23 * m30, + t09 = m21 * m32 - m22 * m31, + t10 = m21 * m33 - m23 * m31, + t11 = m22 * m33 - m23 * m32; + + var det = t00 * t11 - t01 * t10 + t02 * t09 + t03 * t08 - t04 * t07 + t05 * t06; + + if (det === 0) return; + + var invDet = 1 / det; + + dst[0] = (m11 * t11 - m12 * t10 + m13 * t09) * invDet; + dst[1] = (-m01 * t11 + m02 * t10 - m03 * t09) * invDet; + dst[2] = (m31 * t05 - m32 * t04 + m33 * t03) * invDet; + dst[3] = (-m21 * t05 + m22 * t04 - m23 * t03) * invDet; + dst[4] = (-m10 * t11 + m12 * t08 - m13 * t07) * invDet; + dst[5] = (m00 * t11 - m02 * t08 + m03 * t07) * invDet; + dst[6] = (-m30 * t05 + m32 * t02 - m33 * t01) * invDet; + dst[7] = (m20 * t05 - m22 * t02 + m23 * t01) * invDet; + dst[8] = (m10 * t10 - m11 * t08 + m13 * t06) * invDet; + dst[9] = (-m00 * t10 + m01 * t08 - m03 * t06) * invDet; + dst[10] = (m30 * t04 - m31 * t02 + m33 * t00) * invDet; + dst[11] = (-m20 * t04 + m21 * t02 - m23 * t00) * invDet; + dst[12] = (-m10 * t09 + m11 * t07 - m12 * t06) * invDet; + dst[13] = (m00 * t09 - m01 * t07 + m02 * t06) * invDet; + dst[14] = (-m30 * t03 + m31 * t01 - m32 * t00) * invDet; + dst[15] = (m20 * t03 - m21 * t01 + m22 * t00) * invDet; +} + +function asNormalMatrix(m) { + // inverse transpose of upper-left 3x3 matrix + var out = new Float32Array(9); + var m11 = m[0], m21 = m[1], m31 = m[2], + m12 = m[4], m22 = m[5], m32 = m[6], + m13 = m[8], m23 = m[9], m33 = m[10]; + var t11 = m33 * m22 - m32 * m23, + t12 = m32 * m13 - m33 * m12, + t13 = m23 * m12 - m22 * m13; + var det = m11 * t11 + m21 * t12 + m31 * t13; + if (det !== 0 ) { + const s = 1 / det; + // store in transposed order + out[0] = t11 * s; out[3] = (m31 * m23 - m33 * m21) * s; out[6] = (m32 * m21 - m31 * m22) * s; + out[1] = t12 * s; out[4] = (m33 * m11 - m31 * m13) * s; out[7] = (m31 * m12 - m32 * m11) * s; + out[2] = t13 * s; out[5] = (m21 * m13 - m23 * m11) * s; out[8] = (m22 * m11 - m21 * m12) * s; + } + return out; +} + +var GL_Symbols; // reverse mapping for debug printing +function GL_Symbol(constant, rangeStart) { + if (constant === undefined) { debugger; return /* should not happen */} + if (rangeStart !== undefined) { + // rangeStart is e.g. "FALSE" or "POINTS" which are both 0 + var all = Object.keys(GL); // we're relying on insertion order here + var start = all.indexOf(rangeStart); + return all[start + constant - GL[rangeStart]]; + } + else return GL_Symbols[constant] || constant; +} + +function initGLConstants() { + GL = { + DEPTH_BUFFER_BIT: 0x00000100, + STENCIL_BUFFER_BIT: 0x00000400, + COLOR_BUFFER_BIT: 0x00004000, + FALSE: 0, + TRUE: 1, + POINTS: 0x0000, + LINES: 0x0001, + LINE_LOOP: 0x0002, + LINE_STRIP: 0x0003, + TRIANGLES: 0x0004, + TRIANGLE_STRIP: 0x0005, + TRIANGLE_FAN: 0x0006, + QUADS: 0x0007, + QUAD_STRIP: 0x0008, + POLYGON: 0x0009, + NEVER: 0x0200, + LESS: 0x0201, + EQUAL: 0x0202, + LEQUAL: 0x0203, + GREATER: 0x0204, + NOTEQUAL: 0x0205, + GEQUAL: 0x0206, + ALWAYS: 0x0207, + SRC_COLOR: 0x0300, + ONE_MINUS_SRC_COLOR: 0x0301, + SRC_ALPHA: 0x0302, + ONE_MINUS_SRC_ALPHA: 0x0303, + DST_ALPHA: 0x0304, + ONE_MINUS_DST_ALPHA: 0x0305, + DST_COLOR: 0x0306, + ONE_MINUS_DST_COLOR: 0x0307, + SRC_ALPHA_SATURATE: 0x0308, + NONE: 0, + FRONT_LEFT: 0x0400, + FRONT_RIGHT: 0x0401, + BACK_LEFT: 0x0402, + BACK_RIGHT: 0x0403, + FRONT: 0x0404, + BACK: 0x0405, + LEFT: 0x0406, + RIGHT: 0x0407, + FRONT_AND_BACK: 0x0408, + NO_ERROR: 0, + INVALID_ENUM: 0x0500, + INVALID_VALUE: 0x0501, + INVALID_OPERATION: 0x0502, + OUT_OF_MEMORY: 0x0505, + EXP: 0x0800, + EXP2: 0x0801, + CW: 0x0900, + CCW: 0x0901, + POINT_SIZE: 0x0B11, + POINT_SIZE_RANGE: 0x0B12, + POINT_SIZE_GRANULARITY: 0x0B13, + LINE_SMOOTH: 0x0B20, + LINE_WIDTH: 0x0B21, + LINE_WIDTH_RANGE: 0x0B22, + LINE_WIDTH_GRANULARITY: 0x0B23, + LIST_INDEX: 0x0B33, + LIGHTING: 0x0B50, + ALPHA_TEST: 0x0BC0, + POLYGON_MODE: 0x0B40, + POLYGON_SMOOTH: 0x0B41, + CULL_FACE: 0x0B44, + CULL_FACE_MODE: 0x0B45, + FRONT_FACE: 0x0B46, + COLOR_MATERIAL: 0x0B57, + FOG: 0x0B60, + FOG_DENSITY: 0x0B62, + FOG_START: 0x0B63, + FOG_END: 0x0B64, + FOG_MODE: 0x0B65, + FOG_COLOR: 0x0B66, + DEPTH_RANGE: 0x0B70, + DEPTH_TEST: 0x0B71, + DEPTH_WRITEMASK: 0x0B72, + DEPTH_CLEAR_VALUE: 0x0B73, + DEPTH_FUNC: 0x0B74, + STENCIL_TEST: 0x0B90, + STENCIL_CLEAR_VALUE: 0x0B91, + STENCIL_FUNC: 0x0B92, + STENCIL_VALUE_MASK: 0x0B93, + STENCIL_FAIL: 0x0B94, + STENCIL_PASS_DEPTH_FAIL: 0x0B95, + STENCIL_PASS_DEPTH_PASS: 0x0B96, + STENCIL_REF: 0x0B97, + STENCIL_WRITEMASK: 0x0B98, + MATRIX_MODE: 0x0BA0, + NORMALIZE: 0x0BA1, + VIEWPORT: 0x0BA2, + MODELVIEW_STACK_DEPTH: 0x0BA3, + PROJECTION_STACK_DEPTH: 0x0BA4, + TEXTURE_STACK_DEPTH: 0x0BA5, + MODELVIEW_MATRIX: 0x0BA6, + PROJECTION_MATRIX: 0x0BA7, + TEXTURE_MATRIX: 0x0BA8, + ATTRIB_STACK_DEPTH: 0x0BB0, + CLIENT_ATTRIB_STACK_DEPTH: 0x0BB1, + ALPHA_TEST: 0x0BC0, + ALPHA_TEST_FUNC: 0x0BC1, + ALPHA_TEST_REF: 0x0BC2, + DITHER: 0x0BD0, + BLEND_DST: 0x0BE0, + BLEND_SRC: 0x0BE1, + BLEND: 0x0BE2, + LOGIC_OP_MODE: 0x0BF0, + DRAW_BUFFER: 0x0C01, + READ_BUFFER: 0x0C02, + SCISSOR_BOX: 0x0C10, + SCISSOR_TEST: 0x0C11, + COLOR_CLEAR_VALUE: 0x0C22, + COLOR_WRITEMASK: 0x0C23, + DOUBLEBUFFER: 0x0C32, + STEREO: 0x0C33, + PERSPECTIVE_CORRECTION_HINT: 0x0C50, + LINE_SMOOTH_HINT: 0x0C52, + POLYGON_SMOOTH_HINT: 0x0C53, + FOG_HINT: 0x0C54, + TEXTURE_GEN_S: 0x0C60, + TEXTURE_GEN_T: 0x0C61, + TEXTURE_GEN_R: 0x0C62, + TEXTURE_GEN_Q: 0x0C63, + PIXEL_MAP_I_TO_I: 0x0C70, + PIXEL_MAP_S_TO_S: 0x0C71, + PIXEL_MAP_I_TO_R: 0x0C72, + PIXEL_MAP_I_TO_G: 0x0C73, + PIXEL_MAP_I_TO_B: 0x0C74, + PIXEL_MAP_I_TO_A: 0x0C75, + PIXEL_MAP_R_TO_R: 0x0C76, + PIXEL_MAP_G_TO_G: 0x0C77, + PIXEL_MAP_B_TO_B: 0x0C78, + PIXEL_MAP_A_TO_A: 0x0C79, + PIXEL_MAP_I_TO_I_SIZE: 0x0CB0, + PIXEL_MAP_S_TO_S_SIZE: 0x0CB1, + PIXEL_MAP_I_TO_R_SIZE: 0x0CB2, + PIXEL_MAP_I_TO_G_SIZE: 0x0CB3, + PIXEL_MAP_I_TO_B_SIZE: 0x0CB4, + PIXEL_MAP_I_TO_A_SIZE: 0x0CB5, + PIXEL_MAP_R_TO_R_SIZE: 0x0CB6, + PIXEL_MAP_G_TO_G_SIZE: 0x0CB7, + PIXEL_MAP_B_TO_B_SIZE: 0x0CB8, + PIXEL_MAP_A_TO_A_SIZE: 0x0CB9, + UNPACK_SWAP_BYTES: 0x0CF0, + UNPACK_LSB_FIRST: 0x0CF1, + UNPACK_ROW_LENGTH: 0x0CF2, + UNPACK_SKIP_ROWS: 0x0CF3, + UNPACK_SKIP_PIXELS: 0x0CF4, + UNPACK_ALIGNMENT: 0x0CF5, + PACK_SWAP_BYTES: 0x0D00, + PACK_LSB_FIRST: 0x0D01, + PACK_ROW_LENGTH: 0x0D02, + PACK_SKIP_ROWS: 0x0D03, + PACK_SKIP_PIXELS: 0x0D04, + PACK_ALIGNMENT: 0x0D05, + MAX_TEXTURE_SIZE: 0x0D33, + MAX_VIEWPORT_DIMS: 0x0D3A, + SUBPIXEL_BITS: 0x0D50, + TEXTURE_1D: 0x0DE0, + TEXTURE_2D: 0x0DE1, + TEXTURE_WIDTH: 0x1000, + TEXTURE_HEIGHT: 0x1001, + TEXTURE_BORDER_COLOR: 0x1004, + DONT_CARE: 0x1100, + FASTEST: 0x1101, + NICEST: 0x1102, + AMBIENT: 0x1200, + DIFFUSE: 0x1201, + SPECULAR: 0x1202, + POSITION: 0x1203, + SPOT_CUTOFF: 0x1206, + COMPILE: 0x1300, + COMPILE_AND_EXECUTE: 0x1301, + BYTE: 0x1400, + UNSIGNED_BYTE: 0x1401, + SHORT: 0x1402, + UNSIGNED_SHORT: 0x1403, + INT: 0x1404, + UNSIGNED_INT: 0x1405, + FLOAT: 0x1406, + STACK_OVERFLOW: 0x0503, + STACK_UNDERFLOW: 0x0504, + CLEAR: 0x1500, + AND: 0x1501, + AND_REVERSE: 0x1502, + COPY: 0x1503, + AND_INVERTED: 0x1504, + NOOP: 0x1505, + XOR: 0x1506, + OR: 0x1507, + NOR: 0x1508, + EQUIV: 0x1509, + INVERT: 0x150A, + OR_REVERSE: 0x150B, + COPY_INVERTED: 0x150C, + OR_INVERTED: 0x150D, + NAND: 0x150E, + SET: 0x150F, + EMISSION: 0x1600, + SHININESS: 0x1601, + AMBIENT_AND_DIFFUSE: 0x1602, + COLOR_INDEXES: 0x1603, + MODELVIEW: 0x1700, + PROJECTION: 0x1701, + TEXTURE: 0x1702, + COLOR: 0x1800, + DEPTH: 0x1801, + STENCIL: 0x1802, + STENCIL_INDEX: 0x1901, + DEPTH_COMPONENT: 0x1902, + RED: 0x1903, + GREEN: 0x1904, + BLUE: 0x1905, + ALPHA: 0x1906, + RGB: 0x1907, + RGBA: 0x1908, + POINT: 0x1B00, + LINE: 0x1B01, + FILL: 0x1B02, + FLAT: 0x1D00, + SMOOTH: 0x1D01, + KEEP: 0x1E00, + REPLACE: 0x1E01, + INCR: 0x1E02, + DECR: 0x1E03, + VENDOR: 0x1F00, + RENDERER: 0x1F01, + VERSION: 0x1F02, + EXTENSIONS: 0x1F03, + S: 0x2000, + T: 0x2001, + R: 0x2002, + Q: 0x2003, + MODULATE: 0x2100, + DECAL: 0x2101, + TEXTURE_ENV_MODE: 0x2200, + TEXTURE_ENV: 0x2300, + EYE_LINEAR: 0x2400, + OBJECT_LINEAR: 0x2401, + SPHERE_MAP: 0x2402, + TEXTURE_GEN_MODE: 0x2500, + OBJECT_PLANE: 0x2501, + EYE_PLANE: 0x2502, + SPHERE_MAP: 0x2402, + NEAREST: 0x2600, + LINEAR: 0x2601, + NEAREST_MIPMAP_NEAREST: 0x2700, + LINEAR_MIPMAP_NEAREST: 0x2701, + NEAREST_MIPMAP_LINEAR: 0x2702, + LINEAR_MIPMAP_LINEAR: 0x2703, + TEXTURE_MAG_FILTER: 0x2800, + TEXTURE_MIN_FILTER: 0x2801, + TEXTURE_WRAP_S: 0x2802, + TEXTURE_WRAP_T: 0x2803, + REPEAT: 0x2901, + CLIP_PLANE0: 0x3000, + CLIP_PLANE1: 0x3001, + CLIP_PLANE2: 0x3002, + CLIP_PLANE3: 0x3003, + CLIP_PLANE4: 0x3004, + CLIP_PLANE5: 0x3005, + CLIP_PLANE6: 0x3006, + CLIP_PLANE7: 0x3007, + LIGHT0: 0x4000, + LIGHT1: 0x4001, + LIGHT2: 0x4002, + LIGHT3: 0x4003, + LIGHT4: 0x4004, + LIGHT5: 0x4005, + LIGHT6: 0x4006, + LIGHT7: 0x4007, + BGRA: 0x80E1, + CLAMP_TO_EDGE: 0x812F, + VERTEX_ARRAY: 0x8074, + NORMAL_ARRAY: 0x8075, + COLOR_ARRAY: 0x8076, + INDEX_ARRAY: 0x8077, + TEXTURE_COORD_ARRAY: 0x8078, + GENERATE_MIPMAP_SGIS: 0x8191, + GENERATE_MIPMAP_HINT_SGIS: 0x8192, + TEXTURE_COMPRESSED: 0x86A1, + CURRENT_BIT: 0x00001, + POINT_BIT: 0x00002, + LINE_BIT: 0x00004, + POLYGON_BIT: 0x00008, + POLYGON_STIPPLE_BIT: 0x00010, + PIXEL_MODE_BIT: 0x00020, + LIGHTING_BIT: 0x00040, + FOG_BIT: 0x00080, + DEPTH_BUFFER_BIT: 0x00100, + ACCUM_BUFFER_BIT: 0x00200, + STENCIL_BUFFER_BIT: 0x00400, + VIEWPORT_BIT: 0x00800, + TRANSFORM_BIT: 0x01000, + ENABLE_BIT: 0x02000, + COLOR_BUFFER_BIT: 0x04000, + HINT_BIT: 0x08000, + EVAL_BIT: 0x10000, + LIST_BIT: 0x20000, + TEXTURE_BIT: 0x40000, + SCISSOR_BIT: 0x80000, + ALL_ATTRIB_BITS: 0xFFFFF, + ZERO: 0, + ONE: 1, + }; + GL_Symbols = {}; + for (var name in GL) { + var value = GL[name]; + GL_Symbols[value] = name; + } +} + +function registerOpenGL() { + if (typeof Squeak === "object" && Squeak.registerExternalModule) { + Squeak.registerExternalModule('libGL.so', OpenGL()); + } else self.setTimeout(registerOpenGL, 100); +}; + +registerOpenGL();