├── src ├── assets │ ├── lut.png │ ├── ogl.png │ ├── black.jpg │ ├── dark.png │ ├── white.jpg │ ├── favicon.png │ ├── ui │ │ ├── download.jpg │ │ ├── loader.jpg │ │ ├── support.jpg │ │ └── instructions.jpg │ ├── interior-diffuse-RGBM.png │ ├── raleway-bold-webfont.woff │ ├── interior-specular-RGBM.png │ ├── raleway-bold-webfont.woff2 │ ├── raleway-regular-webfont.woff │ ├── raleway-regular-webfont.woff2 │ ├── materials │ │ ├── hammered-metal-mro.jpg │ │ └── hammered-metal-normal.jpg │ └── main.css ├── SaveAs.js ├── shaders │ ├── AtlasShader.js │ ├── BackgroundShader.js │ ├── DiffuseShader.js │ ├── SpecularShader.js │ └── PBRShader.js ├── DragDrop.js ├── Diffuse.js ├── 16bit │ ├── use.js │ └── encode16BitPNG.js ├── GLSLVersion.js ├── Specular.js ├── PNG.js ├── PBR.js ├── inflate.module.min.js ├── PNGWalker.js └── HDRLoader.js ├── ogl ├── math │ ├── Vec4.js │ ├── Color.js │ ├── Euler.js │ ├── Mat3.js │ ├── functions │ │ ├── EulerFunc.js │ │ ├── Vec2Func.js │ │ ├── Vec4Func.js │ │ └── QuatFunc.js │ ├── Vec2.js │ ├── Quat.js │ ├── Vec3.js │ └── Mat4.js ├── OGL.js ├── core │ ├── Camera.js │ ├── Mesh.js │ ├── Transform.js │ ├── RenderTarget.js │ ├── Geometry.js │ ├── Texture.js │ ├── Program.js │ └── Renderer.js └── extras │ ├── Cube.js │ ├── Plane.js │ ├── Sphere.js │ └── Orbit.js ├── LICENSE ├── readme.md └── index.html /src/assets/lut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/lut.png -------------------------------------------------------------------------------- /src/assets/ogl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/ogl.png -------------------------------------------------------------------------------- /src/assets/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/black.jpg -------------------------------------------------------------------------------- /src/assets/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/dark.png -------------------------------------------------------------------------------- /src/assets/white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/white.jpg -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/ui/download.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/ui/download.jpg -------------------------------------------------------------------------------- /src/assets/ui/loader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/ui/loader.jpg -------------------------------------------------------------------------------- /src/assets/ui/support.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/ui/support.jpg -------------------------------------------------------------------------------- /src/assets/ui/instructions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/ui/instructions.jpg -------------------------------------------------------------------------------- /src/assets/interior-diffuse-RGBM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/interior-diffuse-RGBM.png -------------------------------------------------------------------------------- /src/assets/raleway-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/raleway-bold-webfont.woff -------------------------------------------------------------------------------- /src/assets/interior-specular-RGBM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/interior-specular-RGBM.png -------------------------------------------------------------------------------- /src/assets/raleway-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/raleway-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/assets/raleway-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/raleway-regular-webfont.woff -------------------------------------------------------------------------------- /src/assets/raleway-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/raleway-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/assets/materials/hammered-metal-mro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/materials/hammered-metal-mro.jpg -------------------------------------------------------------------------------- /src/assets/materials/hammered-metal-normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oframe/ibl-converter/HEAD/src/assets/materials/hammered-metal-normal.jpg -------------------------------------------------------------------------------- /src/SaveAs.js: -------------------------------------------------------------------------------- 1 | var a = document.createElement('a'); 2 | document.body.appendChild(a); 3 | a.style.display = 'none'; 4 | 5 | export function saveAs(arr, fileName) { 6 | const blob = new Blob([arr], {type: 'octet/stream'}); 7 | const url = URL.createObjectURL(blob); 8 | a.href = url; 9 | a.download = fileName; 10 | a.click(); 11 | window.URL.revokeObjectURL(url); 12 | } -------------------------------------------------------------------------------- /src/shaders/AtlasShader.js: -------------------------------------------------------------------------------- 1 | const vertex = ` 2 | precision highp float; 3 | precision highp int; 4 | 5 | attribute vec3 position; 6 | attribute vec2 uv; 7 | 8 | varying vec2 vUv; 9 | 10 | void main() { 11 | vUv = uv; 12 | 13 | gl_Position = vec4(position, 1.0); 14 | } 15 | `; 16 | 17 | const fragment = ` 18 | precision highp float; 19 | precision highp int; 20 | 21 | uniform sampler2D tMap[7]; 22 | 23 | varying vec2 vUv; 24 | 25 | void main() { 26 | vec2 uv = vUv; 27 | uv.y *= 2.0; 28 | vec4 tex; 29 | 30 | for(int i = 0; i < 7; i++) { 31 | if (uv.y >= 0.0 && uv.y <= 1.0) { 32 | tex = texture2D(tMap[i], uv); 33 | } 34 | uv.y -= 1.0; 35 | uv *= 2.0; 36 | } 37 | 38 | gl_FragColor = tex; 39 | } 40 | `; 41 | 42 | export default {vertex, fragment}; -------------------------------------------------------------------------------- /ogl/math/Vec4.js: -------------------------------------------------------------------------------- 1 | import * as Vec4Func from './functions/Vec4Func.js'; 2 | 3 | export class Vec4 extends Float32Array { 4 | constructor(array = [0, 0, 0, 0]) { 5 | if (!array.length) array = [array, array, array]; 6 | super(array); 7 | return this; 8 | } 9 | 10 | get x() { 11 | return this[0]; 12 | } 13 | 14 | set x(v) { 15 | this[0] = v; 16 | } 17 | 18 | get y() { 19 | return this[1]; 20 | } 21 | 22 | set y(v) { 23 | this[1] = v; 24 | } 25 | 26 | get z() { 27 | return this[2]; 28 | } 29 | 30 | set z(v) { 31 | this[2] = v; 32 | } 33 | 34 | get w() { 35 | return this[3]; 36 | } 37 | 38 | set w(v) { 39 | this[3] = v; 40 | } 41 | 42 | set(x, y, z, w) { 43 | Vec4Func.set(this, x, y, z, w); 44 | return this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 oframe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ogl/math/Color.js: -------------------------------------------------------------------------------- 1 | // TODO : support more color formats - e.g 0xffffff, '#fff' 2 | 3 | export class Color extends Float32Array { 4 | constructor(array = [0, 0, 0]) { 5 | super(3); 6 | if (typeof array === 'string') array = Color.hexToRGB(array); 7 | this.set(...array); 8 | return this; 9 | } 10 | 11 | get r() { 12 | return this[0]; 13 | } 14 | 15 | set r(v) { 16 | this[0] = v; 17 | } 18 | 19 | get g() { 20 | return this[1]; 21 | } 22 | 23 | set g(v) { 24 | this[1] = v; 25 | } 26 | 27 | get b() { 28 | return this[2]; 29 | } 30 | 31 | set b(v) { 32 | this[2] = v; 33 | } 34 | 35 | set(r, g, b) { 36 | this[0] = r; 37 | this[1] = g; 38 | this[2] = b; 39 | return this; 40 | } 41 | 42 | static hexToRGB(hex) { 43 | const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 44 | return r ? [ 45 | parseInt(r[1], 16) / 255, 46 | parseInt(r[2], 16) / 255, 47 | parseInt(r[3], 16) / 255 48 | ] : null; 49 | } 50 | } -------------------------------------------------------------------------------- /src/DragDrop.js: -------------------------------------------------------------------------------- 1 | export const DragDrop = {}; 2 | 3 | addHandlers(); 4 | function addHandlers() { 5 | document.addEventListener('dragenter', enter); 6 | document.addEventListener('dragover', over); 7 | document.addEventListener('dragleave', leave); 8 | document.addEventListener('drop', drop); 9 | } 10 | 11 | function enter(e) { 12 | e.preventDefault(); 13 | } 14 | 15 | function over(e) { 16 | e.preventDefault(); 17 | } 18 | 19 | function leave(e) { 20 | e.preventDefault(); 21 | } 22 | 23 | function drop(e) { 24 | e.preventDefault(); 25 | 26 | const file = e.dataTransfer.files[0]; 27 | let split = file.name.split('.'); 28 | const name = split[0]; 29 | const ext = split[1].toLowerCase(); 30 | 31 | const reader = new FileReader(); 32 | 33 | reader.onload = function (e) { 34 | const buffer = e.target.result; 35 | 36 | DragDrop.onDrop && DragDrop.onDrop(buffer, name, ext); 37 | }; 38 | 39 | switch (ext) { 40 | case 'hdr': 41 | case 'exr': 42 | reader.readAsArrayBuffer(file); 43 | break; 44 | default: 45 | reader.readAsDataURL(file); 46 | break; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ogl/OGL.js: -------------------------------------------------------------------------------- 1 | // Core 2 | export {Geometry} from './core/Geometry.js'; 3 | export {Program} from './core/Program.js'; 4 | export {Renderer} from './core/Renderer.js'; 5 | export {Camera} from './core/Camera.js'; 6 | export {Transform} from './core/Transform.js'; 7 | export {Mesh} from './core/Mesh.js'; 8 | export {Texture} from './core/Texture.js'; 9 | export {RenderTarget} from './core/RenderTarget.js'; 10 | 11 | // Maths 12 | export {Color} from './math/Color.js'; 13 | export {Euler} from './math/Euler.js'; 14 | export {Mat3} from './math/Mat3.js'; 15 | export {Mat4} from './math/Mat4.js'; 16 | export {Quat} from './math/Quat.js'; 17 | export {Vec2} from './math/Vec2.js'; 18 | export {Vec3} from './math/Vec3.js'; 19 | export {Vec4} from './math/Vec4.js'; 20 | 21 | // Extras 22 | export {Plane} from './extras/Plane.js'; 23 | export {Cube} from './extras/Cube.js'; 24 | export {Sphere} from './extras/Sphere.js'; 25 | export {Orbit} from './extras/Orbit.js'; 26 | // export {Curve} from './extras/Curve.js'; 27 | // export {Raycasting} from './extras/Raycasting.js'; 28 | // export {Projection} from './extras/Projection.js'; 29 | // export {Post} from './extras/Post.js'; 30 | // export {Rig} from './extras/Rig.js'; 31 | // export {RigAnimation} from './extras/RigAnimation.js'; 32 | -------------------------------------------------------------------------------- /ogl/math/Euler.js: -------------------------------------------------------------------------------- 1 | import * as EulerFunc from './functions/EulerFunc.js'; 2 | import {Mat4} from './Mat4.js'; 3 | 4 | const tmpMat4 = new Mat4(); 5 | 6 | export class Euler extends Float32Array { 7 | constructor(array = [0, 0, 0], order = 'YXZ') { 8 | super(3); 9 | if (typeof array === 'string') array = this.hexToRGB(array); 10 | this.onChange = () => {}; 11 | this.set(...array); 12 | this.reorder(order); 13 | return this; 14 | } 15 | 16 | get x() { 17 | return this[0]; 18 | } 19 | 20 | set x(v) { 21 | this[0] = v; 22 | this.onChange(); 23 | } 24 | 25 | get y() { 26 | return this[1]; 27 | } 28 | 29 | set y(v) { 30 | this[1] = v; 31 | this.onChange(); 32 | } 33 | 34 | get z() { 35 | return this[2]; 36 | } 37 | 38 | set z(v) { 39 | this[2] = v; 40 | this.onChange(); 41 | } 42 | 43 | set(x, y = x, z = x) { 44 | this[0] = x; 45 | this[1] = y; 46 | this[2] = z; 47 | this.onChange(); 48 | return this; 49 | } 50 | 51 | reorder(order) { 52 | this.order = order; 53 | this.onChange(); 54 | return this; 55 | } 56 | 57 | fromRotationMatrix(m, order = this.order) { 58 | EulerFunc.fromRotationMatrix(this, m, order); 59 | return this; 60 | } 61 | 62 | fromQuaternion(q, order = this.order) { 63 | tmpMat4.fromQuaternion(q); 64 | return this.fromRotationMatrix(tmpMat4, order); 65 | } 66 | } -------------------------------------------------------------------------------- /ogl/core/Camera.js: -------------------------------------------------------------------------------- 1 | import {Transform} from './Transform.js'; 2 | import {Mat4} from '../math/Mat4.js'; 3 | 4 | export class Camera extends Transform { 5 | constructor(gl, { 6 | near = 0.1, 7 | far = 100, 8 | fov = 45, 9 | aspect = 1, 10 | left, 11 | right, 12 | bottom, 13 | top, 14 | 15 | } = {}) { 16 | super(gl); 17 | 18 | this.near = near; 19 | this.far = far; 20 | this.fov = fov; 21 | this.aspect = aspect; 22 | 23 | this.projectionMatrix = new Mat4(); 24 | this.viewMatrix = new Mat4(); 25 | 26 | // Use orthographic if values set, else default to perspective camera 27 | if (left || right) this.orthographic({left, right, bottom, top}); 28 | else this.perspective(); 29 | } 30 | 31 | perspective({ 32 | near = this.near, 33 | far = this.far, 34 | fov = this.fov, 35 | aspect = this.aspect, 36 | } = {}) { 37 | this.projectionMatrix.fromPerspective({fov: fov * (Math.PI / 180), aspect, near, far}); 38 | this.type = 'perspective'; 39 | } 40 | 41 | orthographic({ 42 | near = this.near, 43 | far = this.far, 44 | left = -1, 45 | right = 1, 46 | bottom = 1, 47 | top = 1, 48 | } = {}) { 49 | this.projectionMatrix.fromOrthogonal({left, right, bottom, top, near, far}); 50 | this.type = 'orthographic'; 51 | } 52 | 53 | updateMatrixWorld() { 54 | super.updateMatrixWorld(); 55 | this.viewMatrix.copy(this.worldMatrix).invert(); 56 | } 57 | 58 | lookAt(target) { 59 | super.lookAt(target, true); 60 | }; 61 | } -------------------------------------------------------------------------------- /src/Diffuse.js: -------------------------------------------------------------------------------- 1 | import {Renderer, Plane, Texture, RenderTarget, Program, Mesh} from '../ogl/OGL.js'; 2 | import DiffuseShader from './shaders/DiffuseShader.js'; 3 | 4 | // TODO: try and minimize harsh point at poles 5 | 6 | let OUTPUT_SIZE = 128; 7 | 8 | // sRGB == 0 9 | // RGBE == 1 10 | // RGBM == 2 11 | // RGBD == 3 12 | let INPUT_TYPE = 1; 13 | let OUTPUT_TYPE = 2; 14 | 15 | const renderer = new Renderer({alpha: true, premultipliedAlpha: true}); 16 | renderer.setSize(OUTPUT_SIZE, OUTPUT_SIZE / 2); 17 | export const gl = renderer.gl; 18 | gl.clearColor(0, 0, 0, 0); 19 | // document.body.appendChild(gl.canvas); 20 | 21 | const geometry = new Plane(gl, 2, 2); 22 | 23 | const program = new Program(gl, { 24 | vertexShader: DiffuseShader.vertex, 25 | fragmentShader: DiffuseShader.fragment, 26 | uniforms: { 27 | tMap: {value: null}, 28 | uInputType: {value: 0}, 29 | uOutputType: {value: 0}, 30 | }, 31 | transparent: true, 32 | }); 33 | 34 | const mesh = new Mesh(gl, {geometry, program}); 35 | 36 | export function renderDiffuse(data) { 37 | INPUT_TYPE = data.inputType; 38 | OUTPUT_TYPE = data.outputType; 39 | 40 | // Create input texture 41 | const texture = new Texture(gl, { 42 | image: data.data, 43 | width: data.width, 44 | height: data.height, 45 | generateMipmaps: false, 46 | }); 47 | 48 | // Update program with new texture and values 49 | program.uniforms.tMap.value = texture; 50 | program.uniforms.uInputType.value = INPUT_TYPE; 51 | program.uniforms.uOutputType.value = OUTPUT_TYPE; 52 | 53 | const size = OUTPUT_SIZE; 54 | 55 | renderer.setSize(size, size / 2); 56 | 57 | // Need to render twice when on external server - I have no idea why 58 | renderer.render({scene: mesh}); 59 | renderer.render({scene: mesh}); 60 | } -------------------------------------------------------------------------------- /ogl/math/Mat3.js: -------------------------------------------------------------------------------- 1 | import * as Mat3Func from './functions/Mat3Func.js'; 2 | 3 | export class Mat3 extends Float32Array { 4 | constructor(array = [1, 0, 0, 0, 1, 0, 0, 0, 1]) { 5 | super(array); 6 | return this; 7 | } 8 | 9 | set(m00, m01, m02, m10, m11, m12, m20, m21, m22) { 10 | Mat3Func.set(this, m00, m01, m02, m10, m11, m12, m20, m21, m22); 11 | return this; 12 | } 13 | 14 | translate(v, m = this) { 15 | Mat3Func.translate(this, m, v); 16 | return this; 17 | } 18 | 19 | rotate(v, m = this) { 20 | Mat3Func.rotate(this, m, v); 21 | return this; 22 | } 23 | 24 | scale(v, m = this) { 25 | Mat3Func.scale(this, m, v); 26 | return this; 27 | } 28 | 29 | multiply(ma, mb) { 30 | if (mb) { 31 | Mat3Func.multiply(this, ma, mb); 32 | } else { 33 | Mat3Func.multiply(this, this, ma); 34 | } 35 | return this; 36 | } 37 | 38 | identity() { 39 | Mat3Func.identity(this); 40 | return this; 41 | } 42 | 43 | copy(m) { 44 | Mat3Func.copy(this, m); 45 | return this; 46 | } 47 | 48 | fromMatrix4(m) { 49 | Mat3Func.fromMat4(this, m); 50 | return this; 51 | } 52 | 53 | fromQuaternion(q) { 54 | Mat3Func.fromQuat(this, q); 55 | return this; 56 | } 57 | 58 | fromBasis(vec3a, vec3b, vec3c) { 59 | this.set( 60 | vec3a[0], 61 | vec3a[1], 62 | vec3a[2], 63 | vec3b[0], 64 | vec3b[1], 65 | vec3b[2], 66 | vec3c[0], 67 | vec3c[1], 68 | vec3c[2] 69 | ); 70 | return this; 71 | } 72 | 73 | invert(m = this) { 74 | Mat3Func.invert(this, m); 75 | return this; 76 | } 77 | 78 | getNormalMatrix(m) { 79 | Mat3Func.normalFromMat4(this, m); 80 | return this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/16bit/use.js: -------------------------------------------------------------------------------- 1 | import './assets/pako_deflate.js'; 2 | import { encodePNG } from './assets/PNG.js'; 3 | import { saveAs } from './assets/saveAs.js'; 4 | 5 | { 6 | const width = 256; 7 | const height = 256; 8 | const colorChannels = 3; 9 | const alphaChannels = 0; 10 | const depth = 16; 11 | const array = new Uint16Array(width * height * (colorChannels + alphaChannels)); 12 | const max = 65535; 13 | // const depth = 8; 14 | // const array = new Uint8Array(width * height * (colorChannels + alphaChannels)); 15 | 16 | for (let i = 0; i < height; i++) { 17 | for (let j = 0; j < width; j++) { 18 | array[(i * width + j) * 3 + 0] = (j / width) * max; 19 | array[(i * width + j) * 3 + 1] = (j / width) * max; 20 | array[(i * width + j) * 3 + 2] = (j / width) * max; 21 | } 22 | } 23 | 24 | // for (let i = 0, j = 0; i < array.length; i += colorChannels + alphaChannels, j++) { 25 | // // 65535 = max 26 | // array[i + 0] = j; 27 | // array[i + 1] = j; 28 | // array[i + 2] = j; 29 | // // array[i + 0] = (j / 65535) * 255; 30 | // // array[i + 1] = (j / 65535) * 255; 31 | // // array[i + 2] = (j / 65535) * 255; 32 | // // array[i + 0] = Math.random() * 65535; 33 | // // array[i + 1] = Math.random() * 65535; 34 | // // array[i + 2] = Math.random() * 65535; 35 | // } 36 | 37 | const dataView = new DataView(array.buffer); 38 | 39 | // Swap little to big endian 40 | if (depth === 16) { 41 | for (let i = 0; i < dataView.byteLength / 2; i++) { 42 | dataView.setUint16(i * 2, swap16(dataView.getUint16(i * 2))); 43 | } 44 | } 45 | 46 | function swap16(val) { 47 | return ((val & 0xff) << 8) | ((val >> 8) & 0xff); 48 | } 49 | 50 | const png = encodePNG([dataView.buffer], width, height, colorChannels, alphaChannels, depth); 51 | 52 | // saveAs(png, 'test.png'); 53 | } 54 | -------------------------------------------------------------------------------- /ogl/extras/Cube.js: -------------------------------------------------------------------------------- 1 | import {Geometry} from '../core/Geometry.js'; 2 | import {Plane} from './Plane.js'; 3 | 4 | export class Cube extends Geometry { 5 | constructor(gl, width = 1, height = width, depth = width, wSegs = 1, hSegs = wSegs, dSegs = wSegs) { 6 | const num = (wSegs + 1) * (hSegs + 1) * 2 + (wSegs + 1) * (dSegs + 1) * 2 + (hSegs + 1) * (dSegs + 1) * 2; 7 | const numIndices = wSegs * hSegs * 2 + wSegs * dSegs * 2 + hSegs * dSegs * 2; 8 | 9 | const position = new Float32Array(num * 3); 10 | const normal = new Float32Array(num * 3); 11 | const uv = new Float32Array(num * 2); 12 | const index = new Uint16Array(numIndices * 6); 13 | 14 | let i = 0; 15 | let ii = 0; 16 | 17 | // left, right 18 | Plane.buildPlane(position, normal, uv, index, depth, height, width, dSegs, hSegs, 2, 1, 0, -1, -1, i, ii); 19 | Plane.buildPlane(position, normal, uv, index, depth, height, -width, dSegs, hSegs, 2, 1, 0, 1, -1, i += (dSegs + 1) * (hSegs + 1), ii += dSegs * hSegs); 20 | 21 | // top, bottom 22 | Plane.buildPlane(position, normal, uv, index, width, depth, height, dSegs, hSegs, 0, 2, 1, 1, 1, i += (dSegs + 1) * (hSegs + 1), ii += dSegs * hSegs); 23 | Plane.buildPlane(position, normal, uv, index, width, depth, -height, dSegs, hSegs, 0, 2, 1, 1, -1, i += (wSegs + 1) * (dSegs + 1), ii += wSegs * dSegs); 24 | 25 | // front, back 26 | Plane.buildPlane(position, normal, uv, index, width, height, -depth, wSegs, hSegs, 0, 1, 2, -1, -1, i += (wSegs + 1) * (dSegs + 1), ii += wSegs * dSegs); 27 | Plane.buildPlane(position, normal, uv, index, width, height, depth, wSegs, hSegs, 0, 1, 2, 1, -1, i += (wSegs + 1) * (hSegs + 1), ii += wSegs * hSegs); 28 | 29 | super(gl, { 30 | position: {size: 3, data: position}, 31 | normal: {size: 3, data: normal}, 32 | uv: {size: 2, data: uv}, 33 | index: {data: index}, 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /src/GLSLVersion.js: -------------------------------------------------------------------------------- 1 | export function toGLSL100(input) { 2 | return input; 3 | 4 | // TODO: convert down 5 | // if dfdy or dfdx add '#extension GL_OES_standard_derivatives : enable\n' 6 | } 7 | 8 | export function toGLSL300(input) { 9 | const lines = input.split('\n'); 10 | 11 | // Determine if vertex or fragment by checking if gl_Position present 12 | const isVertex = !!~input.indexOf('gl_Position'); 13 | 14 | // Must start with version 15 | let output = '#version 300 es\n'; 16 | 17 | lines.forEach(line => { 18 | 19 | // Skip any old version lines 20 | if (!!~line.indexOf('#version')) return; 21 | 22 | // Skip any extension lines 23 | if (!!~line.indexOf('#extension')) return; 24 | 25 | // Replace 'attribute' with 'in' 26 | if (isVertex) line = line.split('attribute').join('in'); 27 | 28 | // Replace 'varying' with 'in' or 'out' 29 | line = line.split('varying').join(isVertex ? 'out' : 'in'); 30 | 31 | // Replace 'texture2D' and 'textureCube' with 'texture' 32 | line = line.split('texture2D').join('texture'); 33 | line = line.split('textureCube').join('texture'); 34 | 35 | // Add replacement output for gl_FragColor just before main() 36 | if (!isVertex) line = line.split('void main()').join('out vec4 fragColor;\nvoid main()'); 37 | 38 | // Replace 'gl_FragColor' with 'fragColor' 39 | if (!isVertex) line = line.split('gl_FragColor').join('fragColor'); 40 | 41 | output += `${line}\n`; 42 | }); 43 | 44 | return output; 45 | } 46 | 47 | // Automatically determine which GLSL version for the active WebGL version 48 | export function convertGLSL(gl, input) { 49 | const isGLSL300 = !!~input.indexOf('#version 300 es'); 50 | if (gl.renderer.isWebgl2) { 51 | if (isGLSL300) return input; 52 | return toGLSL300(input); 53 | } else { 54 | if (!isGLSL300) return input; 55 | return toGLSL100(input); 56 | } 57 | } -------------------------------------------------------------------------------- /ogl/math/functions/EulerFunc.js: -------------------------------------------------------------------------------- 1 | 2 | // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) 3 | export function fromRotationMatrix(out, m, order = 'YXZ') { 4 | 5 | if (order === 'XYZ') { 6 | out[1] = Math.asin(Math.min(Math.max(m[8], -1), 1)); 7 | if (Math.abs(m[8]) < 0.99999) { 8 | out[0] = Math.atan2(-m[9], m[10]); 9 | out[2] = Math.atan2(-m[4], m[0]); 10 | } else { 11 | out[0] = Math.atan2(m[6], m[5]); 12 | out[2] = 0; 13 | } 14 | } else if (order === 'YXZ') { 15 | out[0] = Math.asin(-Math.min(Math.max(m[9], -1), 1)); 16 | if (Math.abs(m[9]) < 0.99999) { 17 | out[1] = Math.atan2(m[8], m[10]); 18 | out[2] = Math.atan2(m[1], m[5]); 19 | } else { 20 | out[1] = Math.atan2(-m[2], m[0]); 21 | out[2] = 0; 22 | } 23 | } else if (order === 'ZXY') { 24 | out[0] = Math.asin(Math.min(Math.max(m[6], -1), 1)); 25 | if (Math.abs(m[6]) < 0.99999) { 26 | out[1] = Math.atan2(-m[2], m[10]); 27 | out[2] = Math.atan2(-m[4], m[5]); 28 | } else { 29 | out[1] = 0; 30 | out[2] = Math.atan2(m[1], m[0]); 31 | } 32 | } else if (order === 'ZYX') { 33 | out[1] = Math.asin(-Math.min(Math.max(m[2], -1), 1)); 34 | if (Math.abs(m[2]) < 0.99999) { 35 | out[0] = Math.atan2(m[6], m[10]); 36 | out[2] = Math.atan2(m[1], m[0]); 37 | } else { 38 | out[0] = 0; 39 | out[2] = Math.atan2(-m[4], m[5]); 40 | } 41 | } else if (order === 'YZX') { 42 | out[2] = Math.asin(Math.min(Math.max(m[1], -1), 1)); 43 | if (Math.abs(m[1]) < 0.99999) { 44 | out[0] = Math.atan2(-m[9], m[5]); 45 | out[1] = Math.atan2(-m[2], m[0]); 46 | } else { 47 | out[0] = 0; 48 | out[1] = Math.atan2(m[8], m[10]); 49 | } 50 | } else if (order === 'XZY') { 51 | out[2] = Math.asin(-Math.min(Math.max(m[4], -1), 1)); 52 | if (Math.abs(m[4]) < 0.99999) { 53 | out[0] = Math.atan2(m[6], m[5]); 54 | out[1] = Math.atan2(m[8], m[0]); 55 | } else { 56 | out[0] = Math.atan2(-m[9], m[10]); 57 | out[1] = 0; 58 | } 59 | } 60 | 61 | return out; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | O-GL 3 |

4 |
5 | 6 |

IBL Converter For PBR

7 | 8 | 9 |

Generate IBL maps for use with PBR shaders.

10 | 11 |
12 |
13 | 14 | Use this to convert different environment maps into the required formats for use in a PBR render. 15 | 16 | [Link here](https://oframe.github.io/ibl-converter/) 17 | 18 | ## Overview 19 | 20 | Drag and drop an equirectangular environment map to generate the two necessary textures for adding Image Based Lighting to PBR shaders. 21 | 22 | Made for use with the [OGL PBR example](https://oframe.github.io/ogl/examples/?src=pbr.html) shader, however can be plugged into any framework. 23 | 24 | ### Supported input types: 25 | - *.hdr (Radiance/RGBE)*, which outputs a *.png* in *RGBM* format. This conversion takes the HDR (high dynamic range) values and converts them into the RGBA channels of an 8bit PNG (8bit per channel = 32bits); the output PNG will likely look mostly transparent when previewed directly. 26 | 27 | - *.exr (OpenEXR)*, which outputs a *.png* in *RGBM* format. Same output as above. 28 | 29 | - *.jpg/.png (sRGB)* bitmap, which outputs a *.png* in *sRGB* format. This is an SDR (standard dynamic range) format. 30 | 31 | ### Output files: 32 | - Irradiance Diffuse Map. Currently outputting at 128x64, however can likely go smaller with no quality drop. This map is pre-filtered and hence looks very blurry. It gives the average diffuse lighting in a given direction. 33 | 34 | - Radiance Specular Map Atlas. Currently set at 512x512, which is ok for most cases but not close-up reflections. If wished, this setting can be increased in Specular.js. This map is an atlas made up of 7 sequentially prefiltered renders, each half the size of the previous - used to simulate varying roughness levels in the PBR render. The renders are stacked vertically, with the bottom half of the texture being the first, non-filtered level. 35 | 36 | When you hit the download prompt, the two maps will be downloaded to your local file system. The downloaded files use the following naming structure: 37 | ``` 38 | [input filename]-[map type]-[output format].png 39 | ``` 40 | eg. _**sky-diffuse-RGBM.png**_ and _**sky-specular-RGBM.png**_ 41 | 42 | ## TODO 43 | - feature small library of pre-generated maps 44 | - Reduce artifacts at the poles 45 | - Allow user to select output type -------------------------------------------------------------------------------- /ogl/extras/Plane.js: -------------------------------------------------------------------------------- 1 | import {Geometry} from '../core/Geometry.js'; 2 | 3 | export class Plane extends Geometry { 4 | constructor(gl, width = 1, height = width, wSegs = 1, hSegs = wSegs) { 5 | 6 | // Determine length of arrays 7 | const num = (wSegs + 1) * (hSegs + 1); 8 | const numIndices = wSegs * hSegs; 9 | 10 | // Generate empty arrays once 11 | const position = new Float32Array(num * 3); 12 | const normal = new Float32Array(num * 3); 13 | const uv = new Float32Array(num * 2); 14 | const index = new Uint16Array(numIndices * 6); 15 | 16 | Plane.buildPlane(position, normal, uv, index, width, height, 0, wSegs, hSegs); 17 | 18 | super(gl, { 19 | position: {size: 3, data: position}, 20 | normal: {size: 3, data: normal}, 21 | uv: {size: 2, data: uv}, 22 | index: {data: index}, 23 | }); 24 | } 25 | 26 | static buildPlane(position, normal, uv, index, width, height, depth, wSegs, hSegs, 27 | u = 0, v = 1, w = 2, 28 | uDir = 1, vDir = -1, 29 | i = 0, ii = 0 30 | ) { 31 | const io = i; 32 | const segW = width / wSegs; 33 | const segH = height / hSegs; 34 | 35 | for (let iy = 0; iy <= hSegs; iy++) { 36 | let y = iy * segH - height / 2; 37 | for (let ix = 0; ix <= wSegs; ix++, i++) { 38 | let x = ix * segW - width / 2; 39 | 40 | position[i * 3 + u] = x * uDir; 41 | position[i * 3 + v] = y * vDir; 42 | position[i * 3 + w] = depth / 2; 43 | 44 | normal[i * 3 + u] = 0; 45 | normal[i * 3 + v] = 0; 46 | normal[i * 3 + w] = depth >= 0 ? 1 : -1; 47 | 48 | uv[i * 2] = ix / wSegs; 49 | uv[i * 2 + 1] = 1 - iy / hSegs; 50 | 51 | if (iy === hSegs || ix === wSegs) continue; 52 | let a = io + ix + iy * (wSegs + 1); 53 | let b = io + ix + (iy + 1) * (wSegs + 1); 54 | let c = io + ix + (iy + 1) * (wSegs + 1) + 1; 55 | let d = io + ix + iy * (wSegs + 1) + 1; 56 | 57 | index[ii * 6] = a; 58 | index[ii * 6 + 1] = b; 59 | index[ii * 6 + 2] = d; 60 | index[ii * 6 + 3] = b; 61 | index[ii * 6 + 4] = c; 62 | index[ii * 6 + 5] = d; 63 | ii++; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | user-select: none; 4 | -moz-user-select: none; 5 | -webkit-user-select: none; 6 | -o-user-select: none; 7 | -ms-user-select: none; 8 | -webkit-user-drag: none; 9 | } 10 | 11 | body, html { 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | overflow-x: hidden; 15 | height: 100%; 16 | } 17 | 18 | body, button, p, ul { 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | button, select { 24 | border: none; 25 | outline: none; 26 | background: none; 27 | font-family: inherit; 28 | } 29 | 30 | a, button, input, select, textarea { 31 | -webkit-tap-highlight-color: transparent; 32 | } 33 | 34 | a { 35 | color: inherit; /* blue colors for links too */ 36 | text-decoration: inherit; /* no underline */ 37 | } 38 | 39 | :root { 40 | overflow-x: hidden; 41 | height: 100%; 42 | } 43 | 44 | @font-face { 45 | font-family: 'Raleway'; 46 | src: url('raleway-regular-webfont.woff2') format('woff2'), 47 | url('raleway-regular-webfont.woff') format('woff'); 48 | font-weight: 400; 49 | font-style: normal; 50 | } 51 | 52 | @font-face { 53 | font-family: 'Raleway'; 54 | src: url('raleway-bold-webfont.woff2') format('woff2'), 55 | url('raleway-bold-webfont.woff') format('woff'); 56 | font-weight: 700; 57 | font-style: normal; 58 | } 59 | 60 | body { 61 | font-family: 'Raleway', sans-serif; 62 | font-weight: 700; 63 | font-size: 14px; 64 | } 65 | 66 | .Side { padding-left: 20px; width: 280px; height: 100%; overflow: auto; padding-bottom: 100px; } 67 | .Iframe { position: absolute; left: 300px; right: 0; top: 0; bottom: 0; height: 100%; width: calc(100% - 300px); border: none; margin: 0; } 68 | 69 | .Title { font-size: 18px; margin: 20px 0; display: block; } 70 | .SubTitle { font-weight: 400; font-size: 17px; margin: 20px 0 50px; } 71 | .Section { font-weight: 400; margin: 40px 0 20px; padding-left: 15px; } 72 | .Example { margin: 10px 0; padding-left: 30px; display: block; } 73 | div.Example { opacity: 0.2; } 74 | 75 | .CodeIcon { position: absolute; display: block; z-index: 1; bottom: 0; right: 0; margin: 30px; width: 50px; height: 50px; line-height: 50px; text-align: center; background: #fff; border-radius: 100%; font-size: 19px; letter-spacing: -0.1em; box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.1); } 76 | 77 | .Info { padding: 20px; position: relative; z-index: 1; } 78 | canvas { position: absolute; width: 100%; height: 100%; top: 0; left: 0; } -------------------------------------------------------------------------------- /ogl/math/Vec2.js: -------------------------------------------------------------------------------- 1 | import * as Vec2Func from './functions/Vec2Func.js'; 2 | 3 | export class Vec2 extends Float32Array { 4 | constructor(array = [0, 0]) { 5 | if (!array.length) array = [array, array]; 6 | super(array); 7 | return this; 8 | } 9 | 10 | get x() { 11 | return this[0]; 12 | } 13 | 14 | set x(v) { 15 | this[0] = v; 16 | } 17 | 18 | get y() { 19 | return this[1]; 20 | } 21 | 22 | set y(v) { 23 | this[1] = v; 24 | } 25 | 26 | set(x, y = x) { 27 | Vec2Func.set(this, x, y); 28 | return this; 29 | } 30 | 31 | copy(v) { 32 | Vec2Func.copy(this, v); 33 | return this; 34 | } 35 | 36 | add(v) { 37 | Vec2Func.add(this, this, v); 38 | return this; 39 | } 40 | 41 | multiply(m) { 42 | if (m.length) Vec2Func.multiply(this, this, m); 43 | else Vec2Func.scale(this, this, m); 44 | return this; 45 | } 46 | 47 | distance(v) { 48 | if (v) return Vec2Func.distance(this, v); 49 | else return Vec2Func.length(this); 50 | } 51 | 52 | squaredDistance(v) { 53 | if (v) return Vec2Func.squaredDistance(this, v); 54 | else return Vec2Func.squaredLength(this); 55 | } 56 | 57 | squaredLength() { 58 | return this.squaredDistance(); 59 | } 60 | 61 | subtract(va, vb) { 62 | if (vb) Vec2Func.subtract(this, va, vb); 63 | else Vec2Func.subtract(this, this, va); 64 | return this; 65 | } 66 | 67 | negate(v = this) { 68 | Vec2Func.negate(this, v); 69 | return this; 70 | } 71 | 72 | cross(va, vb) { 73 | Vec2Func.cross(this, va, vb); 74 | return this; 75 | } 76 | 77 | scale(v) { 78 | Vec2Func.scale(this, this, v); 79 | return this; 80 | } 81 | 82 | normalize() { 83 | Vec2Func.normalize(this, this); 84 | } 85 | 86 | dot(v) { 87 | return Vec2Func.dot(this, v); 88 | } 89 | 90 | equals(v) { 91 | return Vec2Func.exactEquals(this, v); 92 | } 93 | 94 | applyMatrix3(mat3) { 95 | Vec2Func.transformMat3(this, this, mat3); 96 | return this; 97 | } 98 | 99 | applyMatrix4(mat4) { 100 | Vec2Func.transformMat4(this, this, mat4); 101 | return this; 102 | } 103 | 104 | lerp(v, a) { 105 | Vec2Func.lerp(this, this, v, a); 106 | } 107 | 108 | clone() { 109 | return new Vec2(this); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/shaders/BackgroundShader.js: -------------------------------------------------------------------------------- 1 | const vertex = ` 2 | precision highp float; 3 | precision highp int; 4 | 5 | attribute vec3 position; 6 | attribute vec2 uv; 7 | 8 | uniform mat4 modelViewMatrix; 9 | uniform mat4 projectionMatrix; 10 | 11 | varying vec2 vUv; 12 | 13 | void main() { 14 | vUv = uv; 15 | 16 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 17 | } 18 | `; 19 | 20 | const fragment = ` 21 | precision highp float; 22 | precision highp int; 23 | 24 | uniform sampler2D tMap; 25 | uniform float uInputType; 26 | 27 | varying vec2 vUv; 28 | 29 | vec4 SRGBtoLinear(vec4 srgb) { 30 | vec3 linOut = pow(srgb.xyz, vec3(2.2)); 31 | return vec4(linOut, srgb.w); 32 | } 33 | 34 | vec4 RGBMToLinear(vec4 value) { 35 | float maxRange = 6.0; 36 | return vec4(value.xyz * value.w * maxRange, 1.0); 37 | } 38 | 39 | vec3 linearToSRGB(vec3 color) { 40 | return pow(color, vec3(1.0 / 2.2)); 41 | } 42 | 43 | vec4 RGBDtoLinear(vec4 value) { 44 | float maxRange = 6.0; 45 | return vec4(value.rgb * ((maxRange / 255.0) / value.a), 1.0); 46 | } 47 | 48 | vec4 RGBEtoLinear(vec4 value) { 49 | return vec4(value.rgb * exp2(value.a * 255.0 - 128.0), 1.0); 50 | } 51 | 52 | void main() { 53 | vec4 color; 54 | 55 | // 'If else' statements caused the strangest gpu bug 56 | // if (uInputType < 0.5) { 57 | 58 | // // sRGB == 0 59 | // color = SRGBtoLinear(texture2D(tMap, vUv)); 60 | // } else if (uInputType < 1.5) { 61 | 62 | // // RGBE == 1 63 | // color = RGBEtoLinear(texture2D(tMap, vUv)); 64 | // } else if (uInputType < 2.5) { 65 | 66 | // // RGBM == 2 67 | // color = RGBMToLinear(texture2D(tMap, vUv)); 68 | // } else if (uInputType < 3.5) { 69 | 70 | // // RGBD == 3 71 | // color = RGBDtoLinear(texture2D(tMap, vUv)); 72 | // } 73 | 74 | // sRGB == 0 75 | color = SRGBtoLinear(texture2D(tMap, vUv)); 76 | 77 | // RGBE == 1 78 | float mixRGBE = clamp(1.0 - abs(uInputType - 1.0), 0.0, 1.0); 79 | color = mix(color, RGBEtoLinear(texture2D(tMap, vUv)), mixRGBE); 80 | 81 | // RGBM == 2 82 | float mixRGBM = clamp(1.0 - abs(uInputType - 2.0), 0.0, 1.0); 83 | color = mix(color, RGBMToLinear(texture2D(tMap, vUv)), mixRGBM); 84 | 85 | // RGBD == 3 86 | float mixRGBD = clamp(1.0 - abs(uInputType - 3.0), 0.0, 1.0); 87 | color = mix(color, RGBDtoLinear(texture2D(tMap, vUv)), mixRGBD); 88 | 89 | gl_FragColor = color; 90 | gl_FragColor.rgb = linearToSRGB(gl_FragColor.rgb); 91 | } 92 | `; 93 | 94 | export default {vertex, fragment}; -------------------------------------------------------------------------------- /ogl/core/Mesh.js: -------------------------------------------------------------------------------- 1 | import {Transform} from './Transform.js'; 2 | import {Mat3} from '../math/Mat3.js'; 3 | import {Mat4} from '../math/Mat4.js'; 4 | 5 | export class Mesh extends Transform { 6 | constructor(gl, { 7 | geometry, 8 | program, 9 | mode = gl.TRIANGLES, 10 | } = {}) { 11 | super(gl); 12 | this.gl = gl; 13 | 14 | this.geometry = geometry; 15 | this.program = program; 16 | this.mode = mode; 17 | 18 | this.modelViewMatrix = new Mat4(); 19 | this.normalMatrix = new Mat3(); 20 | 21 | // Add empty matrix uniforms to program if unset 22 | if (!this.program.uniforms.modelMatrix) { 23 | Object.assign(this.program.uniforms, { 24 | modelMatrix: {value: null}, 25 | viewMatrix: {value: null}, 26 | modelViewMatrix: {value: null}, 27 | normalMatrix: {value: null}, 28 | projectionMatrix: {value: null}, 29 | cameraPosition: {value: null}, 30 | }); 31 | } 32 | } 33 | 34 | draw({ 35 | camera, 36 | } = {}) { 37 | this.onBeforeRender && this.onBeforeRender({mesh: this, camera}); 38 | 39 | // Set the matrix uniforms 40 | if (camera) { 41 | this.program.uniforms.projectionMatrix.value = camera.projectionMatrix; 42 | this.program.uniforms.cameraPosition.value = camera.position; 43 | this.program.uniforms.viewMatrix.value = camera.viewMatrix; 44 | 45 | this.modelViewMatrix.multiply(camera.viewMatrix, this.worldMatrix); 46 | this.normalMatrix.getNormalMatrix(this.modelViewMatrix); 47 | 48 | this.program.uniforms.modelMatrix.value = this.matrix; 49 | this.program.uniforms.modelViewMatrix.value = this.modelViewMatrix; 50 | this.program.uniforms.normalMatrix.value = this.normalMatrix; 51 | } 52 | 53 | // determine if faces need to be flipped - when mesh scaled negatively 54 | let flipFaces = this.program.cullFace && this.worldMatrix.determinant() < 0; 55 | 56 | // Check here if any bindings can be skipped. Geometry also needs to be rebound if different program 57 | const programActive = this.gl.renderer.currentProgram === this.program.id; 58 | const geometryBound = programActive && this.gl.renderer.currentGeometry === this.geometry.id; 59 | 60 | this.program.use({programActive, flipFaces}); 61 | this.geometry.draw({mode: this.mode, program: this.program, geometryBound}); 62 | 63 | this.onAfterRender && this.onAfterRender({mesh: this, camera}); 64 | } 65 | } -------------------------------------------------------------------------------- /ogl/math/Quat.js: -------------------------------------------------------------------------------- 1 | import * as QuatFunc from './functions/QuatFunc.js'; 2 | 3 | export class Quat extends Float32Array { 4 | constructor(array = [0, 0, 0, 1]) { 5 | super(array); 6 | this.onChange = () => {}; 7 | return this; 8 | } 9 | 10 | get x() { 11 | return this[0]; 12 | } 13 | 14 | set x(v) { 15 | this[0] = v; 16 | this.onChange(); 17 | } 18 | 19 | get y() { 20 | return this[1]; 21 | } 22 | 23 | set y(v) { 24 | this[1] = v; 25 | this.onChange(); 26 | } 27 | 28 | get z() { 29 | return this[2]; 30 | } 31 | 32 | set z(v) { 33 | this[2] = v; 34 | this.onChange(); 35 | } 36 | 37 | get w() { 38 | return this[3]; 39 | } 40 | 41 | set w(v) { 42 | this[3] = v; 43 | this.onChange(); 44 | } 45 | 46 | identity() { 47 | QuatFunc.identity(this); 48 | this.onChange(); 49 | return this; 50 | } 51 | 52 | set(x, y, z, w) { 53 | QuatFunc.set(this, x, y, z, w); 54 | this.onChange(); 55 | return this; 56 | } 57 | 58 | rotateX(a) { 59 | QuatFunc.rotateX(this, this, a); 60 | this.onChange(); 61 | return this; 62 | } 63 | 64 | rotateY(a) { 65 | QuatFunc.rotateY(this, this, a); 66 | this.onChange(); 67 | return this; 68 | } 69 | 70 | rotateZ(a) { 71 | QuatFunc.rotateZ(this, this, a); 72 | this.onChange(); 73 | return this; 74 | } 75 | 76 | invert(q = this) { 77 | QuatFunc.invert(this, q); 78 | this.onChange(); 79 | return this; 80 | } 81 | 82 | copy(q) { 83 | QuatFunc.copy(this, q); 84 | this.onChange(); 85 | return this; 86 | } 87 | 88 | normalize(q = this) { 89 | QuatFunc.normalize(this, q); 90 | this.onChange(); 91 | return this; 92 | } 93 | 94 | multiply(qA, qB) { 95 | if (qB) { 96 | QuatFunc.multiply(this, qA, qB); 97 | } else { 98 | QuatFunc.multiply(this, this, qA); 99 | } 100 | this.onChange(); 101 | return this; 102 | } 103 | 104 | dot(v) { 105 | return QuatFunc.dot(this, v); 106 | } 107 | 108 | fromMatrix3(matrix3) { 109 | QuatFunc.fromMat3(this, matrix3); 110 | this.onChange(); 111 | return this; 112 | } 113 | 114 | fromEuler(euler) { 115 | QuatFunc.fromEuler(this, euler, euler.order); 116 | return this; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ogl/math/Vec3.js: -------------------------------------------------------------------------------- 1 | import * as Vec3Func from './functions/Vec3Func.js'; 2 | 3 | export class Vec3 extends Float32Array { 4 | constructor(array = [0, 0, 0]) { 5 | if (!array.length) array = [array, array, array]; 6 | super(array); 7 | return this; 8 | } 9 | 10 | get x() { 11 | return this[0]; 12 | } 13 | 14 | set x(v) { 15 | this[0] = v; 16 | } 17 | 18 | get y() { 19 | return this[1]; 20 | } 21 | 22 | set y(v) { 23 | this[1] = v; 24 | } 25 | 26 | get z() { 27 | return this[2]; 28 | } 29 | 30 | set z(v) { 31 | this[2] = v; 32 | } 33 | 34 | set(x, y = x, z = x) { 35 | Vec3Func.set(this, x, y, z); 36 | return this; 37 | } 38 | 39 | copy(v) { 40 | Vec3Func.copy(this, v); 41 | return this; 42 | } 43 | 44 | add(v) { 45 | Vec3Func.add(this, this, v); 46 | return this; 47 | } 48 | 49 | multiply(m) { 50 | if (m.length) Vec3Func.multiply(this, this, m); 51 | else Vec3Func.scale(this, this, m); 52 | return this; 53 | } 54 | 55 | length() { 56 | return Vec3Func.length(this); 57 | } 58 | 59 | distance(v) { 60 | return Vec3Func.distance(this, v); 61 | } 62 | 63 | squaredDistance(v) { 64 | if (v) return Vec3Func.squaredDistance(this, v); 65 | else return Vec3Func.squaredLength(this); 66 | } 67 | 68 | squaredLength() { 69 | return this.squaredDistance(); 70 | } 71 | 72 | subtract(va, vb) { 73 | if (vb) Vec3Func.subtract(this, va, vb); 74 | else Vec3Func.subtract(this, this, va); 75 | return this; 76 | } 77 | 78 | negate(v = this) { 79 | Vec3Func.negate(this, v); 80 | return this; 81 | } 82 | 83 | cross(va, vb) { 84 | Vec3Func.cross(this, va, vb); 85 | return this; 86 | } 87 | 88 | scale(v) { 89 | Vec3Func.scale(this, this, v); 90 | return this; 91 | } 92 | 93 | normalize() { 94 | Vec3Func.normalize(this, this); 95 | return this; 96 | } 97 | 98 | dot(v) { 99 | return Vec3Func.dot(this, v); 100 | } 101 | 102 | equals(v) { 103 | return Vec3Func.exactEquals(this, v); 104 | } 105 | 106 | applyMatrix4(mat4) { 107 | Vec3Func.transformMat4(this, this, mat4); 108 | return this; 109 | } 110 | 111 | angle(v) { 112 | return Vec3Func.angle(this, v); 113 | } 114 | 115 | lerp(v, t) { 116 | Vec3Func.lerp(this, this, v, t); 117 | return this; 118 | } 119 | 120 | clone() { 121 | return new Vec3(this); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /ogl/extras/Sphere.js: -------------------------------------------------------------------------------- 1 | import {Geometry} from '../core/Geometry.js'; 2 | import {Vec3} from '../math/Vec3.js'; 3 | 4 | export class Sphere extends Geometry { 5 | constructor(gl, radius = 0.5, wSegs = 16, hSegs = Math.ceil(wSegs * 0.5), pStart = 0, pLength = Math.PI * 2, tStart = 0, tLength = Math.PI) { 6 | const num = (wSegs + 1) * (hSegs + 1); 7 | const numIndices = wSegs * hSegs * 6; 8 | 9 | const position = new Float32Array(num * 3); 10 | const normal = new Float32Array(num * 3); 11 | const uv = new Float32Array(num * 2); 12 | const index = new Uint16Array(numIndices); 13 | 14 | let i = 0; 15 | let iv = 0; 16 | let ii = 0; 17 | let te = tStart + tLength; 18 | const grid = []; 19 | 20 | let n = new Vec3(); 21 | 22 | for (let iy = 0; iy <= hSegs; iy++) { 23 | let vRow = []; 24 | let v = iy / hSegs; 25 | for (let ix = 0; ix <= wSegs; ix++, i++) { 26 | let u = ix / wSegs; 27 | let x = -radius * Math.cos(pStart + u * pLength) * Math.sin(tStart + v * tLength); 28 | let y = radius * Math.cos(tStart + v * tLength); 29 | let z = radius * Math.sin(pStart + u * pLength) * Math.sin(tStart + v * tLength); 30 | 31 | position[i * 3] = x; 32 | position[i * 3 + 1] = y; 33 | position[i * 3 + 2] = z; 34 | 35 | n.set(x, y, z).normalize(); 36 | normal[i * 3] = n.x; 37 | normal[i * 3 + 1] = n.y; 38 | normal[i * 3 + 2] = n.z; 39 | 40 | uv[i * 2] = u; 41 | uv[i * 2 + 1] = 1 - v; 42 | 43 | vRow.push(iv++); 44 | } 45 | 46 | grid.push(vRow); 47 | } 48 | 49 | for (let iy = 0; iy < hSegs; iy++) { 50 | for (let ix = 0; ix < wSegs; ix++) { 51 | let a = grid[iy][ix + 1]; 52 | let b = grid[iy][ix]; 53 | let c = grid[iy + 1][ix]; 54 | let d = grid[iy + 1][ix + 1]; 55 | 56 | if (iy !== 0 || tStart > 0) { 57 | index[ii * 3] = a; 58 | index[ii * 3 + 1] = b; 59 | index[ii * 3 + 2] = d; 60 | ii++; 61 | } 62 | if (iy !== hSegs - 1 || te < Math.PI) { 63 | index[ii * 3] = b; 64 | index[ii * 3 + 1] = c; 65 | index[ii * 3 + 2] = d; 66 | ii++; 67 | } 68 | } 69 | } 70 | 71 | super(gl, { 72 | position: {size: 3, data: position}, 73 | normal: {size: 3, data: normal}, 74 | uv: {size: 2, data: uv}, 75 | index: {data: index}, 76 | }); 77 | } 78 | } -------------------------------------------------------------------------------- /ogl/core/Transform.js: -------------------------------------------------------------------------------- 1 | import {Vec3} from '../math/Vec3.js'; 2 | import {Quat} from '../math/Quat.js'; 3 | import {Mat4} from '../math/Mat4.js'; 4 | import {Euler} from '../math/Euler.js'; 5 | 6 | export class Transform { 7 | constructor() { 8 | this.parent = null; 9 | this.children = []; 10 | 11 | this.matrix = new Mat4(); 12 | this.worldMatrix = new Mat4(); 13 | this.matrixAutoUpdate = true; 14 | 15 | this.position = new Vec3(); 16 | this.quaternion = new Quat(); 17 | this.scale = new Vec3([1, 1, 1]); 18 | this.rotation = new Euler(); 19 | this.up = new Vec3([0, 1, 0]); 20 | 21 | this.rotation.onChange = () => this.quaternion.fromEuler(this.rotation); 22 | this.quaternion.onChange = () => this.rotation.fromQuaternion(this.quaternion); 23 | } 24 | 25 | setParent(parent, notifyChild = true) { 26 | if (!parent && notifyChild) this.parent.removeChild(this, false); 27 | this.parent = parent; 28 | if (parent && notifyChild) parent.addChild(this, false); 29 | } 30 | 31 | addChild(child, notifyParent = true) { 32 | if (!~this.children.indexOf(child)) this.children.push(child); 33 | if (notifyParent) child.setParent(this, false); 34 | } 35 | 36 | removeChild(child, notifyParent = true) { 37 | if (!!~this.children.indexOf(child)) this.children.splice(this.children.indexOf(child), 1); 38 | if (notifyParent) child.setParent(null, false); 39 | } 40 | 41 | updateMatrixWorld(force) { 42 | if (this.matrixAutoUpdate) this.updateMatrix(); 43 | if (this.worldMatrixNeedsUpdate || force) { 44 | if (this.parent === null) this.worldMatrix.copy(this.matrix); 45 | else this.worldMatrix.multiply(this.parent.worldMatrix, this.matrix); 46 | this.worldMatrixNeedsUpdate = false; 47 | force = true; 48 | } 49 | 50 | let children = this.children; 51 | for (let i = 0, l = children.length; i < l; i ++) { 52 | children[i].updateMatrixWorld(force); 53 | } 54 | } 55 | 56 | updateMatrix() { 57 | this.matrix.compose(this.quaternion, this.position, this.scale); 58 | this.worldMatrixNeedsUpdate = true; 59 | } 60 | 61 | traverse(callback) { 62 | callback(this); 63 | for (let i = 0, l = this.children.length; i < l; i ++) { 64 | this.children[i].traverse(callback); 65 | } 66 | } 67 | 68 | decompose() { 69 | this.matrix.getTranslation(this.position); 70 | this.matrix.getRotation(this.quaternion); 71 | this.matrix.getScaling(this.scale); 72 | this.rotation.fromQuaternion(this.quaternion); 73 | } 74 | 75 | lookAt(target, invert = false) { 76 | if (invert) this.matrix.lookAt(this.position, target, this.up); 77 | else this.matrix.lookAt(target, this.position, this.up); 78 | this.matrix.getRotation(this.quaternion); 79 | this.rotation.fromQuaternion(this.quaternion); 80 | }; 81 | } -------------------------------------------------------------------------------- /src/Specular.js: -------------------------------------------------------------------------------- 1 | import { Renderer, Plane, Texture, RenderTarget, Program, Mesh } from '../ogl/OGL.js'; 2 | import SpecularShader from './shaders/SpecularShader.js'; 3 | import AtlasShader from './shaders/AtlasShader.js'; 4 | 5 | let OUTPUT_SIZE = 512; 6 | 7 | // sRGB == 0 8 | // RGBE == 1 9 | // RGBM == 2 10 | // RGBD == 3 11 | let INPUT_TYPE = 1; 12 | let OUTPUT_TYPE = 2; 13 | 14 | const renderer = new Renderer({ alpha: true, premultipliedAlpha: true }); 15 | renderer.setSize(OUTPUT_SIZE, OUTPUT_SIZE); 16 | export const gl = renderer.gl; 17 | gl.clearColor(0, 0, 0, 0); 18 | // document.body.appendChild(gl.canvas); 19 | 20 | const geometry = new Plane(gl, 2, 2); 21 | 22 | const specularProgram = new Program(gl, { 23 | vertexShader: SpecularShader.vertex, 24 | fragmentShader: SpecularShader.fragment, 25 | uniforms: { 26 | tMap: { value: null }, 27 | uRoughness: { value: 0 }, 28 | uInputType: { value: 0 }, 29 | uOutputType: { value: 0 }, 30 | }, 31 | transparent: true, 32 | }); 33 | 34 | const specularMesh = new Mesh(gl, { geometry, program: specularProgram }); 35 | 36 | const atlasProgram = new Program(gl, { 37 | vertexShader: AtlasShader.vertex, 38 | fragmentShader: AtlasShader.fragment, 39 | uniforms: { 40 | tMap: { value: null }, 41 | }, 42 | transparent: true, 43 | }); 44 | 45 | const atlasMesh = new Mesh(gl, { geometry, program: atlasProgram }); 46 | 47 | export function renderSpecular(data) { 48 | INPUT_TYPE = data.inputType; 49 | OUTPUT_TYPE = data.outputType; 50 | 51 | // Create input texture 52 | const texture = new Texture(gl, { 53 | image: data.data, 54 | width: data.width, 55 | height: data.height, 56 | generateMipmaps: false, 57 | }); 58 | 59 | let size = OUTPUT_SIZE; 60 | let num = 6; // Don't see why you'd want more than that 61 | // let num = Math.log(size) / Math.log(2) - 2; 62 | 63 | // Create RenderTargets for each roughness level 64 | const targets = []; 65 | 66 | for (var i = 0; i < num; i++) { 67 | const target = new RenderTarget(gl, { 68 | width: size, 69 | height: size / 2, 70 | wrapS: gl.REPEAT, 71 | // FLOAT, RGBFormat 72 | }); 73 | targets.push(target); 74 | 75 | size = Math.max(16, size / 2); 76 | } 77 | 78 | // Update program with new texture and values 79 | specularProgram.uniforms.tMap.value = texture; 80 | specularProgram.uniforms.uInputType.value = INPUT_TYPE; 81 | specularProgram.uniforms.uOutputType.value = OUTPUT_TYPE; 82 | 83 | // randiance maps for specular 84 | targets.forEach((target, i) => { 85 | renderer.setSize(target.width, target.height); 86 | 87 | var r = i / (targets.length - 1) || 0; 88 | specularProgram.uniforms.uRoughness.value = r * 0.9; 89 | 90 | renderer.render({ scene: specularMesh, target }); 91 | specularProgram.uniforms.tMap.value = target.texture; 92 | 93 | if (i === 0) specularProgram.uniforms.uInputType.value = specularProgram.uniforms.uOutputType.value; 94 | }); 95 | 96 | // Update atlas textures 97 | atlasProgram.uniforms.tMap.value = targets.map((target) => target.texture); 98 | 99 | // Render all targets to atlas output 100 | renderer.setSize(OUTPUT_SIZE, OUTPUT_SIZE); 101 | 102 | // Need to render twice when on external server - I have no idea why 103 | renderer.render({ scene: atlasMesh }); 104 | renderer.render({ scene: atlasMesh }); 105 | } 106 | -------------------------------------------------------------------------------- /ogl/math/Mat4.js: -------------------------------------------------------------------------------- 1 | import * as Mat4Func from './functions/Mat4Func.js'; 2 | 3 | export class Mat4 extends Float32Array { 4 | constructor(array = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) { 5 | super(array); 6 | return this; 7 | } 8 | 9 | set x(v) { 10 | this[12] = v; 11 | } 12 | 13 | get x() { 14 | return this[12]; 15 | } 16 | 17 | set y(v) { 18 | this[13] = v; 19 | } 20 | 21 | get y() { 22 | return this[13]; 23 | } 24 | 25 | set z(v) { 26 | this[14] = v; 27 | } 28 | 29 | get z() { 30 | return this[14]; 31 | } 32 | 33 | set w(v) { 34 | this[15] = v; 35 | } 36 | 37 | get w() { 38 | return this[15]; 39 | } 40 | 41 | set(m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33) { 42 | if (m00.length) { 43 | return this.copy(m00); 44 | } 45 | Mat4Func.set(this, m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33); 46 | return this; 47 | } 48 | 49 | translate(v, m = this) { 50 | Mat4Func.translate(this, m, v); 51 | return this; 52 | } 53 | 54 | rotateX(v, m = this) { 55 | Mat4Func.rotateX(this, m, v); 56 | return this; 57 | } 58 | 59 | rotateY(v, m = this) { 60 | Mat4Func.rotateY(this, m, v); 61 | return this; 62 | } 63 | 64 | rotateZ(v, m = this) { 65 | Mat4Func.rotateZ(this, m, v); 66 | return this; 67 | } 68 | 69 | scale(v, m = this) { 70 | Mat4Func.scale(this, m, typeof v === "number" ? [v, v, v] : v); 71 | return this; 72 | } 73 | 74 | multiply(ma, mb) { 75 | if (mb) { 76 | Mat4Func.multiply(this, ma, mb); 77 | } else { 78 | Mat4Func.multiply(this, this, ma); 79 | } 80 | return this; 81 | } 82 | 83 | identity() { 84 | Mat4Func.identity(this); 85 | return this; 86 | } 87 | 88 | copy(m) { 89 | Mat4Func.copy(this, m); 90 | return this; 91 | } 92 | 93 | fromPerspective({fov, aspect, near, far} = {}) { 94 | Mat4Func.perspective(this, fov, aspect, near, far); 95 | return this; 96 | } 97 | 98 | fromOrthogonal({left, right, bottom, top, near, far}) { 99 | Mat4Func.ortho(this, left, right, bottom, top, near, far); 100 | return this; 101 | } 102 | 103 | fromQuaternion(q) { 104 | Mat4Func.fromQuat(this, q); 105 | return this; 106 | } 107 | 108 | setPosition(v) { 109 | this.x = v[0]; 110 | this.y = v[1]; 111 | this.z = v[2]; 112 | return this; 113 | } 114 | 115 | invert(m = this) { 116 | Mat4Func.invert(this, m); 117 | return this; 118 | } 119 | 120 | compose(q, pos, scale) { 121 | Mat4Func.fromRotationTranslationScale(this, q, pos, scale); 122 | return this; 123 | } 124 | 125 | getRotation(q) { 126 | Mat4Func.getRotation(q, this); 127 | return this; 128 | } 129 | 130 | getTranslation(pos) { 131 | Mat4Func.getTranslation(pos, this); 132 | return this; 133 | } 134 | 135 | getScaling(scale) { 136 | Mat4Func.getScaling(scale, this); 137 | return this; 138 | } 139 | 140 | lookAt(eye, target, up) { 141 | Mat4Func.targetTo(this, eye, target, up); 142 | return this; 143 | } 144 | 145 | determinant() { 146 | return Mat4Func.determinant(this); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /ogl/core/RenderTarget.js: -------------------------------------------------------------------------------- 1 | // TODO: multi target rendering 2 | // TODO: test stencil and depth 3 | // TODO: different type and format 4 | import {Texture} from './Texture.js'; 5 | 6 | export class RenderTarget { 7 | constructor(gl, { 8 | width = gl.canvas.width, 9 | height = gl.canvas.height, 10 | 11 | target = gl.FRAMEBUFFER, 12 | color = 1, // number of color attachments 13 | depth = true, 14 | stencil = false, 15 | depthTexture = false, // note - stencil breaks 16 | 17 | wrapS = gl.CLAMP_TO_EDGE, 18 | wrapT = gl.CLAMP_TO_EDGE, 19 | minFilter = gl.LINEAR, 20 | magFilter = gl.LINEAR, 21 | 22 | } = {}) { 23 | this.gl = gl; 24 | this.width = width; 25 | this.height = height; 26 | this.buffer = this.gl.createFramebuffer(); 27 | this.target = target; 28 | this.gl.bindFramebuffer(this.target, this.buffer); 29 | 30 | this.textures = []; 31 | 32 | // TODO: multi target rendering 33 | // create and attach required num of color textures 34 | for (let i = 0; i < color; i++) { 35 | this.textures.push(new Texture(gl, { 36 | width, height, wrapS, wrapT, minFilter, magFilter, 37 | flipY: false, 38 | generateMipmaps: false, 39 | })); 40 | this.textures[i].update(); 41 | this.gl.framebufferTexture2D(this.target, this.gl.COLOR_ATTACHMENT0 + i, this.gl.TEXTURE_2D, this.textures[i].texture, 0 /* level */); 42 | } 43 | 44 | // alias for majority of use cases 45 | this.texture = this.textures[0]; 46 | 47 | // note depth textures break stencil - so can't use together 48 | if (depthTexture && (this.gl.renderer.isWebgl2 || this.gl.renderer.getExtension('WEBGL_depth_texture'))) { 49 | this.depthTexture = new Texture(gl, { 50 | width, height, wrapS, wrapT, 51 | minFilter: this.gl.NEAREST, 52 | magFilter: this.gl.NEAREST, 53 | flipY: false, 54 | format: this.gl.DEPTH_COMPONENT, 55 | internalFormat: gl.renderer.isWebgl2 ? this.gl.DEPTH_COMPONENT24 : this.gl.DEPTH_COMPONENT, 56 | type: this.gl.UNSIGNED_INT, 57 | generateMipmaps: false, 58 | }); 59 | this.depthTexture.update(); 60 | this.gl.framebufferTexture2D(this.target, this.gl.DEPTH_ATTACHMENT, this.gl.TEXTURE_2D, this.depthTexture.texture, 0 /* level */); 61 | } else { 62 | 63 | // Render buffers 64 | if (depth && !stencil) { 65 | this.depthBuffer = this.gl.createRenderbuffer(); 66 | this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, this.depthBuffer); 67 | this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, width, height); 68 | this.gl.framebufferRenderbuffer(this.target, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, this.depthBuffer); 69 | } 70 | 71 | if (stencil && !depth) { 72 | this.stencilBuffer = this.gl.createRenderbuffer(); 73 | this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, this.stencilBuffer); 74 | this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.STENCIL_INDEX8, width, height); 75 | this.gl.framebufferRenderbuffer(this.target, this.gl.STENCIL_ATTACHMENT, this.gl.RENDERBUFFER, this.stencilBuffer); 76 | } 77 | 78 | if (depth && stencil) { 79 | this.depthStencilBuffer = this.gl.createRenderbuffer(); 80 | this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, this.depthStencilBuffer); 81 | this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_STENCIL, width, height); 82 | this.gl.framebufferRenderbuffer(this.target, this.gl.DEPTH_STENCIL_ATTACHMENT, this.gl.RENDERBUFFER, this.depthStencilBuffer); 83 | } 84 | } 85 | 86 | this.gl.bindFramebuffer(this.target, null); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/PNG.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Converts a Typed Array into an RGBA PNG. 4 | Lossless, non-compressed, non-filtered. 5 | Just the raw data wrapped in the necessary zlib headers. 6 | 7 | */ 8 | 9 | import { ArrayBufferWalker } from './PNGWalker.js'; 10 | 11 | export function writePNG(width, height, data) { 12 | let w = width; 13 | let h = height; 14 | 15 | const PRE_HEADER = '\x89PNG\r\n\x1A\n'; 16 | const BLOCK_SIZE = 65535; 17 | const dataLength = w * h * 4 + h; // add extra height for filter byte per scanline 18 | const numberOfBlocks = Math.ceil(dataLength / BLOCK_SIZE); 19 | const zlibDataLength = (() => { 20 | return 1 + // Compression method/flags code 21 | 1 + // Additional flags/check bits 22 | (5 * numberOfBlocks) + // Number of Zlib block headers we'll need 23 | 4 + // ADLER checksum 24 | dataLength; // actual data 25 | })(); 26 | 27 | const arrayBufferLength = 28 | 29 | // Header 30 | 8 + 31 | 32 | // IHDR 33 | 4 + // Chunk length identifier 34 | 4 + // chunk header 35 | 13 + // actual IHDR length 36 | 4 + // CRC32 check; 37 | 38 | // IDAT 39 | 4 + // chunk length 40 | 4 + // "IDAT" 41 | zlibDataLength + // raw data wrapped in the zlib shit 42 | 4 + // CRC 43 | 44 | // IEND 45 | 4 + // "IEND" 46 | 4 + // CRC 47 | 4; // length 48 | 49 | const buffer = new ArrayBuffer(arrayBufferLength); 50 | const walker = new ArrayBufferWalker(buffer); 51 | 52 | // Header 53 | walker.writeString(PRE_HEADER); 54 | 55 | // IHDR 56 | walker.writeUint32(13); // IDHR is always 13 bytes 57 | walker.startCRC(); 58 | walker.writeString("IHDR"); 59 | walker.writeUint32(w); 60 | walker.writeUint32(h); 61 | walker.writeUint8(8); // bitDepth 62 | walker.writeUint8(6); // color type. 6 = RGBA 63 | walker.writeUint8(0); // compressionMethod. 0 = none 64 | walker.writeUint8(0); // filter. 0 = none 65 | walker.writeUint8(0); // interface. 0 = none 66 | walker.writeCRC(); 67 | 68 | // IDAT (data) 69 | walker.writeUint32(zlibDataLength); 70 | walker.startCRC(); 71 | walker.writeString("IDAT"); 72 | 73 | // zlib header 74 | walker.writeUint8(120); 75 | walker.writeUint8(1); 76 | 77 | let bytesLeft = dataLength; 78 | let bytesLeftInWindow = 0; 79 | 80 | function startBlock() { 81 | 82 | // Whether this is the final block. If we've got less than 32KB to write, then yes. 83 | let bfinal = bytesLeft < BLOCK_SIZE ? 1 : 0; 84 | 85 | // Compression type. Will always be zero = uncompressed 86 | let btype = 0; 87 | walker.writeUint8((bfinal) | (btype << 1)); 88 | 89 | // Again, this logic comes from: https://github.com/imaya/zlib.js/blob/master/src/deflate.js#L110 90 | let blockLength = Math.min(bytesLeft, BLOCK_SIZE); 91 | let nlen = (~blockLength + 0x10000) & 0xffff; 92 | // IMPORTANT: these values must be little-endian. 93 | walker.writeUint16(blockLength, true); 94 | walker.writeUint16(nlen, true); 95 | 96 | bytesLeftInWindow = Math.min(bytesLeft, BLOCK_SIZE); 97 | } 98 | 99 | function writeBlockData(val) { 100 | if (bytesLeft <= 0) { 101 | throw new Error('Ran out of space'); 102 | } 103 | if (bytesLeftInWindow === 0) { 104 | walker.pauseAdler(); 105 | startBlock(); 106 | walker.startAdler(); 107 | } 108 | walker.writeUint8(val); 109 | bytesLeftInWindow--; 110 | bytesLeft--; 111 | } 112 | 113 | startBlock(); 114 | walker.startAdler(); 115 | 116 | // Read rows back-to-front as gl.readPixels method flips canvas 117 | for (let i = h - 1; i >= 0; i--) { 118 | writeBlockData(0); // filter type per scanline 119 | for (let j = 0; j < w; j++) { 120 | let pixel = i * w + j; 121 | writeBlockData(data[pixel * 4 + 0]); // red 122 | writeBlockData(data[pixel * 4 + 1]); // green 123 | writeBlockData(data[pixel * 4 + 2]); // blue 124 | writeBlockData(data[pixel * 4 + 3]); // alpha 125 | } 126 | } 127 | 128 | walker.writeAdler(); 129 | walker.writeCRC(); 130 | 131 | // IEND 132 | walker.writeUint32(0); 133 | walker.startCRC(); 134 | walker.writeString('IEND'); 135 | walker.writeCRC(); 136 | 137 | return buffer; 138 | } -------------------------------------------------------------------------------- /src/PBR.js: -------------------------------------------------------------------------------- 1 | import { 2 | Renderer, 3 | Transform, 4 | Camera, 5 | Orbit, 6 | Geometry, 7 | Sphere, 8 | Cube, 9 | Texture, 10 | Program, 11 | Mesh, 12 | Color, 13 | Vec3, 14 | } from '../ogl/OGL.js'; 15 | import { convertGLSL } from './GLSLVersion.js'; 16 | import PBRShader from './shaders/PBRShader.js'; 17 | import BackgroundShader from './shaders/BackgroundShader.js'; 18 | 19 | const OUTPUT_SIZE = 512; 20 | 21 | // sRGB == 0 22 | // RGBE == 1 23 | // RGBM == 2 24 | // RGBD == 3 25 | let INPUT_TYPE = 0; 26 | 27 | const renderer = new Renderer({ dpr: 2 }); 28 | renderer.setSize(OUTPUT_SIZE, OUTPUT_SIZE); 29 | const gl = renderer.gl; 30 | document.body.appendChild(gl.canvas); 31 | 32 | const camera = new Camera(gl, { fov: 35 }); 33 | camera.position.set(2, 0.5, 3); 34 | 35 | // Create controls and pass parameters 36 | const controls = new Orbit(camera, { 37 | enableZoom: false, 38 | }); 39 | 40 | function resize() { 41 | renderer.setSize(window.innerWidth, window.innerHeight); 42 | camera.perspective({ aspect: gl.canvas.width / gl.canvas.height }); 43 | } 44 | window.addEventListener('resize', resize, false); 45 | resize(); 46 | 47 | const scene = new Transform(); 48 | 49 | const program = new Program(gl, { 50 | vertexShader: PBRShader.vertex, 51 | fragmentShader: PBRShader.fragment, 52 | uniforms: { 53 | uBaseColor: { value: new Color([1, 1, 1]) }, 54 | 55 | tRMO: { value: getTexture('src/assets/materials/hammered-metal-mro.jpg') }, 56 | uRoughness: { value: 1 }, 57 | uMetallic: { value: 1 }, 58 | uOcclusion: { value: 1 }, 59 | 60 | tNormal: { value: getTexture('src/assets/materials/hammered-metal-normal.jpg') }, 61 | uNormalScale: { value: 2 }, 62 | uNormalUVScale: { value: 3 }, 63 | 64 | tLUT: { value: getTexture('src/assets/lut.png', false) }, 65 | 66 | tEnvDiffuse: { value: getTexture('src/assets/interior-diffuse-RGBM.png', false) }, 67 | tEnvSpecular: { value: getTexture('src/assets/interior-specular-RGBM.png', false) }, 68 | uEnvSpecular: { value: 1.0 }, 69 | 70 | uInputType: { value: 2 }, 71 | 72 | uLightDirection: { value: new Color([1, 1, 1]) }, 73 | uLightColor: { value: new Vec3(1) }, 74 | }, 75 | }); 76 | 77 | loadShaderBall(); 78 | async function loadShaderBall() { 79 | const data = await (await fetch(`src/assets/shaderball.json`)).json(); 80 | 81 | const geometry = new Geometry(gl, { 82 | position: { size: 3, data: new Float32Array(data.position) }, 83 | uv: { size: 2, data: new Float32Array(data.uv) }, 84 | normal: { size: 3, data: new Float32Array(data.normal) }, 85 | }); 86 | 87 | const mesh = new Mesh(gl, { geometry, program }); 88 | mesh.position.y = -0.5; 89 | mesh.setParent(scene); 90 | } 91 | 92 | const bgGeometry = new Sphere(gl, 1, 32); 93 | 94 | const bgProgram = new Program(gl, { 95 | vertexShader: convertGLSL(gl, BackgroundShader.vertex), 96 | fragmentShader: convertGLSL(gl, BackgroundShader.fragment), 97 | uniforms: { 98 | tMap: { value: getTexture('src/assets/interior-diffuse-RGBM.png', false) }, 99 | 100 | uInputType: { value: 2 }, 101 | }, 102 | cullFace: gl.FRONT, 103 | }); 104 | 105 | const background = new Mesh(gl, { geometry: bgGeometry, program: bgProgram }); 106 | background.scale.set(5); 107 | background.setParent(scene); 108 | 109 | function getTexture(src, generateMipmaps = true) { 110 | const texture = new Texture(gl, { generateMipmaps }); 111 | texture.wrapS = texture.wrapT = gl.REPEAT; 112 | const image = new Image(); 113 | image.onload = () => { 114 | texture.image = image; 115 | }; 116 | image.src = src; 117 | 118 | return texture; 119 | } 120 | 121 | export function initPBR() { 122 | animate(); 123 | function animate() { 124 | requestAnimationFrame(animate); 125 | 126 | controls.update(); 127 | renderer.render({ scene, camera }); 128 | } 129 | } 130 | 131 | export function updateIBL(specularData, diffuseData, inputType) { 132 | const diffuse = getTexture(URL.createObjectURL(new Blob([diffuseData], { type: 'image/png' })), false); 133 | const specular = getTexture(URL.createObjectURL(new Blob([specularData], { type: 'image/png' })), false); 134 | 135 | bgProgram.uniforms.tMap.value = diffuse; 136 | bgProgram.uniforms.uInputType.value = inputType; 137 | 138 | program.uniforms.tEnvDiffuse.value = diffuse; 139 | program.uniforms.tEnvSpecular.value = specular; 140 | program.uniforms.uInputType.value = inputType; 141 | } 142 | -------------------------------------------------------------------------------- /src/shaders/DiffuseShader.js: -------------------------------------------------------------------------------- 1 | const vertex = ` 2 | precision highp float; 3 | precision highp int; 4 | 5 | attribute vec3 position; 6 | attribute vec2 uv; 7 | 8 | varying vec2 vUv; 9 | 10 | void main() { 11 | vUv = uv; 12 | 13 | gl_Position = vec4(position, 1.0); 14 | } 15 | `; 16 | 17 | const fragment = ` 18 | precision highp float; 19 | precision highp int; 20 | 21 | uniform sampler2D tMap; 22 | uniform float uInputType; 23 | uniform float uOutputType; 24 | 25 | varying vec2 vUv; 26 | 27 | const float PI = 3.14159265359; 28 | const float PI2 = 6.28318530718; 29 | const float RECIPROCAL_PI = 0.31830988618; 30 | const float RECIPROCAL_PI2 = 0.15915494; 31 | 32 | vec4 SRGBtoLinear(vec4 srgb) { 33 | vec3 linOut = pow(srgb.xyz, vec3(2.2)); 34 | return vec4(linOut, srgb.w);; 35 | } 36 | 37 | vec4 linearToSRGB(vec4 color) { 38 | return vec4(pow(color.rgb, vec3(1.0 / 2.2)), color.a); 39 | } 40 | 41 | vec4 RGBEToLinear(in vec4 value) { 42 | return vec4(value.rgb * exp2(value.a * 255.0 - 128.0), 1.0); 43 | } 44 | 45 | vec4 LinearToRGBE(in vec3 value) { 46 | float maxComponent = max(max(value.r, value.g), value.b); 47 | float fExp = clamp(ceil(log2(maxComponent)), -128.0, 127.0); 48 | return vec4(value.rgb / exp2(fExp), (fExp + 128.0) / 255.0); 49 | } 50 | 51 | vec4 RGBMToLinear(in vec4 value) { 52 | float maxRange = 6.0; 53 | return vec4(value.xyz * value.w * maxRange, 1.0); 54 | } 55 | 56 | vec4 LinearToRGBM(in vec3 value) { 57 | float maxRange = 6.0; 58 | float maxRGB = max(value.x, max(value.g, value.b)); 59 | float M = clamp(maxRGB / maxRange, 0.0, 1.0); 60 | M = ceil(M * 255.0) / 255.0; 61 | return vec4(value.rgb / (M * maxRange), M); 62 | } 63 | 64 | vec4 RGBDToLinear(in vec4 value, in float maxRange) { 65 | return vec4(value.rgb * ((maxRange / 255.0) / value.a), 1.0); 66 | } 67 | 68 | vec4 LinearToRGBD(in vec3 value, in float maxRange) { 69 | float maxRGB = max(value.x, max(value.g, value.b)); 70 | float D = max(maxRange / maxRGB, 1.0); 71 | D = min(floor(D) / 255.0, 1.0); 72 | return vec4(value.rgb * (D * (255.0 / maxRange)), D); 73 | } 74 | 75 | vec2 cartesianToPolar(vec3 n) { 76 | vec2 uv; 77 | uv.x = atan(n.z, n.x) * RECIPROCAL_PI2 + 0.5; 78 | uv.y = asin(n.y) * RECIPROCAL_PI + 0.5; 79 | return uv; 80 | } 81 | 82 | vec3 polarToCartesian(vec2 uv) { 83 | float theta = (uv.x - 0.5) * PI2; 84 | float phi = (uv.y) * PI; 85 | 86 | vec3 n; 87 | n.x = sin(phi) * cos(theta); 88 | n.z = sin(phi) * sin(theta); 89 | n.y = -cos(phi); 90 | return normalize(n); 91 | } 92 | 93 | void main() { 94 | vec2 uv = vUv; 95 | vec3 normal = polarToCartesian(uv); 96 | vec3 irradiance; 97 | 98 | vec3 up = vec3(0.0, 1.0, 0.0); 99 | vec3 right = cross(up, normal); 100 | up = cross(normal, right); 101 | 102 | const float delta = 0.025; 103 | float samples = 0.0; 104 | for(float phi = 0.0; phi < PI2; phi += delta) { 105 | for(float theta = 0.0; theta < 0.5 * PI; theta += delta) { 106 | vec3 tangent = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); 107 | vec3 dir = tangent.x * right + tangent.y * up + tangent.z * normal; 108 | 109 | vec3 sample; 110 | if (uInputType < 0.5) { 111 | 112 | // sRGB == 0 113 | sample = SRGBtoLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 114 | } else if (uInputType < 1.5) { 115 | 116 | // RGBE == 1 117 | sample = RGBEToLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 118 | } else if (uInputType < 2.5) { 119 | 120 | // RGBM == 2 121 | sample = RGBMToLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 122 | } else if (uInputType < 3.5) { 123 | 124 | // RGBD == 3 125 | sample = RGBDToLinear(texture2D(tMap, cartesianToPolar(dir)), 6.0).rgb; 126 | } 127 | irradiance += sample * cos(theta) * sin(theta); 128 | 129 | samples++; 130 | } 131 | } 132 | irradiance = PI * irradiance / samples; 133 | 134 | if (uOutputType < 0.5) { 135 | 136 | // sRGB == 0 137 | gl_FragColor = linearToSRGB(vec4(irradiance, 1.0)); 138 | } else if (uOutputType < 1.5) { 139 | 140 | // RGBE == 1 141 | gl_FragColor = LinearToRGBE(irradiance); 142 | } else if (uOutputType < 2.5) { 143 | 144 | // RGBM == 2 145 | gl_FragColor = LinearToRGBM(irradiance); 146 | } else if (uOutputType < 3.5) { 147 | 148 | // RGBD == 3 149 | gl_FragColor = LinearToRGBD(irradiance, 6.0); 150 | } 151 | } 152 | `; 153 | 154 | export default {vertex, fragment}; -------------------------------------------------------------------------------- /src/shaders/SpecularShader.js: -------------------------------------------------------------------------------- 1 | const vertex = ` 2 | precision highp float; 3 | precision highp int; 4 | 5 | attribute vec3 position; 6 | attribute vec2 uv; 7 | 8 | varying vec2 vUv; 9 | 10 | void main() { 11 | vUv = uv; 12 | gl_Position = vec4(position, 1.0); 13 | } 14 | `; 15 | 16 | const fragment = ` 17 | precision highp float; 18 | precision highp int; 19 | 20 | uniform sampler2D tMap; 21 | uniform float uRoughness; 22 | uniform float uInputType; 23 | uniform float uOutputType; 24 | 25 | varying vec2 vUv; 26 | 27 | const int SAMPLES = 1000; 28 | 29 | const float PI = 3.14159265359; 30 | const float PI2 = 6.28318530718; 31 | const float RECIPROCAL_PI = 0.31830988618; 32 | const float RECIPROCAL_PI2 = 0.15915494; 33 | 34 | float VanDerCorpus(int n, int base) { 35 | float invBase = 1.0 / float(base); 36 | float denom = 1.0; 37 | float result = 0.0; 38 | 39 | for(int i = 0; i < 32; ++i) { 40 | if(n > 0) { 41 | denom = mod(float(n), 2.0); 42 | result += denom * invBase; 43 | invBase = invBase / 2.0; 44 | n = int(float(n) / 2.0); 45 | } 46 | } 47 | return result; 48 | } 49 | 50 | vec2 Hammersley(int i, int N) { 51 | return vec2(float(i) / float(N), VanDerCorpus(i, 2)); 52 | } 53 | 54 | vec4 SRGBtoLinear(vec4 srgb) { 55 | vec3 linOut = pow(srgb.xyz, vec3(2.2)); 56 | return vec4(linOut, srgb.w);; 57 | } 58 | 59 | vec4 linearToSRGB(vec4 color) { 60 | return vec4(pow(color.rgb, vec3(1.0 / 2.2)), color.a); 61 | } 62 | 63 | vec4 RGBEToLinear(in vec4 value) { 64 | return vec4(value.rgb * exp2(value.a * 255.0 - 128.0), 1.0); 65 | } 66 | 67 | vec4 LinearToRGBE(in vec3 value) { 68 | float maxComponent = max(max(value.r, value.g), value.b); 69 | float fExp = clamp(ceil(log2(maxComponent)), -128.0, 127.0); 70 | return vec4(value.rgb / exp2(fExp), (fExp + 128.0) / 255.0); 71 | } 72 | 73 | vec4 RGBMToLinear(in vec4 value) { 74 | float maxRange = 6.0; 75 | return vec4(value.xyz * value.w * maxRange, 1.0); 76 | } 77 | 78 | vec4 LinearToRGBM(in vec3 value) { 79 | float maxRange = 6.0; 80 | float maxRGB = max(value.r, max(value.g, value.b)); 81 | float M = clamp(maxRGB / maxRange, 0.0, 1.0); 82 | M = ceil(M * 255.0) / 255.0; 83 | return vec4(value.rgb / (M * maxRange), M); 84 | } 85 | 86 | vec4 RGBDToLinear(in vec4 value, in float maxRange) { 87 | return vec4(value.rgb * ((maxRange / 255.0) / value.a), 1.0); 88 | } 89 | 90 | vec4 LinearToRGBD(in vec3 value, in float maxRange) { 91 | float maxRGB = max(value.x, max(value.g, value.b)); 92 | float D = max(maxRange / maxRGB, 1.0); 93 | D = min(floor(D) / 255.0, 1.0); 94 | return vec4(value.rgb * (D * (255.0 / maxRange)), D); 95 | } 96 | 97 | vec3 importanceSampleGGX(vec2 uv, mat3 mNormal, float roughness) { 98 | float a = roughness * roughness; 99 | float phi = 2.0 * PI * uv.x; 100 | float cosTheta = sqrt((1.0 - uv.y) / (1.0 + (a * a - 1.0) * uv.y)); 101 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta); 102 | return mNormal * vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); 103 | } 104 | 105 | mat3 matrixFromVector(vec3 n) { 106 | float a = 1.0 / (1.0 + n.z); 107 | float b = -n.x * n.y * a; 108 | vec3 b1 = vec3(1.0 - n.x * n.x * a, b, -n.x); 109 | vec3 b2 = vec3(b, 1.0 - n.y * n.y * a, -n.y); 110 | return mat3(b1, b2, n); 111 | } 112 | 113 | vec2 cartesianToPolar(vec3 n) { 114 | vec2 uv; 115 | uv.x = atan(n.z, n.x) * RECIPROCAL_PI2 + 0.5; 116 | uv.y = asin(n.y) * RECIPROCAL_PI + 0.5; 117 | return uv; 118 | } 119 | 120 | vec3 polarToCartesian(vec2 uv) { 121 | float theta = (uv.x - 0.5) * PI2; 122 | float phi = (uv.y) * PI; 123 | 124 | vec3 n; 125 | n.x = sin(phi) * cos(theta); 126 | n.z = sin(phi) * sin(theta); 127 | n.y = -cos(phi); 128 | return normalize(n); 129 | } 130 | 131 | void main() { 132 | vec2 uv = vUv; 133 | vec3 normal = polarToCartesian(uv); 134 | mat3 mNormal = matrixFromVector(normal); 135 | vec3 color; 136 | vec3 dir; 137 | for(int i = 0; i < SAMPLES; i++) { 138 | vec2 r = Hammersley(i, SAMPLES); 139 | dir = importanceSampleGGX(vec2(float(i) / float(SAMPLES), r), mNormal, uRoughness); 140 | 141 | if (uInputType < 0.5) { 142 | 143 | // sRGB == 0 144 | color.rgb += SRGBtoLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 145 | } else if (uInputType < 1.5) { 146 | 147 | // RGBE == 1 148 | color.rgb += RGBEToLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 149 | } else if (uInputType < 2.5) { 150 | 151 | // RGBM == 2 152 | color.rgb += RGBMToLinear(texture2D(tMap, cartesianToPolar(dir))).rgb; 153 | } else if (uInputType < 3.5) { 154 | 155 | // RGBD == 3 156 | color.rgb += RGBDToLinear(texture2D(tMap, cartesianToPolar(dir)), 6.0).rgb; 157 | } 158 | } 159 | color /= float(SAMPLES); 160 | 161 | if (uOutputType < 0.5) { 162 | 163 | // sRGB == 0 164 | gl_FragColor = linearToSRGB(vec4(color, 1.0)); 165 | } else if (uOutputType < 1.5) { 166 | 167 | // RGBE == 1 168 | gl_FragColor = LinearToRGBE(color); 169 | } else if (uOutputType < 2.5) { 170 | 171 | // RGBM == 2 172 | gl_FragColor = LinearToRGBM(color); 173 | } else if (uOutputType < 3.5) { 174 | 175 | // RGBD == 3 176 | gl_FragColor = LinearToRGBD(color, 6.0); 177 | } 178 | } 179 | `; 180 | 181 | export default {vertex, fragment}; -------------------------------------------------------------------------------- /ogl/core/Geometry.js: -------------------------------------------------------------------------------- 1 | // attribute params 2 | // { 3 | // data - typed array eg UInt16Array for indices, Float32Array 4 | // size - int default 1 5 | // instanced - boolean default false. can pass true or divisor amount 6 | // type - gl enum default gl.UNSIGNED_SHORT for 'index', gl.FLOAT for others 7 | // normalize - boolean default false 8 | // } 9 | 10 | // TODO: fit in transform feedback 11 | // TODO: bounding box for auto culling 12 | // this.boundingBox.center 13 | 14 | // TODO: when would I disableVertexAttribArray ? 15 | // TODO: do I need to unbind after drawing ? 16 | // TODO: test updating attributes on the fly 17 | 18 | let ID = 0; 19 | 20 | export class Geometry { 21 | constructor(gl, attributes = {}) { 22 | this.gl = gl; 23 | this.attributes = attributes; 24 | this.id = ID++; 25 | 26 | this.drawRange = {start: 0, count: 0}; 27 | this.instancedCount = 0; 28 | 29 | // Unbind current VAO so that new buffers don't get added to active mesh 30 | this.gl.renderer.bindVertexArray(null); 31 | this.gl.renderer.currentGeometry = null; 32 | 33 | // create the buffers 34 | for (let key in attributes) { 35 | this.addAttribute(key, attributes[key]); 36 | } 37 | } 38 | 39 | addAttribute(key, attr) { 40 | this.attributes[key] = attr; 41 | 42 | // Set options 43 | attr.size = attr.size || 1; 44 | attr.type = attr.type || key === 'index' ? this.gl.UNSIGNED_SHORT : this.gl.FLOAT; 45 | attr.target = key === 'index' ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER; 46 | attr.normalize = attr.normalize || false; 47 | attr.buffer = this.gl.createBuffer(); 48 | attr.count = attr.data.length / attr.size; 49 | attr.divisor = !attr.instanced ? 0 : typeof attr.instanced === 'number' ? attr.instanced : 1; 50 | 51 | // Push data to buffer 52 | this.updateAttribute(attr); 53 | 54 | // Update geometry counts. If indexed, ignore regular attributes 55 | if (attr.divisor) { 56 | this.isInstanced = true; 57 | if (this.instancedCount && this.instancedCount !== attr.count * attr.divisor) { 58 | console.warn('geometry has multiple instanced buffers of different length'); 59 | return this.instancedCount = Math.min(this.instancedCount, attr.count * attr.divisor); 60 | } 61 | this.instancedCount = attr.count * attr.divisor; 62 | } else if (key === 'index') { 63 | this.drawRange.count = attr.count; 64 | } else if (!this.attributes.index) { 65 | this.drawRange.count = Math.max(this.drawRange.count, attr.count); 66 | } 67 | } 68 | 69 | updateAttribute(attr) { 70 | this.gl.bindBuffer(attr.target, attr.buffer); 71 | this.gl.bufferData(attr.target, attr.data, this.gl.STATIC_DRAW); 72 | this.gl.bindBuffer(attr.target, null); 73 | } 74 | 75 | setIndex(value) { 76 | this.addAttribute('index', value); 77 | } 78 | 79 | setDrawRange(start, count) { 80 | this.drawRange.start = start; 81 | this.drawRange.count = count; 82 | } 83 | 84 | setInstancedCount(value) { 85 | this.instancedCount = value; 86 | } 87 | 88 | createVAO(program) { 89 | this.vao = this.gl.renderer.createVertexArray(); 90 | this.gl.renderer.bindVertexArray(this.vao); 91 | this.bindAttributes(program); 92 | } 93 | 94 | bindAttributes(program) { 95 | 96 | // Link all attributes to program using gl.vertexAttribPointer 97 | program.attributeLocations.forEach((location, name) => { 98 | 99 | // If geometry missing a required shader attribute 100 | if (!this.attributes[name]) { 101 | console.warn(`active attribute ${name} not being supplied`); 102 | return; 103 | } 104 | 105 | const attr = this.attributes[name]; 106 | 107 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, attr.buffer); 108 | this.gl.vertexAttribPointer( 109 | location, 110 | attr.size, 111 | attr.type, 112 | attr.normalize, 113 | 0, // stride 114 | 0 // offset 115 | ); 116 | this.gl.enableVertexAttribArray(location); 117 | 118 | // For instanced attributes, divisor needs to be set. 119 | // For firefox, need to set back to 0 if non-instanced drawn after instanced. Else won't render 120 | this.gl.renderer.vertexAttribDivisor(location, attr.divisor); 121 | }); 122 | 123 | // Bind indices if geometry indexed 124 | if (this.attributes.index) this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.attributes.index.buffer); 125 | } 126 | 127 | draw({ 128 | program, 129 | mode = this.gl.TRIANGLES, 130 | geometryBound = false, 131 | }) { 132 | if (!geometryBound) { 133 | 134 | // Create VAO on first draw. Needs to wait for program to get attribute locations 135 | if (!this.vao) this.createVAO(program); 136 | 137 | // TODO: add fallback for non vao support (ie) 138 | 139 | // Bind if not already bound to program 140 | this.gl.renderer.bindVertexArray(this.vao); 141 | 142 | // Store so doesn't bind redundantly 143 | this.gl.renderer.currentGeometry = this.id; 144 | } 145 | 146 | if (this.isInstanced) { 147 | if (this.attributes.index) { 148 | this.gl.renderer.drawElementsInstanced(mode, this.drawRange.count, this.attributes.index.type, this.drawRange.start, this.instancedCount); 149 | } else { 150 | this.gl.renderer.drawArraysInstanced(mode, this.drawRange.start, this.drawRange.count, this.instancedCount); 151 | } 152 | } else { 153 | if (this.attributes.index) { 154 | this.gl.drawElements(mode, this.drawRange.count, this.attributes.index.type, this.drawRange.start); 155 | } else { 156 | this.gl.drawArrays(mode, this.drawRange.start, this.drawRange.count); 157 | } 158 | } 159 | } 160 | 161 | remove() { 162 | if (this.vao) this.gl.renderer.deleteVertexArray(this.vao); 163 | for (let key in this.attributes) { 164 | this.gl.deleteBuffer(this.attributes[key].buffer); 165 | delete this.attributes[key]; 166 | 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /src/inflate.module.min.js: -------------------------------------------------------------------------------- 1 | /** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */var mod={}, l=void 0,aa=mod;function r(c,d){var a=c.split("."),b=aa;!(a[0]in b)&&b.execScript&&b.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)!a.length&&d!==l?b[e]=d:b=b[e]?b[e]:b[e]={}};var t="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function v(c){var d=c.length,a=0,b=Number.POSITIVE_INFINITY,e,f,g,h,k,m,n,p,s,x;for(p=0;pa&&(a=c[p]),c[p]>=1;x=g<<16|p;for(s=m;s>>=1;switch(c){case 0:var d=this.input,a=this.a,b=this.c,e=this.b,f=d.length,g=l,h=l,k=b.length,m=l;this.d=this.f=0;if(a+1>=f)throw Error("invalid uncompressed block header: LEN");g=d[a++]|d[a++]<<8;if(a+1>=f)throw Error("invalid uncompressed block header: NLEN");h=d[a++]|d[a++]<<8;if(g===~h)throw Error("invalid uncompressed block header: length verify");if(a+g>d.length)throw Error("input buffer is broken");switch(this.i){case A:for(;e+ 4 | g>b.length;){m=k-e;g-=m;if(t)b.set(d.subarray(a,a+m),e),e+=m,a+=m;else for(;m--;)b[e++]=d[a++];this.b=e;b=this.e();e=this.b}break;case y:for(;e+g>b.length;)b=this.e({p:2});break;default:throw Error("invalid inflate mode");}if(t)b.set(d.subarray(a,a+g),e),e+=g,a+=g;else for(;g--;)b[e++]=d[a++];this.a=a;this.b=e;this.c=b;break;case 1:this.j(ba,ca);break;case 2:for(var n=C(this,5)+257,p=C(this,5)+1,s=C(this,4)+4,x=new (t?Uint8Array:Array)(D.length),S=l,T=l,U=l,u=l,M=l,F=l,z=l,q=l,V=l,q=0;q=P?8:255>=P?9:279>=P?7:8;var ba=v(O),Q=new (t?Uint8Array:Array)(30),R,ga;R=0;for(ga=Q.length;R=g)throw Error("input buffer is broken");a|=e[f++]<>>d;c.d=b-d;c.a=f;return h} 8 | function E(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h=d[0],k=d[1],m,n;b=g);)a|=e[f++]<>>16;if(n>b)throw Error("invalid code length: "+n);c.f=a>>n;c.d=b-n;c.a=f;return m&65535} 9 | w.prototype.j=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length-258,f,g,h,k;256!==(f=E(this,c));)if(256>f)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=I[g];0=e&&(this.b=b,a=this.e(),b=this.b);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; 10 | w.prototype.w=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length,f,g,h,k;256!==(f=E(this,c));)if(256>f)b>=e&&(a=this.e(),e=a.length),a[b++]=f;else{g=f-257;k=I[g];0e&&(a=this.e(),e=a.length);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; 11 | w.prototype.e=function(){var c=new (t?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,b,e=this.c;if(t)c.set(e.subarray(32768,c.length));else{a=0;for(b=c.length;aa;++a)e[a]=e[d+a];this.b=32768;return e}; 12 | w.prototype.z=function(c){var d,a=this.input.length/this.a+1|0,b,e,f,g=this.input,h=this.c;c&&("number"===typeof c.p&&(a=c.p),"number"===typeof c.u&&(a+=c.u));2>a?(b=(g.length-this.a)/this.o[2],f=258*(b/2)|0,e=fd&&(this.c.length=d),c=this.c);return this.buffer=c};function W(c,d){var a,b;this.input=c;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.A=d.verify);a=c[this.a++];b=c[this.a++];switch(a&15){case ha:this.method=ha;break;default:throw Error("unsupported compression method");}if(0!==((a<<8)+b)%31)throw Error("invalid fcheck flag:"+((a<<8)+b)%31);if(b&32)throw Error("fdict flag is not supported");this.q=new w(c,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})} 15 | W.prototype.k=function(){var c=this.input,d,a;d=this.q.k();this.a=this.q.a;if(this.A){a=(c[this.a++]<<24|c[this.a++]<<16|c[this.a++]<<8|c[this.a++])>>>0;var b=d;if("string"===typeof b){var e=b.split(""),f,g;f=0;for(g=e.length;f>>0;b=e}for(var h=1,k=0,m=b.length,n,p=0;0>>0)throw Error("invalid adler-32 checksum");}return d};var ha=8;r("Zlib.Inflate",W);r("Zlib.Inflate.prototype.decompress",W.prototype.k);var X={ADAPTIVE:B.s,BLOCK:B.t},Y,Z,$,ia;if(Object.keys)Y=Object.keys(X);else for(Z in Y=[],$=0,X)Y[$++]=Z;$=0;for(ia=Y.length;$> 24) & 255; 6 | buff[p + 1] = (n >> 16) & 255; 7 | buff[p + 2] = (n >> 8) & 255; 8 | buff[p + 3] = n & 255; 9 | } 10 | function writeASCII(data, p, s) { 11 | for (var i = 0; i < s.length; i++) data[p + i] = s.charCodeAt(i); 12 | } 13 | 14 | const crcTable = (function () { 15 | var tab = new Uint32Array(256); 16 | for (var n = 0; n < 256; n++) { 17 | var c = n; 18 | for (var k = 0; k < 8; k++) { 19 | if (c & 1) c = 0xedb88320 ^ (c >>> 1); 20 | else c = c >>> 1; 21 | } 22 | tab[n] = c; 23 | } 24 | return tab; 25 | })(); 26 | function crcUpdate(c, buf, off, len) { 27 | for (var i = 0; i < len; i++) c = crcTable[(c ^ buf[off + i]) & 0xff] ^ (c >>> 8); 28 | return c; 29 | } 30 | function crc(b, o, l) { 31 | return crcUpdate(0xffffffff, b, o, l) ^ 0xffffffff; 32 | } 33 | 34 | export function encodePNG(bufs, w, h, cc, ac, depth, dels, tabs) { 35 | var nimg = { ctype: 0 + (cc == 1 ? 0 : 2) + (ac == 0 ? 0 : 4), depth: depth, frames: [] }; 36 | 37 | var bipp = (cc + ac) * depth, 38 | bipl = bipp * w; 39 | for (var i = 0; i < bufs.length; i++) 40 | nimg.frames.push({ 41 | rect: { x: 0, y: 0, width: w, height: h }, 42 | img: new Uint8Array(bufs[i]), 43 | blend: 0, 44 | dispose: 1, 45 | bpp: Math.ceil(bipp / 8), 46 | bpl: Math.ceil(bipl / 8), 47 | }); 48 | 49 | compressPNG(nimg); 50 | 51 | var out = encodeMain(nimg, w, h, dels, tabs); 52 | return out; 53 | } 54 | 55 | function compressPNG(out) { 56 | for (var i = 0; i < out.frames.length; i++) { 57 | var frm = out.frames[i], 58 | nh = frm.rect.height; 59 | var fdata = new Uint8Array(nh * frm.bpl + nh); 60 | frm.cimg = filterZero(frm.img, nh, frm.bpp, frm.bpl, fdata); 61 | } 62 | } 63 | 64 | function filterZero(img, h, bpp, bpl, data) { 65 | var fls = []; 66 | var opts = { level: 0 }; 67 | var CMPR = pako; 68 | 69 | for (var y = 0; y < h; y++) filterLine(data, img, y, bpl, bpp, 0); 70 | fls.push(CMPR['deflate'](data, opts)); 71 | 72 | var ti, 73 | tsize = 1e9; 74 | for (var i = 0; i < fls.length; i++) 75 | if (fls[i].length < tsize) { 76 | ti = i; 77 | tsize = fls[i].length; 78 | } 79 | return fls[ti]; 80 | } 81 | function filterLine(data, img, y, bpl, bpp, type) { 82 | var i = y * bpl, 83 | di = i + y; 84 | data[di] = type; 85 | di++; 86 | 87 | if (type == 0) { 88 | if (bpl < 500) for (var x = 0; x < bpl; x++) data[di + x] = img[i + x]; 89 | else data.set(new Uint8Array(img.buffer, i, bpl), di); 90 | } 91 | } 92 | 93 | function encodeMain(nimg, w, h, dels, tabs) { 94 | if (tabs == null) tabs = {}; 95 | var wUi = writeUint, 96 | wAs = writeASCII; 97 | var offset = 8, 98 | pltAlpha = false; 99 | 100 | var leng = 8 + (16 + 5 + 4); /*+ (9+4)*/ 101 | if (tabs['sRGB'] != null) leng += 8 + 1 + 4; 102 | if (tabs['pHYs'] != null) leng += 8 + 9 + 4; 103 | if (nimg.ctype == 3) { 104 | var dl = nimg.plte.length; 105 | for (var i = 0; i < dl; i++) if (nimg.plte[i] >>> 24 != 255) pltAlpha = true; 106 | leng += 8 + dl * 3 + 4 + (pltAlpha ? 8 + dl * 1 + 4 : 0); 107 | } 108 | for (var j = 0; j < nimg.frames.length; j++) { 109 | var fr = nimg.frames[j]; 110 | leng += fr.cimg.length + 12; 111 | if (j != 0) leng += 4; 112 | } 113 | leng += 12; 114 | 115 | var data = new Uint8Array(leng); 116 | var wr = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 117 | for (var i = 0; i < 8; i++) data[i] = wr[i]; 118 | 119 | wUi(data, offset, 13); 120 | offset += 4; 121 | wAs(data, offset, 'IHDR'); 122 | offset += 4; 123 | wUi(data, offset, w); 124 | offset += 4; 125 | wUi(data, offset, h); 126 | offset += 4; 127 | data[offset] = nimg.depth; 128 | offset++; // depth 129 | data[offset] = nimg.ctype; 130 | offset++; // ctype 131 | data[offset] = 0; 132 | offset++; // compress 133 | data[offset] = 0; 134 | offset++; // filter 135 | data[offset] = 0; 136 | offset++; // interlace 137 | wUi(data, offset, crc(data, offset - 17, 17)); 138 | offset += 4; // crc 139 | 140 | // 13 bytes to say, that it is sRGB 141 | if (tabs['sRGB'] != null) { 142 | wUi(data, offset, 1); 143 | offset += 4; 144 | wAs(data, offset, 'sRGB'); 145 | offset += 4; 146 | data[offset] = tabs['sRGB']; 147 | offset++; 148 | wUi(data, offset, crc(data, offset - 5, 5)); 149 | offset += 4; // crc 150 | } 151 | if (tabs['pHYs'] != null) { 152 | wUi(data, offset, 9); 153 | offset += 4; 154 | wAs(data, offset, 'pHYs'); 155 | offset += 4; 156 | wUi(data, offset, tabs['pHYs'][0]); 157 | offset += 4; 158 | wUi(data, offset, tabs['pHYs'][1]); 159 | offset += 4; 160 | data[offset] = tabs['pHYs'][2]; 161 | offset++; 162 | wUi(data, offset, crc(data, offset - 13, 13)); 163 | offset += 4; // crc 164 | } 165 | 166 | if (nimg.ctype == 3) { 167 | var dl = nimg.plte.length; 168 | wUi(data, offset, dl * 3); 169 | offset += 4; 170 | wAs(data, offset, 'PLTE'); 171 | offset += 4; 172 | for (var i = 0; i < dl; i++) { 173 | var ti = i * 3, 174 | c = nimg.plte[i], 175 | r = c & 255, 176 | g = (c >>> 8) & 255, 177 | b = (c >>> 16) & 255; 178 | data[offset + ti + 0] = r; 179 | data[offset + ti + 1] = g; 180 | data[offset + ti + 2] = b; 181 | } 182 | offset += dl * 3; 183 | wUi(data, offset, crc(data, offset - dl * 3 - 4, dl * 3 + 4)); 184 | offset += 4; // crc 185 | 186 | if (pltAlpha) { 187 | wUi(data, offset, dl); 188 | offset += 4; 189 | wAs(data, offset, 'tRNS'); 190 | offset += 4; 191 | for (var i = 0; i < dl; i++) data[offset + i] = (nimg.plte[i] >>> 24) & 255; 192 | offset += dl; 193 | wUi(data, offset, crc(data, offset - dl - 4, dl + 4)); 194 | offset += 4; // crc 195 | } 196 | } 197 | 198 | var fi = 0; 199 | for (var j = 0; j < nimg.frames.length; j++) { 200 | var fr = nimg.frames[j]; 201 | 202 | var imgd = fr.cimg, 203 | dl = imgd.length; 204 | wUi(data, offset, dl + (j == 0 ? 0 : 4)); 205 | offset += 4; 206 | var ioff = offset; 207 | wAs(data, offset, j == 0 ? 'IDAT' : 'fdAT'); 208 | offset += 4; 209 | if (j != 0) { 210 | wUi(data, offset, fi++); 211 | offset += 4; 212 | } 213 | data.set(imgd, offset); 214 | offset += dl; 215 | wUi(data, offset, crc(data, ioff, offset - ioff)); 216 | offset += 4; // crc 217 | } 218 | 219 | wUi(data, offset, 0); 220 | offset += 4; 221 | wAs(data, offset, 'IEND'); 222 | offset += 4; 223 | wUi(data, offset, crc(data, offset - 4, 4)); 224 | offset += 4; // crc 225 | 226 | return data.buffer; 227 | } 228 | -------------------------------------------------------------------------------- /ogl/core/Texture.js: -------------------------------------------------------------------------------- 1 | // TODO: Compressed Texture 2 | // TODO: data texture 3 | // TODO: cube map 4 | 5 | const emptyPixel = new Uint8Array(4); 6 | 7 | function isPowerOf2(value) { 8 | return (value & (value - 1)) === 0; 9 | } 10 | 11 | export class Texture { 12 | constructor(gl, { 13 | image, 14 | target = gl.TEXTURE_2D, 15 | type = gl.UNSIGNED_BYTE, 16 | format = gl.RGBA, 17 | internalFormat = format, 18 | wrapS = gl.CLAMP_TO_EDGE, 19 | wrapT = gl.CLAMP_TO_EDGE, 20 | generateMipmaps = true, 21 | minFilter = generateMipmaps ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR, 22 | magFilter = gl.LINEAR, 23 | premultiplyAlpha = false, 24 | flipY = true, 25 | level = 0, 26 | width, // used for RenderTargets or Data Textures 27 | height = width, 28 | 29 | // TODO: need? encoding = linearEncoding 30 | } = {}) { 31 | this.gl = gl; 32 | 33 | this.image = image; 34 | this.target = target; 35 | this.type = type; 36 | this.format = format; 37 | this.internalFormat = internalFormat; 38 | this.minFilter = minFilter; 39 | this.magFilter = magFilter; 40 | this.wrapS = wrapS; 41 | this.wrapT = wrapT; 42 | this.generateMipmaps = generateMipmaps; 43 | this.premultiplyAlpha = premultiplyAlpha; 44 | this.flipY = flipY; 45 | this.level = level; 46 | this.width = width; 47 | this.height = height; 48 | this.texture = this.gl.createTexture(); 49 | 50 | // Store which values have been set on the gpu. Fill with webgl defaults 51 | this.store = { 52 | image: null, 53 | minFilter: this.gl.NEAREST_MIPMAP_LINEAR, 54 | magFilter: this.gl.LINEAR, 55 | wrapS: this.gl.REPEAT, 56 | wrapT: this.gl.REPEAT, 57 | premultiplyAlpha: false, 58 | flipY: false, 59 | }; 60 | } 61 | 62 | update() { 63 | 64 | // Bind so we can set its params 65 | this.gl.bindTexture(this.target, this.texture); 66 | 67 | if (this.flipY !== this.store.flipY) { 68 | this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, this.flipY); 69 | this.store.flipY = this.flipY; 70 | } 71 | 72 | if (this.premultiplyAlpha !== this.store.premultiplyAlpha) { 73 | this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha); 74 | this.store.premultiplyAlpha = this.premultiplyAlpha; 75 | } 76 | 77 | if (this.minFilter !== this.store.minFilter) { 78 | this.gl.texParameteri(this.target, this.gl.TEXTURE_MIN_FILTER, this.minFilter); 79 | this.store.minFilter = this.minFilter; 80 | } 81 | 82 | if (this.magFilter !== this.store.magFilter) { 83 | this.gl.texParameteri(this.target, this.gl.TEXTURE_MAG_FILTER, this.magFilter); 84 | this.store.magFilter = this.magFilter; 85 | } 86 | 87 | if (this.wrapS !== this.store.wrapS) { 88 | this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); 89 | this.store.wrapS = this.wrapS; 90 | } 91 | 92 | if (this.wrapT !== this.store.wrapT) { 93 | this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); 94 | this.store.wrapT = this.wrapT; 95 | } 96 | 97 | if (this.image !== this.store.image || this.needsUpdate) { 98 | this.needsUpdate = false; 99 | if (this.image) { 100 | 101 | if (this.image.width) { 102 | this.width = this.image.width; 103 | this.height = this.image.height; 104 | } 105 | 106 | // TODO: all sides if cubemap 107 | // gl.TEXTURE_CUBE_MAP_POSITIVE_X 108 | 109 | // TODO: check is ArrayBuffer.isView is best way to check for Typed Arrays? 110 | if (this.gl.renderer.isWebgl2 || ArrayBuffer.isView(this.image)) { 111 | this.gl.texImage2D(this.target, this.level, this.internalFormat, this.width, this.height, 0 /* border */, this.format, this.type, this.image); 112 | } else { 113 | this.gl.texImage2D(this.target, this.level, this.internalFormat, this.format, this.type, this.image); 114 | } 115 | 116 | // TODO: support everything 117 | // WebGL1: 118 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView? pixels); 119 | // gl.texImage2D(target, level, internalformat, format, type, ImageData? pixels); 120 | // gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels); 121 | // gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels); 122 | // gl.texImage2D(target, level, internalformat, format, type, HTMLVideoElement? pixels); 123 | // gl.texImage2D(target, level, internalformat, format, type, ImageBitmap? pixels); 124 | 125 | // WebGL2: 126 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, GLintptr offset); 127 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLCanvasElement source); 128 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLImageElement source); 129 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLVideoElement source); 130 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageBitmap source); 131 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageData source); 132 | // gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView srcData, srcOffset); 133 | 134 | // TODO: mipmap data textures? 135 | if (this.generateMipmaps) { 136 | 137 | // For WebGL1, if not a power of 2, turn off mips, set wrapping to clamp to edge and minFilter to linear 138 | if (!this.gl.renderer.isWebgl2 && (!isPowerOf2(this.image.width) || !isPowerOf2(this.image.height))) { 139 | this.generateMipmaps = false; 140 | this.wrapS = this.wrapT = this.gl.CLAMP_TO_EDGE; 141 | this.minFilter = this.gl.LINEAR; 142 | } else { 143 | this.gl.generateMipmap(this.target); 144 | } 145 | } 146 | } else { 147 | 148 | if (this.width) { 149 | 150 | // image intentionally left null for RenderTarget 151 | this.gl.texImage2D(this.target, this.level, this.internalFormat, this.width, this.height, 0, this.format, this.type, null); 152 | } else { 153 | 154 | // Upload empty pixel if no image to avoid errors while image or video loading 155 | this.gl.texImage2D(this.target, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, emptyPixel); 156 | } 157 | 158 | } 159 | this.store.image = this.image; 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/PNGWalker.js: -------------------------------------------------------------------------------- 1 | // Functions below sourced from the png-pong lib to help with 2 | // the adlers and CNCs and whatnot 3 | // https://github.com/gdnmobilelab/png-pong 4 | 5 | let TABLE = new Int32Array([ 6 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 7 | 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 8 | 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 9 | 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 10 | 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 11 | 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 12 | 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 13 | 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 14 | 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 15 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 16 | 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 17 | 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 18 | 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 19 | 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 20 | 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 21 | 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 22 | 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 23 | 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 24 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 25 | 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 26 | 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 27 | 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 28 | 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 29 | 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 30 | 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 31 | 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 32 | 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 33 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 34 | 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 35 | 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 36 | 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 37 | 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 38 | 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 39 | 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 40 | 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 41 | 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 42 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 43 | 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 44 | 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 45 | 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 46 | 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 47 | 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 48 | 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 49 | 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 50 | 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 51 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 52 | 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 53 | 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 54 | 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 55 | 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 56 | 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 57 | 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 58 | 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 59 | 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 60 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 61 | 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 62 | 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 63 | 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 64 | 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 65 | 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 66 | 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 67 | 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 68 | 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 69 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d 70 | ]); 71 | 72 | function crc32(buf, offset, length, previous) { 73 | let crc = previous === 0 ? 0 : ~~previous ^ -1; 74 | for (let index = offset; index < offset + length; index++) { 75 | const byte = buf[index]; 76 | crc = TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); 77 | } 78 | return crc ^ -1; 79 | } 80 | 81 | function adler32_buf(buf, offset, length, seed) { 82 | var a = 1, b = 0, L = offset + length, M = 0; 83 | if (typeof seed === 'number') { 84 | a = seed & 0xFFFF; 85 | b = (seed >>> 16) & 0xFFFF; 86 | } 87 | for (var i = offset; i < L;) { 88 | M = Math.min(L - i, 3850) + i; 89 | for (; i < M; i++) { 90 | a += buf[i] & 0xFF; 91 | b += a; 92 | } 93 | a = (15 * (a >>> 16) + (a & 65535)); 94 | b = (15 * (b >>> 16) + (b & 65535)); 95 | } 96 | return ((b % 65521) << 16) | (a % 65521); 97 | } 98 | 99 | function swap16(val) { 100 | return ((val & 0xFF) << 8) 101 | | ((val >> 8) & 0xFF); 102 | } 103 | 104 | function swap32(val) { 105 | return ((val & 0xFF) << 24) 106 | | ((val & 0xFF00) << 8) 107 | | ((val >> 8) & 0xFF00) 108 | | ((val >> 24) & 0xFF); 109 | } 110 | 111 | export class ArrayBufferWalker { 112 | constructor(bufferOrLength) { 113 | this.bufferOrLength = bufferOrLength; 114 | this.offset = 0; 115 | if (bufferOrLength instanceof ArrayBuffer) { 116 | this.array = new Uint8Array(bufferOrLength); 117 | } 118 | else { 119 | this.array = new Uint8Array(bufferOrLength); 120 | } 121 | } 122 | 123 | writeUint32(value, littleEndian = false) { 124 | if (littleEndian) { 125 | value = swap32(value); 126 | } 127 | this.array[this.offset++] = (value >> 24) & 255; 128 | this.array[this.offset++] = (value >> 16) & 255; 129 | this.array[this.offset++] = (value >> 8) & 255; 130 | this.array[this.offset++] = value & 255; 131 | } 132 | 133 | writeUint16(value, littleEndian = false) { 134 | if (littleEndian) { 135 | value = swap16(value); 136 | } 137 | this.array[this.offset++] = (value >> 8) & 255; 138 | this.array[this.offset++] = value & 255; 139 | } 140 | 141 | writeUint8(value) { 142 | this.array[this.offset++] = value & 255; 143 | } 144 | 145 | writeString(value) { 146 | for (let i = 0, n = value.length; i < n; i++) { 147 | this.array[this.offset++] = value.charCodeAt(i); 148 | } 149 | } 150 | 151 | startCRC() { 152 | if (this.crcStartOffset) { 153 | throw new Error("CRC already started"); 154 | } 155 | this.crcStartOffset = this.offset; 156 | } 157 | 158 | writeCRC() { 159 | if (this.crcStartOffset === undefined) { 160 | throw new Error("CRC has not been started, cannot write"); 161 | } 162 | let crc = crc32(this.array, this.crcStartOffset, this.offset - this.crcStartOffset); 163 | this.crcStartOffset = undefined; 164 | this.writeUint32(crc); 165 | } 166 | 167 | startAdler() { 168 | if (this.adlerStartOffset) { 169 | throw new Error("Adler already started"); 170 | } 171 | this.adlerStartOffset = this.offset; 172 | } 173 | 174 | pauseAdler() { 175 | if (this.adlerStartOffset === undefined) { 176 | throw new Error("Adler has not been started, cannot pause"); 177 | } 178 | this.savedAdlerValue = adler32_buf(this.array, this.adlerStartOffset, this.offset - this.adlerStartOffset, this.savedAdlerValue); 179 | this.adlerStartOffset = undefined; 180 | } 181 | 182 | writeAdler() { 183 | if (this.adlerStartOffset === undefined && this.savedAdlerValue === undefined) { 184 | throw new Error("CRC has not been started, cannot write"); 185 | } 186 | if (this.adlerStartOffset === undefined) { 187 | this.writeUint32(this.savedAdlerValue); 188 | this.savedAdlerValue = undefined; 189 | return; 190 | } 191 | let adler = adler32_buf(this.array, this.adlerStartOffset, this.offset - this.adlerStartOffset, this.savedAdlerValue); 192 | this.adlerStartOffset = undefined; 193 | this.writeUint32(adler); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | IBL Map Converter For PBR 13 | 14 | 15 | 16 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/HDRLoader.js: -------------------------------------------------------------------------------- 1 | // Load 32bit .hdr (RGBE) format 2 | 3 | const RGBE_RETURN_FAILURE = -1; 4 | 5 | // default error routine. Change this to change error handling 6 | const READ_ERROR = 1; 7 | const WRITE_ERROR = 2; 8 | const FORMAT_ERROR = 3; 9 | const MEMORY_ERROR = 4; 10 | 11 | // flags indicating which fields in an rgbe_header_info are valid 12 | const RGBE_VALID_PROGRAMTYPE = 1; 13 | const RGBE_VALID_FORMAT = 2; 14 | const RGBE_VALID_DIMENSIONS = 4; 15 | const NEWLINE = '\n'; 16 | 17 | export function parseHDR(buffer) { 18 | // If src passed in instead 19 | // const buffer = await (await fetch(src)).arrayBuffer(); 20 | 21 | const byteArray = new Uint8Array(buffer); 22 | byteArray.pos = 0; 23 | 24 | const rgbe_header_info = readHeader(byteArray); 25 | if (rgbe_header_info === RGBE_RETURN_FAILURE) return; 26 | 27 | let width = rgbe_header_info.width; 28 | let height = rgbe_header_info.height; 29 | let image_rgba_data = readPixels_RLE(byteArray.subarray(byteArray.pos), width, height); 30 | if (image_rgba_data === RGBE_RETURN_FAILURE) return; 31 | 32 | return { 33 | width, 34 | height, 35 | data: image_rgba_data, 36 | header: rgbe_header_info, 37 | gamma: rgbe_header_info.gamma, 38 | exposure: rgbe_header_info.exposure, 39 | format: 'RGBE', 40 | type: 'UnsignedByteType', 41 | }; 42 | 43 | function logError(logError_code, msg) { 44 | switch (logError_code) { 45 | case READ_ERROR: 46 | console.error('Read Error: ' + (msg || '')); 47 | break; 48 | case WRITE_ERROR: 49 | console.error('Write Error: ' + (msg || '')); 50 | break; 51 | case FORMAT_ERROR: 52 | console.error('Bad File Format: ' + (msg || '')); 53 | break; 54 | default: 55 | case MEMORY_ERROR: 56 | console.error('Error: ' + (msg || '')); 57 | } 58 | return RGBE_RETURN_FAILURE; 59 | } 60 | 61 | function fgets(buffer, lineLimit = 1024, consume) { 62 | let p = buffer.pos; 63 | let i = -1; 64 | let len = 0; 65 | let s = ''; 66 | let chunkSize = 128; 67 | let chunk = String.fromCharCode.apply(null, new Uint16Array(buffer.subarray(p, p + chunkSize))); 68 | 69 | while ((i = chunk.indexOf(NEWLINE)) < 0 && len < lineLimit && p < buffer.byteLength) { 70 | s += chunk; 71 | len += chunk.length; 72 | p += chunkSize; 73 | chunk += String.fromCharCode.apply(null, new Uint16Array(buffer.subarray(p, p + chunkSize))); 74 | } 75 | if (i > -1) { 76 | buffer.pos += len + i + 1; 77 | return s + chunk.slice(0, i); 78 | } 79 | return false; 80 | } 81 | 82 | function readHeader(buffer) { 83 | // regexes to parse header info fields 84 | const magic_token_re = /^#\?(\S+)$/; 85 | const gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/; 86 | const exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/; 87 | const format_re = /^\s*FORMAT=(\S+)\s*$/; 88 | const dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/; 89 | 90 | // RGBE format header struct 91 | const header = { 92 | valid: 0 /* indicate which fields are valid */, 93 | string: '' /* the actual header string */, 94 | comments: '' /* comments found in header */, 95 | programtype: 96 | 'RGBE' /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */, 97 | format: '' /* RGBE format, default 32-bit_rle_rgbe */, 98 | gamma: 1.0 /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */, 99 | exposure: 1.0 /* a value of 1.0 in an image corresponds to watts/steradian/m^2. defaults to 1.0 */, 100 | width: 0, 101 | height: 0 /* image dimensions, width/height */, 102 | }; 103 | 104 | let line = fgets(buffer); 105 | if (buffer.pos >= buffer.byteLength || !line) return logError(READ_ERROR, 'no header found'); 106 | 107 | let match = line.match(magic_token_re); 108 | if (!match) return logError(FORMAT_ERROR, 'bad initial token'); 109 | 110 | header.valid |= RGBE_VALID_PROGRAMTYPE; 111 | header.programtype = match[1]; 112 | header.string += line + '\n'; 113 | 114 | while (true) { 115 | line = fgets(buffer); 116 | if (line === false) break; 117 | header.string += line + '\n'; 118 | if (line.charAt(0) === '#') { 119 | header.comments += line + '\n'; 120 | continue; // comment line 121 | } 122 | if ((match = line.match(gamma_re))) { 123 | header.gamma = parseFloat(match[1], 10); 124 | } 125 | if ((match = line.match(exposure_re))) { 126 | header.exposure = parseFloat(match[1], 10); 127 | } 128 | if ((match = line.match(format_re))) { 129 | header.valid |= RGBE_VALID_FORMAT; 130 | header.format = match[1]; //'32-bit_rle_rgbe'; 131 | } 132 | if ((match = line.match(dimensions_re))) { 133 | header.valid |= RGBE_VALID_DIMENSIONS; 134 | header.height = parseInt(match[1], 10); 135 | header.width = parseInt(match[2], 10); 136 | } 137 | if (header.valid & RGBE_VALID_FORMAT && header.valid & RGBE_VALID_DIMENSIONS) break; 138 | } 139 | if (!(header.valid & RGBE_VALID_FORMAT)) { 140 | return logError(FORMAT_ERROR, 'missing format specifier'); 141 | } 142 | if (!(header.valid & RGBE_VALID_DIMENSIONS)) { 143 | return logError(FORMAT_ERROR, 'missing image size specifier'); 144 | } 145 | return header; 146 | } 147 | 148 | function readPixels_RLE(buffer, w, h) { 149 | const scanline_width = w; 150 | let num_scanlines = h; 151 | 152 | // run length encoding is not allowed so read flat 153 | // this file is not run length encoded 154 | if ( 155 | scanline_width < 8 || 156 | scanline_width > 0x7fff || 157 | 2 !== buffer[0] || 158 | 2 !== buffer[1] || 159 | buffer[2] & 0x80 160 | ) { 161 | // return the flat buffer 162 | return new Uint8Array(buffer); 163 | } 164 | if (scanline_width !== ((buffer[2] << 8) | buffer[3])) { 165 | return logError(FORMAT_ERROR, 'wrong scanline width'); 166 | } 167 | 168 | // Create output buffer 169 | const data_rgba = new Uint8Array(4 * w * h); 170 | if (!data_rgba || !data_rgba.length) return logError(MEMORY_ERROR, 'unable to allocate buffer space'); 171 | 172 | let offset = 0; 173 | let pos = 0; 174 | const ptr_end = 4 * scanline_width; 175 | const rgbeStart = new Uint8Array(4); 176 | const scanline_buffer = new Uint8Array(ptr_end); 177 | 178 | // read in each successive scanline 179 | while (num_scanlines > 0 && pos < buffer.byteLength) { 180 | if (pos + 4 > buffer.byteLength) { 181 | return logError(READ_ERROR); 182 | } 183 | rgbeStart[0] = buffer[pos++]; 184 | rgbeStart[1] = buffer[pos++]; 185 | rgbeStart[2] = buffer[pos++]; 186 | rgbeStart[3] = buffer[pos++]; 187 | if ( 188 | 2 != rgbeStart[0] || 189 | 2 != rgbeStart[1] || 190 | ((rgbeStart[2] << 8) | rgbeStart[3]) != scanline_width 191 | ) { 192 | return logError(FORMAT_ERROR, 'bad rgbe scanline format'); 193 | } 194 | 195 | // read each of the four channels for the scanline into the buffer 196 | // first red, then green, then blue, then exponent 197 | let ptr = 0; 198 | while (ptr < ptr_end && pos < buffer.byteLength) { 199 | let count = buffer[pos++]; 200 | const isEncodedRun = count > 128; 201 | if (isEncodedRun) count -= 128; 202 | if (0 === count || ptr + count > ptr_end) { 203 | return logError(FORMAT_ERROR, 'bad scanline data'); 204 | } 205 | if (isEncodedRun) { 206 | // a (encoded) run of the same value 207 | let byteValue = buffer[pos++]; 208 | for (let i = 0; i < count; i++) { 209 | scanline_buffer[ptr++] = byteValue; 210 | } 211 | } else { 212 | // a literal-run 213 | scanline_buffer.set(buffer.subarray(pos, pos + count), ptr); 214 | ptr += count; 215 | pos += count; 216 | } 217 | } 218 | 219 | // now convert data from buffer into rgba 220 | // first red, then green, then blue, then exponent (alpha) 221 | for (let i = 0; i < scanline_width; i++) { 222 | let off = 0; 223 | data_rgba[offset] = scanline_buffer[i + off]; 224 | off += scanline_width; //1; 225 | data_rgba[offset + 1] = scanline_buffer[i + off]; 226 | off += scanline_width; //1; 227 | data_rgba[offset + 2] = scanline_buffer[i + off]; 228 | off += scanline_width; //1; 229 | data_rgba[offset + 3] = scanline_buffer[i + off]; 230 | offset += 4; 231 | } 232 | num_scanlines--; 233 | } 234 | return data_rgba; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /ogl/core/Program.js: -------------------------------------------------------------------------------- 1 | // TODO: upload empty texture if null 2 | // TODO: upload identity matrix if null 3 | // TODO: texture units/locations 4 | // TODO: sampler Cube 5 | 6 | let ID = 0; 7 | 8 | // cache of typed arrays used to flatten uniform arrays 9 | const arrayCacheF32 = {}; 10 | 11 | function setUniform(gl, type, location, value) { 12 | switch (type) { 13 | case 5126 : return value.length ? gl.uniform1fv(location, value) : gl.uniform1f(location, value); // FLOAT 14 | case 35664 : return gl.uniform2fv(location, value[0].length ? flatten(value) : value); // FLOAT_VEC2 15 | case 35665 : return gl.uniform3fv(location, value[0].length ? flatten(value) : value); // FLOAT_VEC3 16 | case 35666 : return gl.uniform4fv(location, value[0].length ? flatten(value) : value); // FLOAT_VEC4 17 | case 35670 : // BOOL 18 | case 5124 : // INT 19 | case 35678 : // SAMPLER_2D 20 | case 35680 : return value.length ? gl.uniform1iv(location, value) : gl.uniform1i(location, value); // SAMPLER_CUBE 21 | case 35671 : // BOOL_VEC2 22 | case 35667 : return gl.uniform2iv(location, value); // INT_VEC2 23 | case 35672 : // BOOL_VEC3 24 | case 35668 : return gl.uniform3iv(location, value); // INT_VEC3 25 | case 35673 : // BOOL_VEC4 26 | case 35669 : return gl.uniform4iv(location, value); // INT_VEC4 27 | case 35674 : return gl.uniformMatrix2fv(location, false, value[0].length ? flatten(value) : value); // FLOAT_MAT2 28 | case 35675 : return gl.uniformMatrix3fv(location, false, value[0].length ? flatten(value) : value); // FLOAT_MAT3 29 | case 35676 : return gl.uniformMatrix4fv(location, false, value[0].length ? flatten(value) : value); // FLOAT_MAT4 30 | } 31 | } 32 | 33 | function addLineNumbers(string) { 34 | let lines = string.split('\n'); 35 | for (let i = 0; i < lines.length; i ++) { 36 | lines[i] = (i + 1) + ': ' + lines[i]; 37 | } 38 | return lines.join('\n'); 39 | } 40 | 41 | function flatten(array) { 42 | const arrayLen = array.length; 43 | const valueLen = array[0].length; 44 | const length = arrayLen * valueLen; 45 | let value = arrayCacheF32[length]; 46 | if (!value) arrayCacheF32[length] = value = new Float32Array(length); 47 | for (let i = 0; i < arrayLen; i++) value.set(array[i], i * valueLen); 48 | return value; 49 | } 50 | 51 | export class Program { 52 | constructor(gl, { 53 | vertexShader, 54 | fragmentShader, 55 | uniforms = {}, 56 | 57 | transparent = false, 58 | cullFace = gl.BACK, 59 | frontFace = gl.CCW, 60 | depthTest = true, 61 | depthWrite = true, 62 | depthFunc = gl.LESS, 63 | } = {}) { 64 | this.gl = gl; 65 | this.uniforms = uniforms; 66 | this.id = ID++; 67 | 68 | if (!vertexShader) console.warn('vertex shader not supplied'); 69 | if (!fragmentShader) console.warn('fragment shader not supplied'); 70 | 71 | // Store program state 72 | this.transparent = transparent; 73 | this.cullFace = cullFace; 74 | this.frontFace = frontFace; 75 | this.depthTest = depthTest; 76 | this.depthWrite = depthWrite; 77 | this.depthFunc = depthFunc; 78 | this.blendFunc = {}; 79 | this.blendEquation = {}; 80 | 81 | // set default blendFunc if transparent flagged 82 | if (this.transparent && !this.blendFunc.src) { 83 | if (this.gl.renderer.premultipliedAlpha) this.setBlendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA); 84 | else this.setBlendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); 85 | } 86 | 87 | // compile vertex shader and log errors 88 | this.vertexShader = gl.createShader(gl.VERTEX_SHADER); 89 | gl.shaderSource(this.vertexShader, vertexShader); 90 | gl.compileShader(this.vertexShader); 91 | if (gl.getShaderInfoLog(this.vertexShader) !== '') { 92 | console.warn(`${gl.getShaderInfoLog(this.vertexShader)}\nVertex Shader\n${addLineNumbers(vertexShader)}`); 93 | } 94 | 95 | // compile fragment shader and log errors 96 | this.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 97 | gl.shaderSource(this.fragmentShader, fragmentShader); 98 | gl.compileShader(this.fragmentShader); 99 | if (gl.getShaderInfoLog(this.fragmentShader) !== '') { 100 | console.warn(`${gl.getShaderInfoLog(this.fragmentShader)}\nFragment Shader\n${addLineNumbers(fragmentShader)}`); 101 | } 102 | 103 | // compile program and log errors 104 | this.program = gl.createProgram(); 105 | gl.attachShader(this.program, this.vertexShader); 106 | gl.attachShader(this.program, this.fragmentShader); 107 | gl.linkProgram(this.program); 108 | if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 109 | return console.warn(gl.getProgramInfoLog(this.program)); 110 | } 111 | 112 | // Get active uniform locations 113 | this.uniformLocations = new Map(); 114 | let numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); 115 | for (let uIndex = 0; uIndex < numUniforms; uIndex++) { 116 | let uniform = gl.getActiveUniform(this.program, uIndex); 117 | this.uniformLocations.set(uniform, gl.getUniformLocation(this.program, uniform.name)); 118 | 119 | // trim uniforms' names to omit array declarations 120 | uniform.uniformName = uniform.name.split('[')[0]; 121 | } 122 | 123 | // Get active attribute locations 124 | this.attributeLocations = new Map(); 125 | let numAttribs = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES); 126 | for (let aIndex = 0; aIndex < numAttribs; aIndex++) { 127 | let attribute = gl.getActiveAttrib(this.program, aIndex); 128 | this.attributeLocations.set(attribute.name, gl.getAttribLocation(this.program, attribute.name)); 129 | } 130 | } 131 | 132 | setBlendFunc(src, dst, srcAlpha, dstAlpha) { 133 | this.blendFunc.src = src; 134 | this.blendFunc.dst = dst; 135 | this.blendFunc.srcAlpha = srcAlpha; 136 | this.blendFunc.dstAlpha = dstAlpha; 137 | if (src) this.transparent = true; 138 | } 139 | 140 | setBlendEquation(modeRGB, modeAlpha) { 141 | this.blendEquation.modeRGB = modeRGB; 142 | this.blendEquation.modeAlpha = modeAlpha; 143 | } 144 | 145 | applyState() { 146 | if (this.depthTest) this.gl.renderer.enable(this.gl.DEPTH_TEST); 147 | else this.gl.renderer.disable(this.gl.DEPTH_TEST); 148 | 149 | if (this.cullFace) this.gl.renderer.enable(this.gl.CULL_FACE); 150 | else this.gl.renderer.disable(this.gl.CULL_FACE); 151 | 152 | if (this.blendFunc.src) this.gl.renderer.enable(this.gl.BLEND); 153 | else this.gl.renderer.disable(this.gl.BLEND); 154 | 155 | if (this.cullFace) this.gl.renderer.setCullFace(this.cullFace); 156 | this.gl.renderer.setFrontFace(this.frontFace); 157 | this.gl.renderer.setDepthMask(this.depthWrite); 158 | this.gl.renderer.setDepthFunc(this.depthFunc); 159 | if (this.blendFunc.src) this.gl.renderer.setBlendFunc(this.blendFunc.src, this.blendFunc.dst, this.blendFunc.srcAlpha, this.blendFunc.dstAlpha); 160 | if (this.blendEquation.modeRGB) this.gl.renderer.setBlendEquation(this.blendEquation.modeRGB, this.blendEquation.modeAlpha); 161 | } 162 | 163 | use({ 164 | programActive = false, 165 | flipFaces = false, 166 | } = {}) { 167 | let textureUnit = -1; 168 | 169 | // Avoid gl call if program already in use 170 | if (!programActive) { 171 | this.gl.useProgram(this.program); 172 | this.gl.renderer.currentProgram = this.id; 173 | } 174 | 175 | // Set only the active uniforms found in the shader 176 | this.uniformLocations.forEach((location, activeUniform) => { 177 | const name = activeUniform.uniformName; 178 | 179 | // get supplied uniform 180 | const uniform = this.uniforms[name]; 181 | if (!uniform) { 182 | return console.warn(`Active uniform ${name} has not been supplied`); 183 | } 184 | if (uniform && uniform.value === undefined) { 185 | return console.warn(`${name} uniform is missing a value parameter`); 186 | } 187 | 188 | if (uniform.value.texture) { 189 | textureUnit++; 190 | this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit); 191 | 192 | // bind texture and update any unset props 193 | uniform.value.update(); 194 | 195 | // set uniform using texture unit instead of value 196 | return setUniform(this.gl, activeUniform.type, location, textureUnit); 197 | } 198 | 199 | if (uniform.value.length && uniform.value[0].texture) { 200 | const textureUnits = []; 201 | uniform.value.forEach(value => { 202 | textureUnits.push(++textureUnit); 203 | this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit); 204 | value.update(); 205 | }); 206 | 207 | // For texture arrays, pass an array of texture units 208 | return setUniform(this.gl, activeUniform.type, location, textureUnits); 209 | } 210 | 211 | setUniform(this.gl, activeUniform.type, location, uniform.value); 212 | }); 213 | 214 | this.applyState(); 215 | if (flipFaces) this.gl.renderer.setFrontFace(this.frontFace === this.gl.CCW ? this.gl.CW : this.gl.CCW); 216 | } 217 | 218 | remove() { 219 | this.gl.deleteProgram(this.program); 220 | this.gl.deleteShader(this.vertexShader); 221 | this.gl.deleteShader(this.fragmentShader); 222 | } 223 | } -------------------------------------------------------------------------------- /src/shaders/PBRShader.js: -------------------------------------------------------------------------------- 1 | const vertex = `#version 300 es 2 | precision highp float; 3 | precision highp int; 4 | 5 | in vec3 position; 6 | in vec2 uv; 7 | in vec3 normal; 8 | 9 | uniform mat3 normalMatrix; 10 | uniform mat4 modelMatrix; 11 | uniform mat4 modelViewMatrix; 12 | uniform mat4 projectionMatrix; 13 | 14 | out vec2 vUv; 15 | out vec3 vNormal; 16 | out vec3 vMPos; 17 | 18 | void main() { 19 | vUv = uv; 20 | vNormal = normalize(normalMatrix * normal); 21 | vec4 mPos = modelMatrix * vec4(position, 1.0); 22 | vMPos = mPos.xyz / mPos.w; 23 | 24 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 25 | } 26 | `; 27 | 28 | const fragment = `#version 300 es 29 | precision highp float; 30 | precision highp int; 31 | 32 | uniform vec3 cameraPosition; 33 | uniform mat4 viewMatrix; 34 | 35 | uniform vec3 uBaseColor; 36 | 37 | uniform sampler2D tRMO; 38 | uniform float uMetallic; 39 | uniform float uRoughness; 40 | uniform float uOcclusion; 41 | 42 | uniform sampler2D tNormal; 43 | uniform float uNormalScale; 44 | uniform float uNormalUVScale; 45 | 46 | uniform sampler2D tLUT; 47 | uniform sampler2D tEnvDiffuse; 48 | uniform sampler2D tEnvSpecular; 49 | uniform float uEnvSpecular; 50 | 51 | uniform float uInputType; 52 | 53 | uniform vec3 uLightDirection; 54 | uniform vec3 uLightColor; 55 | 56 | in vec2 vUv; 57 | in vec3 vNormal; 58 | in vec3 vMPos; 59 | 60 | out vec4 FragColor; 61 | 62 | const float PI = 3.14159265359; 63 | const float RECIPROCAL_PI = 0.31830988618; 64 | const float RECIPROCAL_PI2 = 0.15915494; 65 | const float LN2 = 0.6931472; 66 | 67 | const float ENV_LODS = 6.0; 68 | 69 | vec4 SRGBtoLinear(vec4 srgb) { 70 | vec3 linOut = pow(srgb.xyz, vec3(2.2)); 71 | return vec4(linOut, srgb.w); 72 | } 73 | 74 | vec3 linearToSRGB(vec3 color) { 75 | return pow(color, vec3(1.0 / 2.2)); 76 | } 77 | 78 | vec4 RGBEToLinear(vec4 value) { 79 | return vec4(value.rgb * exp2(value.a * 255.0 - 128.0), 1.0); 80 | } 81 | 82 | vec4 RGBMToLinear(vec4 value) { 83 | float maxRange = 6.0; 84 | return vec4(value.xyz * value.w * maxRange, 1.0); 85 | } 86 | 87 | vec4 RGBDToLinear(vec4 value) { 88 | float maxRange = 6.0; 89 | return vec4(value.rgb * ((maxRange / 255.0) / value.a), 1.0); 90 | } 91 | 92 | vec3 getNormal() { 93 | vec3 pos_dx = dFdx(vMPos.xyz); 94 | vec3 pos_dy = dFdy(vMPos.xyz); 95 | vec2 tex_dx = dFdx(vUv); 96 | vec2 tex_dy = dFdy(vUv); 97 | 98 | vec3 t = normalize(pos_dx * tex_dy.t - pos_dy * tex_dx.t); 99 | vec3 b = normalize(-pos_dx * tex_dy.s + pos_dy * tex_dx.s); 100 | mat3 tbn = mat3(t, b, normalize(vNormal)); 101 | 102 | vec3 n = texture(tNormal, vUv * uNormalUVScale).rgb * 2.0 - 1.0; 103 | n.xy *= uNormalScale; 104 | vec3 normal = normalize(tbn * n); 105 | 106 | // Get world normal from view normal (normalMatrix * normal) 107 | return normalize((vec4(normal, 0.0) * viewMatrix).xyz); 108 | } 109 | 110 | vec3 specularReflection(vec3 specularEnvR0, vec3 specularEnvR90, float VdH) { 111 | return specularEnvR0 + (specularEnvR90 - specularEnvR0) * pow(clamp(1.0 - VdH, 0.0, 1.0), 5.0); 112 | } 113 | 114 | float geometricOcclusion(float NdL, float NdV, float roughness) { 115 | float r = roughness; 116 | 117 | float attenuationL = 2.0 * NdL / (NdL + sqrt(r * r + (1.0 - r * r) * (NdL * NdL))); 118 | float attenuationV = 2.0 * NdV / (NdV + sqrt(r * r + (1.0 - r * r) * (NdV * NdV))); 119 | return attenuationL * attenuationV; 120 | } 121 | 122 | float microfacetDistribution(float roughness, float NdH) { 123 | float roughnessSq = roughness * roughness; 124 | float f = (NdH * roughnessSq - NdH) * NdH + 1.0; 125 | return roughnessSq / (PI * f * f); 126 | } 127 | 128 | vec2 cartesianToPolar(vec3 n) { 129 | vec2 uv; 130 | uv.x = atan(n.z, n.x) * RECIPROCAL_PI2 + 0.5; 131 | uv.y = asin(n.y) * RECIPROCAL_PI + 0.5; 132 | return uv; 133 | } 134 | 135 | void getIBLContribution(inout vec3 diffuse, inout vec3 specular, float NdV, float roughness, vec3 n, vec3 reflection, vec3 diffuseColor, vec3 specularColor) { 136 | vec3 brdf = SRGBtoLinear(texture(tLUT, vec2(NdV, roughness))).rgb; 137 | 138 | // Sample 2 levels and mix between to get smoother degradation 139 | float blend = roughness * ENV_LODS; 140 | float level0 = floor(blend); 141 | float level1 = min(ENV_LODS, level0 + 1.0); 142 | blend -= level0; 143 | 144 | // Sample the specular env map atlas depending on the roughness value 145 | vec2 uvSpec = cartesianToPolar(reflection); 146 | uvSpec.y /= 2.0; 147 | 148 | vec2 uv0 = uvSpec; 149 | vec2 uv1 = uvSpec; 150 | 151 | uv0 /= pow(2.0, level0); 152 | uv0.y += 1.0 - exp(-LN2 * level0); 153 | 154 | uv1 /= pow(2.0, level1); 155 | uv1.y += 1.0 - exp(-LN2 * level1); 156 | 157 | vec3 diffuseLight; 158 | vec3 specular0; 159 | vec3 specular1; 160 | 161 | // 'If else' statements caused the strangest gpu bug 162 | // if (uInputType < 0.5) { 163 | // 164 | // // sRGB == 0 165 | // diffuseLight = SRGBToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb; 166 | // specular0 = SRGBToLinear(texture(tEnvSpecular, uv0)).rgb; 167 | // specular1 = SRGBToLinear(texture(tEnvSpecular, uv1)).rgb; 168 | // } else if (uInputType < 1.5) { 169 | // 170 | // // RGBE == 1 171 | // diffuseLight = RGBEToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb; 172 | // specular0 = RGBEToLinear(texture(tEnvSpecular, uv0)).rgb; 173 | // specular1 = RGBEToLinear(texture(tEnvSpecular, uv1)).rgb; 174 | // } else if (uInputType < 2.5) { 175 | // 176 | // // RGBM == 2 177 | // diffuseLight = RGBMToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb; 178 | // specular0 = RGBMToLinear(texture(tEnvSpecular, uv0)).rgb; 179 | // specular1 = RGBMToLinear(texture(tEnvSpecular, uv1)).rgb; 180 | // } else if (uInputType < 3.5) { 181 | // 182 | // // RGBD == 3 183 | // diffuseLight = RGBDToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb; 184 | // specular0 = RGBDToLinear(texture(tEnvSpecular, uv0)).rgb; 185 | // specular1 = RGBDToLinear(texture(tEnvSpecular, uv1)).rgb; 186 | // } 187 | 188 | 189 | // sRGB == 0 190 | diffuseLight = SRGBtoLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb; 191 | specular0 = SRGBtoLinear(texture(tEnvSpecular, uv0)).rgb; 192 | specular1 = SRGBtoLinear(texture(tEnvSpecular, uv1)).rgb; 193 | 194 | // RGBE == 1 195 | float mixRGBE = clamp(1.0 - abs(uInputType - 1.0), 0.0, 1.0); 196 | diffuseLight = mix(diffuseLight, RGBEToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb, mixRGBE); 197 | specular0 = mix(specular0, RGBEToLinear(texture(tEnvSpecular, uv0)).rgb, mixRGBE); 198 | specular1 = mix(specular1, RGBEToLinear(texture(tEnvSpecular, uv1)).rgb, mixRGBE); 199 | 200 | // RGBM == 2 201 | float mixRGBM = clamp(1.0 - abs(uInputType - 2.0), 0.0, 1.0); 202 | diffuseLight = mix(diffuseLight, RGBMToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb, mixRGBM); 203 | specular0 = mix(specular0, RGBMToLinear(texture(tEnvSpecular, uv0)).rgb, mixRGBM); 204 | specular1 = mix(specular1, RGBMToLinear(texture(tEnvSpecular, uv1)).rgb, mixRGBM); 205 | 206 | // RGBD == 3 207 | float mixRGBD = clamp(1.0 - abs(uInputType - 3.0), 0.0, 1.0); 208 | diffuseLight = mix(diffuseLight, RGBDToLinear(texture(tEnvDiffuse, cartesianToPolar(n))).rgb, mixRGBD); 209 | specular0 = mix(specular0, RGBDToLinear(texture(tEnvSpecular, uv0)).rgb, mixRGBD); 210 | specular1 = mix(specular1, RGBDToLinear(texture(tEnvSpecular, uv1)).rgb, mixRGBD); 211 | 212 | 213 | 214 | vec3 specularLight = mix(specular0, specular1, blend); 215 | 216 | diffuse = diffuseLight * diffuseColor; 217 | 218 | // Bit of extra reflection for smooth materials 219 | float reflectivity = pow((1.0 - roughness), 2.0) * 0.05; 220 | specular = specularLight * (specularColor * brdf.x + brdf.y + reflectivity); 221 | specular *= uEnvSpecular; 222 | } 223 | 224 | void main() { 225 | vec3 baseColor = uBaseColor; 226 | 227 | // RMO map packed as rgb = [roughness, metallic, occlusion] 228 | vec4 rmaSample = texture(tRMO, vUv); 229 | float roughness = clamp(rmaSample.r * uRoughness, 0.04, 1.0); 230 | float metallic = clamp(rmaSample.g * uMetallic, 0.04, 1.0); 231 | 232 | vec3 f0 = vec3(0.04); 233 | vec3 diffuseColor = baseColor * (vec3(1.0) - f0) * (1.0 - metallic); 234 | vec3 specularColor = mix(f0, baseColor, metallic); 235 | 236 | vec3 specularEnvR0 = specularColor; 237 | vec3 specularEnvR90 = vec3(clamp(max(max(specularColor.r, specularColor.g), specularColor.b) * 25.0, 0.0, 1.0)); 238 | 239 | vec3 N = getNormal(); 240 | vec3 V = normalize(cameraPosition - vMPos); 241 | vec3 L = normalize(uLightDirection); 242 | vec3 H = normalize(L + V); 243 | vec3 reflection = normalize(reflect(-V, N)); 244 | 245 | float NdL = clamp(dot(N, L), 0.001, 1.0); 246 | float NdV = clamp(abs(dot(N, V)), 0.001, 1.0); 247 | float NdH = clamp(dot(N, H), 0.0, 1.0); 248 | float LdH = clamp(dot(L, H), 0.0, 1.0); 249 | float VdH = clamp(dot(V, H), 0.0, 1.0); 250 | 251 | vec3 F = specularReflection(specularEnvR0, specularEnvR90, VdH); 252 | float G = geometricOcclusion(NdL, NdV, roughness); 253 | float D = microfacetDistribution(roughness, NdH); 254 | 255 | vec3 diffuseContrib = (1.0 - F) * (diffuseColor / PI); 256 | vec3 specContrib = F * G * D / (4.0 * NdL * NdV); 257 | 258 | // Shading based off lights 259 | vec3 color = NdL * uLightColor * (diffuseContrib + specContrib); 260 | 261 | // Calculate IBL lighting 262 | vec3 diffuseIBL; 263 | vec3 specularIBL; 264 | getIBLContribution(diffuseIBL, specularIBL, NdV, roughness, N, reflection, diffuseColor, specularColor); 265 | 266 | // Add IBL on top of color 267 | color += diffuseIBL + specularIBL; 268 | 269 | // Multiply occlusion 270 | color = mix(color, color * rmaSample.b, uOcclusion); 271 | 272 | // Convert to sRGB to display 273 | FragColor.rgb = linearToSRGB(color); 274 | FragColor.a = 1.0; 275 | } 276 | `; 277 | 278 | export default {vertex, fragment}; -------------------------------------------------------------------------------- /ogl/core/Renderer.js: -------------------------------------------------------------------------------- 1 | import {Mat4} from '../math/Mat4.js'; 2 | import {Vec3} from '../math/Vec3.js'; 3 | 4 | // TODO: frustum culling 5 | // TODO: gl.canvas.addEventListener('webglcontextlost', contextLost, false); 6 | // TODO: gl.canvas.addEventListener('webglcontextrestored', contextRestore, false); 7 | 8 | // Not automatic - devs to use these methods manually 9 | // gl.colorMask( colorMask, colorMask, colorMask, colorMask ); 10 | // gl.clearColor( r, g, b, a ); 11 | // gl.stencilMask( stencilMask ); 12 | // gl.stencilFunc( stencilFunc, stencilRef, stencilMask ); 13 | // gl.stencilOp( stencilFail, stencilZFail, stencilZPass ); 14 | // gl.clearStencil( stencil ); 15 | 16 | const projMat4 = new Mat4(); 17 | const tempVec3 = new Vec3(); 18 | 19 | export class Renderer { 20 | constructor({ 21 | canvas = document.createElement('canvas'), 22 | width = 300, 23 | height = 150, 24 | dpr = 1, 25 | alpha = false, 26 | depth = true, 27 | stencil = false, 28 | antialias = false, 29 | premultipliedAlpha = false, 30 | preserveDrawingBuffer = false, 31 | powerPreference = 'default', 32 | autoClear = true, 33 | sort = true, 34 | } = {}) { 35 | const attributes = {alpha, depth, stencil, antialias, premultipliedAlpha, preserveDrawingBuffer, powerPreference}; 36 | this.dpr = dpr; 37 | this.alpha = alpha; 38 | this.color = true; 39 | this.depth = depth; 40 | this.stencil = stencil; 41 | this.premultipliedAlpha = premultipliedAlpha; 42 | this.autoClear = autoClear; 43 | this.sort = sort; 44 | 45 | // Attempt WebGL2, otherwise fallback to WebGL1 46 | this.gl = canvas.getContext('webgl2', attributes); 47 | this.isWebgl2 = !!this.gl; 48 | if (!this.gl) { 49 | this.gl = canvas.getContext('webgl', attributes) || canvas.getContext('experimental-webgl', attributes); 50 | } 51 | 52 | // Attach renderer to gl so that all classes have access to internal state functions 53 | this.gl.renderer = this; 54 | 55 | // initialise size values 56 | this.setSize(width, height); 57 | 58 | // gl state stores to avoid redundant calls on methods used internally 59 | this.state = {}; 60 | this.state.blendFunc = {src: this.gl.ONE, dst: this.gl.ZERO}; 61 | this.state.blendEquation = {modeRGB: this.gl.FUNC_ADD}; 62 | this.state.cullFace = null; 63 | this.state.frontFace = this.gl.CCW; 64 | this.state.depthMask = true; 65 | this.state.depthFunc = this.gl.LESS; 66 | this.state.framebuffer = null; 67 | this.state.viewport = {width: null, height: null}; 68 | 69 | // store requested extensions 70 | this.extensions = {}; 71 | 72 | if (!this.isWebgl2) { 73 | 74 | // Initialise extra format types 75 | this.getExtension('OES_texture_float'); 76 | this.getExtension('OES_texture_float_linear'); 77 | this.getExtension('OES_texture_half_float'); 78 | this.getExtension('OES_element_index_uint'); 79 | this.getExtension('OES_standard_derivatives'); 80 | this.getExtension('EXT_sRGB'); 81 | this.getExtension('WEBGL_depth_texture'); 82 | } 83 | 84 | // Create method aliases using extension or native if available 85 | this.vertexAttribDivisor = this.gl.renderer.getExtension('ANGLE_instanced_arrays', 'vertexAttribDivisor', 'vertexAttribDivisorANGLE'); 86 | this.drawArraysInstanced = this.gl.renderer.getExtension('ANGLE_instanced_arrays', 'drawArraysInstanced', 'drawArraysInstancedANGLE'); 87 | this.drawElementsInstanced = this.gl.renderer.getExtension('ANGLE_instanced_arrays', 'drawElementsInstanced', 'drawElementsInstancedANGLE'); 88 | this.createVertexArray = this.gl.renderer.getExtension('OES_vertex_array_object', 'createVertexArray', 'createVertexArrayOES'); 89 | this.bindVertexArray = this.gl.renderer.getExtension('OES_vertex_array_object', 'bindVertexArray', 'bindVertexArrayOES'); 90 | this.deleteVertexArray = this.gl.renderer.getExtension('OES_vertex_array_object', 'deleteVertexArray', 'deleteVertexArrayOES'); 91 | } 92 | 93 | setSize(width, height) { 94 | this.width = width; 95 | this.height = height; 96 | 97 | this.gl.canvas.width = width * this.dpr; 98 | this.gl.canvas.height = height * this.dpr; 99 | 100 | Object.assign(this.gl.canvas.style, { 101 | width: width + 'px', 102 | height: height + 'px', 103 | }); 104 | } 105 | 106 | setViewport(width, height) { 107 | if (this.state.viewport.width === width && this.state.viewport.height === height) return; 108 | this.state.viewport.width = width; 109 | this.state.viewport.height = height; 110 | this.gl.viewport(0, 0, width, height); 111 | } 112 | 113 | enable(id) { 114 | if (this.state[id] === true) return; 115 | this.gl.enable(id); 116 | this.state[id] = true; 117 | } 118 | 119 | disable(id) { 120 | if (this.state[id] === false) return; 121 | this.gl.disable(id); 122 | this.state[id] = false; 123 | } 124 | 125 | setBlendFunc(src, dst, srcAlpha, dstAlpha) { 126 | if (this.state.blendFunc.src === src && 127 | this.state.blendFunc.dst === dst && 128 | this.state.blendFunc.srcAlpha === srcAlpha && 129 | this.state.blendFunc.dstAlpha === dstAlpha) return; 130 | this.state.blendFunc.src = src; 131 | this.state.blendFunc.dst = dst; 132 | this.state.blendFunc.srcAlpha = srcAlpha; 133 | this.state.blendFunc.dstAlpha = dstAlpha; 134 | if (srcAlpha !== undefined) this.gl.blendFuncSeparate(src, dst, srcAlpha, dstAlpha); 135 | else this.gl.blendFunc(src, dst); 136 | } 137 | 138 | setBlendEquation(modeRGB, modeAlpha) { 139 | if (this.state.blendEquation.modeRGB === modeRGB && 140 | this.state.blendEquation.modeAlpha === modeAlpha) return; 141 | this.state.blendEquation.modeRGB = modeRGB; 142 | this.state.blendEquation.modeAlpha = modeAlpha; 143 | if (modeAlpha !== undefined) this.gl.blendEquationSeparate(modeRGB, modeAlpha); 144 | else this.gl.blendEquation(modeRGB); 145 | } 146 | 147 | setCullFace(value) { 148 | if (this.state.cullFace === value) return; 149 | this.state.cullFace = value; 150 | this.gl.cullFace(value); 151 | } 152 | 153 | setFrontFace(value) { 154 | if (this.state.frontFace === value) return; 155 | this.state.frontFace = value; 156 | this.gl.frontFace(value); 157 | } 158 | 159 | setDepthMask(value) { 160 | if (this.state.depthMask === value) return; 161 | this.state.depthMask = value; 162 | this.gl.depthMask(value); 163 | } 164 | 165 | setDepthFunc(value) { 166 | if (this.state.depthFunc === value) return; 167 | this.state.depthFunc = value; 168 | this.gl.depthFunc(value); 169 | } 170 | 171 | bindFramebuffer({target = this.gl.FRAMEBUFFER, buffer = null} = {}) { 172 | if (this.state.framebuffer === buffer) return; 173 | this.state.framebuffer = buffer; 174 | this.gl.bindFramebuffer(target, buffer); 175 | } 176 | 177 | getExtension(extension, webgl2Func, extFunc) { 178 | 179 | // if webgl2 function supported, return func bound to gl context 180 | if (webgl2Func && this.gl[webgl2Func]) return this.gl[webgl2Func].bind(this.gl); 181 | 182 | // fetch extension once only 183 | if (!this.extensions[extension]) { 184 | this.extensions[extension] = this.gl.getExtension(extension); 185 | } 186 | 187 | // return extension if no function requested 188 | if (!webgl2Func) return this.extensions[extension]; 189 | 190 | // return extension function, bound to extension 191 | return this.extensions[extension][extFunc].bind(this.extensions[extension]); 192 | } 193 | 194 | drawOpaque(scene, camera) { 195 | const opaque = []; 196 | scene.traverse(node => { 197 | if (!node.program || node.program.transparent) return; 198 | opaque.splice(getRenderOrder(node), 0, node); 199 | }); 200 | 201 | function getRenderOrder(node) { 202 | node.worldMatrix.getTranslation(tempVec3); 203 | tempVec3.applyMatrix4(projMat4); 204 | 205 | node.zDepth = tempVec3.z; 206 | for (let i = 0; i < opaque.length; i++) { 207 | if (node.zDepth <= opaque[i].zDepth) return i; 208 | } 209 | return opaque.length; 210 | } 211 | 212 | for (let i = 0; i < opaque.length; i++) { 213 | opaque[i].draw({camera}); 214 | } 215 | } 216 | 217 | drawTransparent(scene, camera) { 218 | const transparent = []; 219 | const custom = []; 220 | scene.traverse(node => { 221 | if (!node.program || !node.program.transparent) return; 222 | 223 | // If manual order set, add to queue last 224 | if (node.renderOrder !== undefined) return custom.push(node); 225 | transparent.splice(getRenderOrder(node), 0, node); 226 | }); 227 | 228 | function getRenderOrder(node) { 229 | node.worldMatrix.getTranslation(tempVec3); 230 | tempVec3.applyMatrix4(projMat4); 231 | 232 | node.zDepth = tempVec3.z; 233 | for (let i = 0; i < transparent.length; i++) { 234 | if (node.zDepth >= transparent[i].zDepth) return i; 235 | } 236 | return transparent.length; 237 | } 238 | 239 | // Add manually ordered nodes 240 | for (let i = 0; i < custom.length; i++) { 241 | transparent.splice(custom[i].renderOrder, 0, custom[i]); 242 | } 243 | 244 | for (let i = 0; i < transparent.length; i++) { 245 | transparent[i].draw({camera}); 246 | } 247 | } 248 | 249 | render({ 250 | scene, 251 | camera, 252 | target = null, 253 | update = true, 254 | }) { 255 | 256 | if (target === null) { 257 | 258 | // make sure no render target bound so draws to canvas 259 | this.bindFramebuffer(); 260 | this.setViewport(this.width * this.dpr, this.height * this.dpr); 261 | } else { 262 | 263 | // bind supplied render target and update viewport 264 | this.bindFramebuffer(target); 265 | this.setViewport(target.width, target.height); 266 | } 267 | 268 | if (this.autoClear) { 269 | 270 | // Ensure depth buffer writing is enabled so it can be cleared 271 | if (this.depth) { 272 | this.enable(this.gl.DEPTH_TEST); 273 | this.setDepthMask(true); 274 | } 275 | this.gl.clear((this.color ? this.gl.COLOR_BUFFER_BIT : 0) | (this.depth ? this.gl.DEPTH_BUFFER_BIT : 0) | (this.stencil ? this.gl.STENCIL_BUFFER_BIT : 0)); 276 | } 277 | 278 | // updates all scene graph matrices 279 | if (update) scene.updateMatrixWorld(); 280 | 281 | // Update camera separately if not in scene graph 282 | if (camera && camera.parent === null) camera.updateMatrixWorld(); 283 | 284 | // can only sort if camera available 285 | if (this.sort && camera) { 286 | projMat4.multiply(camera.projectionMatrix, camera.viewMatrix); 287 | this.drawOpaque(scene, camera); 288 | this.drawTransparent(scene, camera); 289 | } else { 290 | scene.traverse(node => { 291 | if (!node.draw) return; 292 | node.draw({camera}); 293 | }); 294 | } 295 | 296 | // culling 297 | // _projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); 298 | // _frustum.setFromMatrix( _projScreenMatrix ); 299 | } 300 | } -------------------------------------------------------------------------------- /ogl/math/functions/Vec2Func.js: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.000001; 2 | 3 | /** 4 | * Copy the values from one vec2 to another 5 | * 6 | * @param {vec2} out the receiving vector 7 | * @param {vec2} a the source vector 8 | * @returns {vec2} out 9 | */ 10 | export function copy(out, a) { 11 | out[0] = a[0]; 12 | out[1] = a[1]; 13 | return out; 14 | } 15 | 16 | /** 17 | * Set the components of a vec2 to the given values 18 | * 19 | * @param {vec2} out the receiving vector 20 | * @param {Number} x X component 21 | * @param {Number} y Y component 22 | * @returns {vec2} out 23 | */ 24 | export function set(out, x, y) { 25 | out[0] = x; 26 | out[1] = y; 27 | return out; 28 | } 29 | 30 | /** 31 | * Adds two vec2's 32 | * 33 | * @param {vec2} out the receiving vector 34 | * @param {vec2} a the first operand 35 | * @param {vec2} b the second operand 36 | * @returns {vec2} out 37 | */ 38 | export function add(out, a, b) { 39 | out[0] = a[0] + b[0]; 40 | out[1] = a[1] + b[1]; 41 | return out; 42 | } 43 | 44 | /** 45 | * Subtracts vector b from vector a 46 | * 47 | * @param {vec2} out the receiving vector 48 | * @param {vec2} a the first operand 49 | * @param {vec2} b the second operand 50 | * @returns {vec2} out 51 | */ 52 | export function subtract(out, a, b) { 53 | out[0] = a[0] - b[0]; 54 | out[1] = a[1] - b[1]; 55 | return out; 56 | } 57 | 58 | /** 59 | * Multiplies two vec2's 60 | * 61 | * @param {vec2} out the receiving vector 62 | * @param {vec2} a the first operand 63 | * @param {vec2} b the second operand 64 | * @returns {vec2} out 65 | */ 66 | export function multiply(out, a, b) { 67 | out[0] = a[0] * b[0]; 68 | out[1] = a[1] * b[1]; 69 | return out; 70 | }; 71 | 72 | /** 73 | * Divides two vec2's 74 | * 75 | * @param {vec2} out the receiving vector 76 | * @param {vec2} a the first operand 77 | * @param {vec2} b the second operand 78 | * @returns {vec2} out 79 | */ 80 | export function divide(out, a, b) { 81 | out[0] = a[0] / b[0]; 82 | out[1] = a[1] / b[1]; 83 | return out; 84 | }; 85 | 86 | /** 87 | * Math.ceil the components of a vec2 88 | * 89 | * @param {vec2} out the receiving vector 90 | * @param {vec2} a vector to ceil 91 | * @returns {vec2} out 92 | */ 93 | export function ceil(out, a) { 94 | out[0] = Math.ceil(a[0]); 95 | out[1] = Math.ceil(a[1]); 96 | return out; 97 | }; 98 | 99 | /** 100 | * Math.floor the components of a vec2 101 | * 102 | * @param {vec2} out the receiving vector 103 | * @param {vec2} a vector to floor 104 | * @returns {vec2} out 105 | */ 106 | export function floor(out, a) { 107 | out[0] = Math.floor(a[0]); 108 | out[1] = Math.floor(a[1]); 109 | return out; 110 | }; 111 | 112 | /** 113 | * Returns the minimum of two vec2's 114 | * 115 | * @param {vec2} out the receiving vector 116 | * @param {vec2} a the first operand 117 | * @param {vec2} b the second operand 118 | * @returns {vec2} out 119 | */ 120 | export function min(out, a, b) { 121 | out[0] = Math.min(a[0], b[0]); 122 | out[1] = Math.min(a[1], b[1]); 123 | return out; 124 | }; 125 | 126 | /** 127 | * Returns the maximum of two vec2's 128 | * 129 | * @param {vec2} out the receiving vector 130 | * @param {vec2} a the first operand 131 | * @param {vec2} b the second operand 132 | * @returns {vec2} out 133 | */ 134 | export function max(out, a, b) { 135 | out[0] = Math.max(a[0], b[0]); 136 | out[1] = Math.max(a[1], b[1]); 137 | return out; 138 | }; 139 | 140 | /** 141 | * Math.round the components of a vec2 142 | * 143 | * @param {vec2} out the receiving vector 144 | * @param {vec2} a vector to round 145 | * @returns {vec2} out 146 | */ 147 | export function round(out, a) { 148 | out[0] = Math.round(a[0]); 149 | out[1] = Math.round(a[1]); 150 | return out; 151 | }; 152 | 153 | /** 154 | * Scales a vec2 by a scalar number 155 | * 156 | * @param {vec2} out the receiving vector 157 | * @param {vec2} a the vector to scale 158 | * @param {Number} b amount to scale the vector by 159 | * @returns {vec2} out 160 | */ 161 | export function scale(out, a, b) { 162 | out[0] = a[0] * b; 163 | out[1] = a[1] * b; 164 | return out; 165 | }; 166 | 167 | /** 168 | * Adds two vec2's after scaling the second operand by a scalar value 169 | * 170 | * @param {vec2} out the receiving vector 171 | * @param {vec2} a the first operand 172 | * @param {vec2} b the second operand 173 | * @param {Number} scale the amount to scale b by before adding 174 | * @returns {vec2} out 175 | */ 176 | export function scaleAndAdd(out, a, b, scale) { 177 | out[0] = a[0] + (b[0] * scale); 178 | out[1] = a[1] + (b[1] * scale); 179 | return out; 180 | }; 181 | 182 | /** 183 | * Calculates the euclidian distance between two vec2's 184 | * 185 | * @param {vec2} a the first operand 186 | * @param {vec2} b the second operand 187 | * @returns {Number} distance between a and b 188 | */ 189 | export function distance(a, b) { 190 | var x = b[0] - a[0], 191 | y = b[1] - a[1]; 192 | return Math.sqrt(x * x + y * y); 193 | }; 194 | 195 | /** 196 | * Calculates the squared euclidian distance between two vec2's 197 | * 198 | * @param {vec2} a the first operand 199 | * @param {vec2} b the second operand 200 | * @returns {Number} squared distance between a and b 201 | */ 202 | export function squaredDistance(a, b) { 203 | var x = b[0] - a[0], 204 | y = b[1] - a[1]; 205 | return x * x + y * y; 206 | }; 207 | 208 | /** 209 | * Calculates the length of a vec2 210 | * 211 | * @param {vec2} a vector to calculate length of 212 | * @returns {Number} length of a 213 | */ 214 | export function length(a) { 215 | var x = a[0], 216 | y = a[1]; 217 | return Math.sqrt(x * x + y * y); 218 | }; 219 | 220 | /** 221 | * Calculates the squared length of a vec2 222 | * 223 | * @param {vec2} a vector to calculate squared length of 224 | * @returns {Number} squared length of a 225 | */ 226 | export function squaredLength(a) { 227 | var x = a[0], 228 | y = a[1]; 229 | return x * x + y * y; 230 | }; 231 | 232 | /** 233 | * Negates the components of a vec2 234 | * 235 | * @param {vec2} out the receiving vector 236 | * @param {vec2} a vector to negate 237 | * @returns {vec2} out 238 | */ 239 | export function negate(out, a) { 240 | out[0] = -a[0]; 241 | out[1] = -a[1]; 242 | return out; 243 | }; 244 | 245 | /** 246 | * Returns the inverse of the components of a vec2 247 | * 248 | * @param {vec2} out the receiving vector 249 | * @param {vec2} a vector to invert 250 | * @returns {vec2} out 251 | */ 252 | export function inverse(out, a) { 253 | out[0] = 1.0 / a[0]; 254 | out[1] = 1.0 / a[1]; 255 | return out; 256 | }; 257 | 258 | /** 259 | * Normalize a vec2 260 | * 261 | * @param {vec2} out the receiving vector 262 | * @param {vec2} a vector to normalize 263 | * @returns {vec2} out 264 | */ 265 | export function normalize(out, a) { 266 | var x = a[0], 267 | y = a[1]; 268 | var len = x * x + y * y; 269 | if (len > 0) { 270 | //TODO: evaluate use of glm_invsqrt here? 271 | len = 1 / Math.sqrt(len); 272 | out[0] = a[0] * len; 273 | out[1] = a[1] * len; 274 | } 275 | return out; 276 | }; 277 | 278 | /** 279 | * Calculates the dot product of two vec2's 280 | * 281 | * @param {vec2} a the first operand 282 | * @param {vec2} b the second operand 283 | * @returns {Number} dot product of a and b 284 | */ 285 | export function dot(a, b) { 286 | return a[0] * b[0] + a[1] * b[1]; 287 | }; 288 | 289 | /** 290 | * Computes the cross product of two vec2's 291 | * Note that the cross product must by definition produce a 3D vector 292 | * 293 | * @param {vec3} out the receiving vector 294 | * @param {vec2} a the first operand 295 | * @param {vec2} b the second operand 296 | * @returns {vec3} out 297 | */ 298 | export function cross(out, a, b) { 299 | var z = a[0] * b[1] - a[1] * b[0]; 300 | out[0] = out[1] = 0; 301 | out[2] = z; 302 | return out; 303 | }; 304 | 305 | /** 306 | * Performs a linear interpolation between two vec2's 307 | * 308 | * @param {vec2} out the receiving vector 309 | * @param {vec2} a the first operand 310 | * @param {vec2} b the second operand 311 | * @param {Number} t interpolation amount between the two inputs 312 | * @returns {vec2} out 313 | */ 314 | export function lerp(out, a, b, t) { 315 | var ax = a[0], 316 | ay = a[1]; 317 | out[0] = ax + t * (b[0] - ax); 318 | out[1] = ay + t * (b[1] - ay); 319 | return out; 320 | }; 321 | 322 | /** 323 | * Generates a random vector with the given scale 324 | * 325 | * @param {vec2} out the receiving vector 326 | * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned 327 | * @returns {vec2} out 328 | */ 329 | export function random(out, scale) { 330 | scale = scale || 1.0; 331 | var r = Math.random() * 2.0 * Math.PI; 332 | out[0] = Math.cos(r) * scale; 333 | out[1] = Math.sin(r) * scale; 334 | return out; 335 | }; 336 | 337 | /** 338 | * Transforms the vec2 with a mat2 339 | * 340 | * @param {vec2} out the receiving vector 341 | * @param {vec2} a the vector to transform 342 | * @param {mat2} m matrix to transform with 343 | * @returns {vec2} out 344 | */ 345 | export function transformMat2(out, a, m) { 346 | var x = a[0], 347 | y = a[1]; 348 | out[0] = m[0] * x + m[2] * y; 349 | out[1] = m[1] * x + m[3] * y; 350 | return out; 351 | }; 352 | 353 | /** 354 | * Transforms the vec2 with a mat2d 355 | * 356 | * @param {vec2} out the receiving vector 357 | * @param {vec2} a the vector to transform 358 | * @param {mat2d} m matrix to transform with 359 | * @returns {vec2} out 360 | */ 361 | export function transformMat2d(out, a, m) { 362 | var x = a[0], 363 | y = a[1]; 364 | out[0] = m[0] * x + m[2] * y + m[4]; 365 | out[1] = m[1] * x + m[3] * y + m[5]; 366 | return out; 367 | }; 368 | 369 | /** 370 | * Transforms the vec2 with a mat3 371 | * 3rd vector component is implicitly '1' 372 | * 373 | * @param {vec2} out the receiving vector 374 | * @param {vec2} a the vector to transform 375 | * @param {mat3} m matrix to transform with 376 | * @returns {vec2} out 377 | */ 378 | export function transformMat3(out, a, m) { 379 | var x = a[0], 380 | y = a[1]; 381 | out[0] = m[0] * x + m[3] * y + m[6]; 382 | out[1] = m[1] * x + m[4] * y + m[7]; 383 | return out; 384 | }; 385 | 386 | /** 387 | * Transforms the vec2 with a mat4 388 | * 3rd vector component is implicitly '0' 389 | * 4th vector component is implicitly '1' 390 | * 391 | * @param {vec2} out the receiving vector 392 | * @param {vec2} a the vector to transform 393 | * @param {mat4} m matrix to transform with 394 | * @returns {vec2} out 395 | */ 396 | export function transformMat4(out, a, m) { 397 | let x = a[0]; 398 | let y = a[1]; 399 | out[0] = m[0] * x + m[4] * y + m[12]; 400 | out[1] = m[1] * x + m[5] * y + m[13]; 401 | return out; 402 | } 403 | 404 | /** 405 | * Returns a string representation of a vector 406 | * 407 | * @param {vec2} a vector to represent as a string 408 | * @returns {String} string representation of the vector 409 | */ 410 | export function str(a) { 411 | return 'vec2(' + a[0] + ', ' + a[1] + ')'; 412 | } 413 | 414 | /** 415 | * Returns whether or not the vectors exactly have the same elements in the same position (when compared with ===) 416 | * 417 | * @param {vec2} a The first vector. 418 | * @param {vec2} b The second vector. 419 | * @returns {Boolean} True if the vectors are equal, false otherwise. 420 | */ 421 | export function exactEquals(a, b) { 422 | return a[0] === b[0] && a[1] === b[1]; 423 | } 424 | 425 | /** 426 | * Returns whether or not the vectors have approximately the same elements in the same position. 427 | * 428 | * @param {vec2} a The first vector. 429 | * @param {vec2} b The second vector. 430 | * @returns {Boolean} True if the vectors are equal, false otherwise. 431 | */ 432 | export function equals(a, b) { 433 | let a0 = a[0], a1 = a[1]; 434 | let b0 = b[0], b1 = b[1]; 435 | return (Math.abs(a0 - b0) <= EPSILON * Math.max(1.0, Math.abs(a0), Math.abs(b0)) && 436 | Math.abs(a1 - b1) <= EPSILON * Math.max(1.0, Math.abs(a1), Math.abs(b1))); 437 | } -------------------------------------------------------------------------------- /ogl/math/functions/Vec4Func.js: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.000001; 2 | 3 | /** 4 | * Copy the values from one vec4 to another 5 | * 6 | * @param {vec4} out the receiving vector 7 | * @param {vec4} a the source vector 8 | * @returns {vec4} out 9 | */ 10 | export function copy(out, a) { 11 | out[0] = a[0]; 12 | out[1] = a[1]; 13 | out[2] = a[2]; 14 | out[3] = a[3]; 15 | return out; 16 | } 17 | 18 | /** 19 | * Set the components of a vec4 to the given values 20 | * 21 | * @param {vec4} out the receiving vector 22 | * @param {Number} x X component 23 | * @param {Number} y Y component 24 | * @param {Number} z Z component 25 | * @param {Number} w W component 26 | * @returns {vec4} out 27 | */ 28 | export function set(out, x, y, z, w) { 29 | out[0] = x; 30 | out[1] = y; 31 | out[2] = z; 32 | out[3] = w; 33 | return out; 34 | } 35 | 36 | /** 37 | * Adds two vec4's 38 | * 39 | * @param {vec4} out the receiving vector 40 | * @param {vec4} a the first operand 41 | * @param {vec4} b the second operand 42 | * @returns {vec4} out 43 | */ 44 | export function add(out, a, b) { 45 | out[0] = a[0] + b[0]; 46 | out[1] = a[1] + b[1]; 47 | out[2] = a[2] + b[2]; 48 | out[3] = a[3] + b[3]; 49 | return out; 50 | } 51 | 52 | /** 53 | * Subtracts vector b from vector a 54 | * 55 | * @param {vec4} out the receiving vector 56 | * @param {vec4} a the first operand 57 | * @param {vec4} b the second operand 58 | * @returns {vec4} out 59 | */ 60 | export function subtract(out, a, b) { 61 | out[0] = a[0] - b[0]; 62 | out[1] = a[1] - b[1]; 63 | out[2] = a[2] - b[2]; 64 | out[3] = a[3] - b[3]; 65 | return out; 66 | } 67 | 68 | /** 69 | * Multiplies two vec4's 70 | * 71 | * @param {vec4} out the receiving vector 72 | * @param {vec4} a the first operand 73 | * @param {vec4} b the second operand 74 | * @returns {vec4} out 75 | */ 76 | export function multiply(out, a, b) { 77 | out[0] = a[0] * b[0]; 78 | out[1] = a[1] * b[1]; 79 | out[2] = a[2] * b[2]; 80 | out[3] = a[3] * b[3]; 81 | return out; 82 | } 83 | 84 | /** 85 | * Divides two vec4's 86 | * 87 | * @param {vec4} out the receiving vector 88 | * @param {vec4} a the first operand 89 | * @param {vec4} b the second operand 90 | * @returns {vec4} out 91 | */ 92 | export function divide(out, a, b) { 93 | out[0] = a[0] / b[0]; 94 | out[1] = a[1] / b[1]; 95 | out[2] = a[2] / b[2]; 96 | out[3] = a[3] / b[3]; 97 | return out; 98 | } 99 | 100 | /** 101 | * Math.ceil the components of a vec4 102 | * 103 | * @param {vec4} out the receiving vector 104 | * @param {vec4} a vector to ceil 105 | * @returns {vec4} out 106 | */ 107 | export function ceil(out, a) { 108 | out[0] = Math.ceil(a[0]); 109 | out[1] = Math.ceil(a[1]); 110 | out[2] = Math.ceil(a[2]); 111 | out[3] = Math.ceil(a[3]); 112 | return out; 113 | } 114 | 115 | /** 116 | * Math.floor the components of a vec4 117 | * 118 | * @param {vec4} out the receiving vector 119 | * @param {vec4} a vector to floor 120 | * @returns {vec4} out 121 | */ 122 | export function floor(out, a) { 123 | out[0] = Math.floor(a[0]); 124 | out[1] = Math.floor(a[1]); 125 | out[2] = Math.floor(a[2]); 126 | out[3] = Math.floor(a[3]); 127 | return out; 128 | } 129 | 130 | /** 131 | * Returns the minimum of two vec4's 132 | * 133 | * @param {vec4} out the receiving vector 134 | * @param {vec4} a the first operand 135 | * @param {vec4} b the second operand 136 | * @returns {vec4} out 137 | */ 138 | export function min(out, a, b) { 139 | out[0] = Math.min(a[0], b[0]); 140 | out[1] = Math.min(a[1], b[1]); 141 | out[2] = Math.min(a[2], b[2]); 142 | out[3] = Math.min(a[3], b[3]); 143 | return out; 144 | } 145 | 146 | /** 147 | * Returns the maximum of two vec4's 148 | * 149 | * @param {vec4} out the receiving vector 150 | * @param {vec4} a the first operand 151 | * @param {vec4} b the second operand 152 | * @returns {vec4} out 153 | */ 154 | export function max(out, a, b) { 155 | out[0] = Math.max(a[0], b[0]); 156 | out[1] = Math.max(a[1], b[1]); 157 | out[2] = Math.max(a[2], b[2]); 158 | out[3] = Math.max(a[3], b[3]); 159 | return out; 160 | } 161 | 162 | /** 163 | * Math.round the components of a vec4 164 | * 165 | * @param {vec4} out the receiving vector 166 | * @param {vec4} a vector to round 167 | * @returns {vec4} out 168 | */ 169 | export function round(out, a) { 170 | out[0] = Math.round(a[0]); 171 | out[1] = Math.round(a[1]); 172 | out[2] = Math.round(a[2]); 173 | out[3] = Math.round(a[3]); 174 | return out; 175 | } 176 | 177 | /** 178 | * Scales a vec4 by a scalar number 179 | * 180 | * @param {vec4} out the receiving vector 181 | * @param {vec4} a the vector to scale 182 | * @param {Number} b amount to scale the vector by 183 | * @returns {vec4} out 184 | */ 185 | export function scale(out, a, b) { 186 | out[0] = a[0] * b; 187 | out[1] = a[1] * b; 188 | out[2] = a[2] * b; 189 | out[3] = a[3] * b; 190 | return out; 191 | } 192 | 193 | /** 194 | * Adds two vec4's after scaling the second operand by a scalar value 195 | * 196 | * @param {vec4} out the receiving vector 197 | * @param {vec4} a the first operand 198 | * @param {vec4} b the second operand 199 | * @param {Number} scale the amount to scale b by before adding 200 | * @returns {vec4} out 201 | */ 202 | export function scaleAndAdd(out, a, b, scale) { 203 | out[0] = a[0] + (b[0] * scale); 204 | out[1] = a[1] + (b[1] * scale); 205 | out[2] = a[2] + (b[2] * scale); 206 | out[3] = a[3] + (b[3] * scale); 207 | return out; 208 | } 209 | 210 | /** 211 | * Calculates the euclidian distance between two vec4's 212 | * 213 | * @param {vec4} a the first operand 214 | * @param {vec4} b the second operand 215 | * @returns {Number} distance between a and b 216 | */ 217 | export function distance(a, b) { 218 | let x = b[0] - a[0]; 219 | let y = b[1] - a[1]; 220 | let z = b[2] - a[2]; 221 | let w = b[3] - a[3]; 222 | return Math.sqrt(x * x + y * y + z * z + w * w); 223 | } 224 | 225 | /** 226 | * Calculates the squared euclidian distance between two vec4's 227 | * 228 | * @param {vec4} a the first operand 229 | * @param {vec4} b the second operand 230 | * @returns {Number} squared distance between a and b 231 | */ 232 | export function squaredDistance(a, b) { 233 | let x = b[0] - a[0]; 234 | let y = b[1] - a[1]; 235 | let z = b[2] - a[2]; 236 | let w = b[3] - a[3]; 237 | return x * x + y * y + z * z + w * w; 238 | } 239 | 240 | /** 241 | * Calculates the length of a vec4 242 | * 243 | * @param {vec4} a vector to calculate length of 244 | * @returns {Number} length of a 245 | */ 246 | export function length(a) { 247 | let x = a[0]; 248 | let y = a[1]; 249 | let z = a[2]; 250 | let w = a[3]; 251 | return Math.sqrt(x * x + y * y + z * z + w * w); 252 | } 253 | 254 | /** 255 | * Calculates the squared length of a vec4 256 | * 257 | * @param {vec4} a vector to calculate squared length of 258 | * @returns {Number} squared length of a 259 | */ 260 | export function squaredLength(a) { 261 | let x = a[0]; 262 | let y = a[1]; 263 | let z = a[2]; 264 | let w = a[3]; 265 | return x * x + y * y + z * z + w * w; 266 | } 267 | 268 | /** 269 | * Negates the components of a vec4 270 | * 271 | * @param {vec4} out the receiving vector 272 | * @param {vec4} a vector to negate 273 | * @returns {vec4} out 274 | */ 275 | export function negate(out, a) { 276 | out[0] = -a[0]; 277 | out[1] = -a[1]; 278 | out[2] = -a[2]; 279 | out[3] = -a[3]; 280 | return out; 281 | } 282 | 283 | /** 284 | * Returns the inverse of the components of a vec4 285 | * 286 | * @param {vec4} out the receiving vector 287 | * @param {vec4} a vector to invert 288 | * @returns {vec4} out 289 | */ 290 | export function inverse(out, a) { 291 | out[0] = 1.0 / a[0]; 292 | out[1] = 1.0 / a[1]; 293 | out[2] = 1.0 / a[2]; 294 | out[3] = 1.0 / a[3]; 295 | return out; 296 | } 297 | 298 | /** 299 | * Normalize a vec4 300 | * 301 | * @param {vec4} out the receiving vector 302 | * @param {vec4} a vector to normalize 303 | * @returns {vec4} out 304 | */ 305 | export function normalize(out, a) { 306 | let x = a[0]; 307 | let y = a[1]; 308 | let z = a[2]; 309 | let w = a[3]; 310 | let len = x * x + y * y + z * z + w * w; 311 | if (len > 0) { 312 | len = 1 / Math.sqrt(len); 313 | out[0] = x * len; 314 | out[1] = y * len; 315 | out[2] = z * len; 316 | out[3] = w * len; 317 | } 318 | return out; 319 | } 320 | 321 | /** 322 | * Calculates the dot product of two vec4's 323 | * 324 | * @param {vec4} a the first operand 325 | * @param {vec4} b the second operand 326 | * @returns {Number} dot product of a and b 327 | */ 328 | export function dot(a, b) { 329 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; 330 | } 331 | 332 | /** 333 | * Performs a linear interpolation between two vec4's 334 | * 335 | * @param {vec4} out the receiving vector 336 | * @param {vec4} a the first operand 337 | * @param {vec4} b the second operand 338 | * @param {Number} t interpolation amount between the two inputs 339 | * @returns {vec4} out 340 | */ 341 | export function lerp(out, a, b, t) { 342 | let ax = a[0]; 343 | let ay = a[1]; 344 | let az = a[2]; 345 | let aw = a[3]; 346 | out[0] = ax + t * (b[0] - ax); 347 | out[1] = ay + t * (b[1] - ay); 348 | out[2] = az + t * (b[2] - az); 349 | out[3] = aw + t * (b[3] - aw); 350 | return out; 351 | } 352 | 353 | /** 354 | * Transforms the vec4 with a mat4. 355 | * 356 | * @param {vec4} out the receiving vector 357 | * @param {vec4} a the vector to transform 358 | * @param {mat4} m matrix to transform with 359 | * @returns {vec4} out 360 | */ 361 | export function transformMat4(out, a, m) { 362 | let x = a[0], y = a[1], z = a[2], w = a[3]; 363 | out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; 364 | out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; 365 | out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; 366 | out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; 367 | return out; 368 | } 369 | 370 | /** 371 | * Transforms the vec4 with a quat 372 | * 373 | * @param {vec4} out the receiving vector 374 | * @param {vec4} a the vector to transform 375 | * @param {quat} q quaternion to transform with 376 | * @returns {vec4} out 377 | */ 378 | export function transformQuat(out, a, q) { 379 | let x = a[0], y = a[1], z = a[2]; 380 | let qx = q[0], qy = q[1], qz = q[2], qw = q[3]; 381 | 382 | // calculate quat * vec 383 | let ix = qw * x + qy * z - qz * y; 384 | let iy = qw * y + qz * x - qx * z; 385 | let iz = qw * z + qx * y - qy * x; 386 | let iw = -qx * x - qy * y - qz * z; 387 | 388 | // calculate result * inverse quat 389 | out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; 390 | out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; 391 | out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; 392 | out[3] = a[3]; 393 | return out; 394 | } 395 | 396 | /** 397 | * Returns a string representation of a vector 398 | * 399 | * @param {vec4} a vector to represent as a string 400 | * @returns {String} string representation of the vector 401 | */ 402 | export function str(a) { 403 | return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; 404 | } 405 | 406 | /** 407 | * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===) 408 | * 409 | * @param {vec4} a The first vector. 410 | * @param {vec4} b The second vector. 411 | * @returns {Boolean} True if the vectors are equal, false otherwise. 412 | */ 413 | export function exactEquals(a, b) { 414 | return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; 415 | } 416 | 417 | /** 418 | * Returns whether or not the vectors have approximately the same elements in the same position. 419 | * 420 | * @param {vec4} a The first vector. 421 | * @param {vec4} b The second vector. 422 | * @returns {Boolean} True if the vectors are equal, false otherwise. 423 | */ 424 | export function equals(a, b) { 425 | let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; 426 | let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; 427 | return (Math.abs(a0 - b0) <= EPSILON * Math.max(1.0, Math.abs(a0), Math.abs(b0)) && 428 | Math.abs(a1 - b1) <= EPSILON * Math.max(1.0, Math.abs(a1), Math.abs(b1)) && 429 | Math.abs(a2 - b2) <= EPSILON * Math.max(1.0, Math.abs(a2), Math.abs(b2)) && 430 | Math.abs(a3 - b3) <= EPSILON * Math.max(1.0, Math.abs(a3), Math.abs(b3))); 431 | } 432 | -------------------------------------------------------------------------------- /ogl/extras/Orbit.js: -------------------------------------------------------------------------------- 1 | // Based from ThreeJS' OrbitControls class, rewritten using es6 with some additions and subtractions. 2 | // TODO: abstract event handlers so can be fed from other sources 3 | // TODO: make scroll zoom more accurate than just >/< zero 4 | 5 | import {Vec3} from '../math/Vec3.js'; 6 | import {Vec2} from '../math/Vec2.js'; 7 | 8 | const STATE = {NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, DOLLY_PAN: 3}; 9 | const tempVec3 = new Vec3(); 10 | const tempVec2a = new Vec2(); 11 | const tempVec2b = new Vec2(); 12 | 13 | export function Orbit(object, { 14 | element = document, 15 | enabled = true, 16 | target = new Vec3(), 17 | ease = 0.25, 18 | inertia = 0.85, 19 | enableRotate = true, 20 | rotateSpeed = 0.1, 21 | enableZoom = true, 22 | zoomSpeed = 1, 23 | enablePan = true, 24 | panSpeed = 0.1, 25 | minPolarAngle = 0, 26 | maxPolarAngle = Math.PI, 27 | minAzimuthAngle = -Infinity, 28 | maxAzimuthAngle = Infinity, 29 | minDistance = 0, 30 | maxDistance = Infinity, 31 | } = {}) { 32 | this.enabled = enabled; 33 | this.target = target; 34 | 35 | // Catch attempts to disable - set to 1 so has no effect 36 | ease = ease || 1; 37 | inertia = inertia || 1; 38 | 39 | this.minDistance = minDistance; 40 | this.maxDistance = maxDistance; 41 | 42 | // current position in sphericalTarget coordinates 43 | const sphericalDelta = {radius: 1, phi: 0, theta: 0}; 44 | const sphericalTarget = {radius: 1, phi: 0, theta: 0}; 45 | const spherical = {radius: 1, phi: 0, theta: 0}; 46 | const panDelta = new Vec3(); 47 | 48 | // Grab initial position values 49 | const offset = new Vec3(); 50 | offset.copy(object.position).subtract(this.target); 51 | spherical.radius = sphericalTarget.radius = offset.length(); 52 | spherical.theta = sphericalTarget.theta = Math.atan2(offset.x, offset.z); 53 | spherical.phi = sphericalTarget.phi = Math.acos(Math.min(Math.max(offset.y / sphericalTarget.radius, -1), 1)); 54 | 55 | this.update = () => { 56 | 57 | // apply delta 58 | sphericalTarget.radius *= sphericalDelta.radius; 59 | sphericalTarget.theta += sphericalDelta.theta; 60 | sphericalTarget.phi += sphericalDelta.phi; 61 | 62 | // apply boundaries 63 | sphericalTarget.theta = Math.max(minAzimuthAngle, Math.min(maxAzimuthAngle, sphericalTarget.theta)); 64 | sphericalTarget.phi = Math.max(minPolarAngle, Math.min(maxPolarAngle, sphericalTarget.phi)); 65 | sphericalTarget.radius = Math.max(this.minDistance, Math.min(this.maxDistance, sphericalTarget.radius)); 66 | 67 | // ease values 68 | spherical.phi += (sphericalTarget.phi - spherical.phi) * ease; 69 | spherical.theta += (sphericalTarget.theta - spherical.theta) * ease; 70 | spherical.radius += (sphericalTarget.radius - spherical.radius) * ease; 71 | 72 | // apply pan to target. As offset is relative to target, it also shifts 73 | this.target.add(panDelta); 74 | 75 | // apply rotation to offset 76 | let sinPhiRadius = spherical.radius * Math.sin(Math.max(0.000001, spherical.phi)); 77 | offset.x = sinPhiRadius * Math.sin(spherical.theta); 78 | offset.y = spherical.radius * Math.cos(spherical.phi); 79 | offset.z = sinPhiRadius * Math.cos(spherical.theta); 80 | 81 | // Apply updated values to object 82 | object.position.copy(this.target).add(offset); 83 | object.lookAt(this.target); 84 | 85 | // Apply inertia to values 86 | sphericalDelta.theta *= inertia; 87 | sphericalDelta.phi *= inertia; 88 | panDelta.multiply(inertia); 89 | 90 | // Reset scale every frame to avoid applying scale multiple times 91 | sphericalDelta.radius = 1; 92 | }; 93 | 94 | 95 | 96 | // Everything below here just updates panDelta and sphericalDelta 97 | // Using those two objects' values, the orbit is calculated 98 | 99 | 100 | 101 | const rotateStart = new Vec2(); 102 | const panStart = new Vec2(); 103 | const dollyStart = new Vec2(); 104 | 105 | let state = STATE.NONE; 106 | this.mouseButtons = {ORBIT: 0, ZOOM: 1, PAN: 2}; 107 | 108 | function getZoomScale() { 109 | return Math.pow(0.95, zoomSpeed); 110 | } 111 | 112 | function panLeft(distance, m) { 113 | tempVec3.set(m[0], m[1], m[2]); 114 | tempVec3.multiply(-distance); 115 | panDelta.add(tempVec3); 116 | } 117 | 118 | function panUp(distance, m) { 119 | tempVec3.set(m[4], m[5], m[6]); 120 | tempVec3.multiply(distance); 121 | panDelta.add(tempVec3); 122 | } 123 | 124 | const pan = (deltaX, deltaY) => { 125 | let el = element === document ? document.body : element; 126 | tempVec3.copy(object.position).subtract(this.target); 127 | let targetDistance = tempVec3.length(); 128 | targetDistance *= Math.tan(((object.fov || 45) / 2) * Math.PI / 180.0); 129 | panLeft(2 * deltaX * targetDistance / el.clientHeight, object.matrix); 130 | panUp(2 * deltaY * targetDistance / el.clientHeight, object.matrix); 131 | }; 132 | 133 | function dolly(dollyScale) { 134 | sphericalDelta.radius /= dollyScale; 135 | } 136 | 137 | function handleMoveRotate(x, y) { 138 | tempVec2a.set(x, y); 139 | tempVec2b.subtract(tempVec2a, rotateStart).multiply(rotateSpeed); 140 | let el = element === document ? document.body : element; 141 | sphericalDelta.theta -= 2 * Math.PI * tempVec2b.x / el.clientHeight; 142 | sphericalDelta.phi -= 2 * Math.PI * tempVec2b.y / el.clientHeight; 143 | rotateStart.copy(tempVec2a); 144 | } 145 | 146 | function handleMouseMoveDolly(e) { 147 | tempVec2a.set(e.clientX, e.clientY); 148 | tempVec2b.subtract(tempVec2a, dollyStart); 149 | if (tempVec2b.y > 0) { 150 | dolly(getZoomScale()); 151 | } else if (tempVec2b.y < 0) { 152 | dolly(1 / getZoomScale()); 153 | } 154 | dollyStart.copy(tempVec2a); 155 | } 156 | 157 | function handleMovePan(x, y) { 158 | tempVec2a.set(x, y); 159 | tempVec2b.subtract(tempVec2a, panStart).multiply(panSpeed); 160 | pan(tempVec2b.x, tempVec2b.y); 161 | panStart.copy(tempVec2a); 162 | } 163 | 164 | function handleTouchStartDollyPan(e) { 165 | if (enableZoom) { 166 | let dx = e.touches[0].pageX - e.touches[1].pageX; 167 | let dy = e.touches[0].pageY - e.touches[1].pageY; 168 | let distance = Math.sqrt(dx * dx + dy * dy); 169 | dollyStart.set(0, distance); 170 | } 171 | 172 | if (enablePan) { 173 | let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX); 174 | let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY); 175 | panStart.set(x, y); 176 | } 177 | } 178 | 179 | function handleTouchMoveDollyPan(e) { 180 | if (enableZoom) { 181 | let dx = e.touches[0].pageX - e.touches[1].pageX; 182 | let dy = e.touches[0].pageY - e.touches[1].pageY; 183 | let distance = Math.sqrt(dx * dx + dy * dy); 184 | tempVec2a.set(0, distance); 185 | tempVec2b.set(0, Math.pow(tempVec2a.y / dollyStart.y, zoomSpeed)); 186 | dolly(tempVec2b.y); 187 | dollyStart.copy(tempVec2a); 188 | } 189 | 190 | if (enablePan) { 191 | let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX); 192 | let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY); 193 | handleMovePan(x, y); 194 | } 195 | } 196 | 197 | const onMouseDown = (e) => { 198 | if (!this.enabled) return; 199 | 200 | switch (e.button) { 201 | case this.mouseButtons.ORBIT: 202 | if (enableRotate === false) return; 203 | rotateStart.set(e.clientX, e.clientY); 204 | state = STATE.ROTATE; 205 | break; 206 | case this.mouseButtons.ZOOM: 207 | if (enableZoom === false) return; 208 | dollyStart.set(e.clientX, e.clientY); 209 | state = STATE.DOLLY; 210 | break; 211 | case this.mouseButtons.PAN: 212 | if (enablePan === false) return; 213 | panStart.set(e.clientX, e.clientY); 214 | state = STATE.PAN; 215 | break; 216 | } 217 | 218 | if (state !== STATE.NONE) { 219 | window.addEventListener('mousemove', onMouseMove, false); 220 | window.addEventListener('mouseup', onMouseUp, false); 221 | } 222 | }; 223 | 224 | const onMouseMove = (e) => { 225 | if (!this.enabled) return; 226 | 227 | switch (state) { 228 | case STATE.ROTATE: 229 | if (enableRotate === false) return; 230 | handleMoveRotate(e.clientX, e.clientY); 231 | break; 232 | case STATE.DOLLY: 233 | if (enableZoom === false) return; 234 | handleMouseMoveDolly(e); 235 | break; 236 | case STATE.PAN: 237 | if (enablePan === false) return; 238 | handleMovePan(e.clientX, e.clientY); 239 | break; 240 | } 241 | }; 242 | 243 | const onMouseUp = () => { 244 | if (!this.enabled) return; 245 | document.removeEventListener('mousemove', onMouseMove, false); 246 | document.removeEventListener('mouseup', onMouseUp, false); 247 | state = STATE.NONE; 248 | }; 249 | 250 | const onMouseWheel = (e) => { 251 | if (!this.enabled || !enableZoom || (state !== STATE.NONE && state !== STATE.ROTATE)) return; 252 | e.preventDefault(); 253 | e.stopPropagation(); 254 | 255 | if (e.deltaY < 0) { 256 | dolly(1 / getZoomScale()); 257 | } else if (e.deltaY > 0) { 258 | dolly(getZoomScale()); 259 | } 260 | }; 261 | 262 | const onTouchStart = (e) => { 263 | if (!this.enabled) return; 264 | e.preventDefault(); 265 | 266 | switch (e.touches.length) { 267 | case 1: 268 | if (enableRotate === false) return; 269 | rotateStart.set(e.touches[0].pageX, e.touches[0].pageY); 270 | state = STATE.ROTATE; 271 | break; 272 | case 2: 273 | if (enableZoom === false && enablePan === false) return; 274 | handleTouchStartDollyPan(e); 275 | state = STATE.DOLLY_PAN; 276 | break; 277 | default: 278 | state = STATE.NONE; 279 | } 280 | }; 281 | 282 | const onTouchMove = (e) => { 283 | if (!this.enabled) return; 284 | e.preventDefault(); 285 | e.stopPropagation(); 286 | 287 | switch (e.touches.length) { 288 | case 1: 289 | if (enableRotate === false) return; 290 | handleMoveRotate(e.touches[0].pageX, e.touches[0].pageY); 291 | break; 292 | case 2: 293 | if (enableZoom === false && enablePan === false) return; 294 | handleTouchMoveDollyPan(e); 295 | break; 296 | default: 297 | state = STATE.NONE; 298 | } 299 | }; 300 | 301 | const onTouchEnd = () => { 302 | if (!this.enabled) return; 303 | state = STATE.NONE; 304 | }; 305 | 306 | const onContextMenu = (e) => { 307 | if (!this.enabled) return; 308 | e.preventDefault(); 309 | }; 310 | 311 | function addHandlers() { 312 | element.addEventListener('contextmenu', onContextMenu, false); 313 | element.addEventListener('mousedown', onMouseDown, false); 314 | window.addEventListener('wheel', onMouseWheel, false); 315 | element.addEventListener('touchstart', onTouchStart, {passive: false}); 316 | element.addEventListener('touchend', onTouchEnd, false); 317 | element.addEventListener('touchmove', onTouchMove, {passive: false}); 318 | } 319 | 320 | this.remove = function() { 321 | element.removeEventListener('contextmenu', onContextMenu, false); 322 | element.removeEventListener('mousedown', onMouseDown, false); 323 | window.removeEventListener('wheel', onMouseWheel, false); 324 | element.removeEventListener('touchstart', onTouchStart, false); 325 | element.removeEventListener('touchend', onTouchEnd, false); 326 | element.removeEventListener('touchmove', onTouchMove, false);`` 327 | window.removeEventListener('mousemove', onMouseMove, false); 328 | window.removeEventListener('mouseup', onMouseUp, false); 329 | }; 330 | 331 | addHandlers(); 332 | } -------------------------------------------------------------------------------- /ogl/math/functions/QuatFunc.js: -------------------------------------------------------------------------------- 1 | import * as mat3 from './Mat3Func.js'; 2 | import * as vec3 from './Vec3Func.js'; 3 | import * as vec4 from './Vec4Func.js'; 4 | 5 | /** 6 | * Set a quat to the identity quaternion 7 | * 8 | * @param {quat} out the receiving quaternion 9 | * @returns {quat} out 10 | */ 11 | export function identity(out) { 12 | out[0] = 0; 13 | out[1] = 0; 14 | out[2] = 0; 15 | out[3] = 1; 16 | return out; 17 | } 18 | 19 | /** 20 | * Sets a quat from the given angle and rotation axis, 21 | * then returns it. 22 | * 23 | * @param {quat} out the receiving quaternion 24 | * @param {vec3} axis the axis around which to rotate 25 | * @param {Number} rad the angle in radians 26 | * @returns {quat} out 27 | **/ 28 | export function setAxisAngle(out, axis, rad) { 29 | rad = rad * 0.5; 30 | let s = Math.sin(rad); 31 | out[0] = s * axis[0]; 32 | out[1] = s * axis[1]; 33 | out[2] = s * axis[2]; 34 | out[3] = Math.cos(rad); 35 | return out; 36 | } 37 | 38 | /** 39 | * Gets the rotation axis and angle for a given 40 | * quaternion. If a quaternion is created with 41 | * setAxisAngle, this method will return the same 42 | * values as providied in the original parameter list 43 | * OR functionally equivalent values. 44 | * Example: The quaternion formed by axis [0, 0, 1] and 45 | * angle -90 is the same as the quaternion formed by 46 | * [0, 0, 1] and 270. This method favors the latter. 47 | * @param {vec3} out_axis Vector receiving the axis of rotation 48 | * @param {quat} q Quaternion to be decomposed 49 | * @return {Number} Angle, in radians, of the rotation 50 | */ 51 | export function getAxisAngle(out_axis, q) { 52 | let rad = Math.acos(q[3]) * 2.0; 53 | let s = Math.sin(rad / 2.0); 54 | if (s != 0.0) { 55 | out_axis[0] = q[0] / s; 56 | out_axis[1] = q[1] / s; 57 | out_axis[2] = q[2] / s; 58 | } else { 59 | // If s is zero, return any axis (no rotation - axis does not matter) 60 | out_axis[0] = 1; 61 | out_axis[1] = 0; 62 | out_axis[2] = 0; 63 | } 64 | return rad; 65 | } 66 | 67 | /** 68 | * Multiplies two quats 69 | * 70 | * @param {quat} out the receiving quaternion 71 | * @param {quat} a the first operand 72 | * @param {quat} b the second operand 73 | * @returns {quat} out 74 | */ 75 | export function multiply(out, a, b) { 76 | let ax = a[0], ay = a[1], az = a[2], aw = a[3]; 77 | let bx = b[0], by = b[1], bz = b[2], bw = b[3]; 78 | 79 | out[0] = ax * bw + aw * bx + ay * bz - az * by; 80 | out[1] = ay * bw + aw * by + az * bx - ax * bz; 81 | out[2] = az * bw + aw * bz + ax * by - ay * bx; 82 | out[3] = aw * bw - ax * bx - ay * by - az * bz; 83 | return out; 84 | } 85 | 86 | /** 87 | * Rotates a quaternion by the given angle about the X axis 88 | * 89 | * @param {quat} out quat receiving operation result 90 | * @param {quat} a quat to rotate 91 | * @param {number} rad angle (in radians) to rotate 92 | * @returns {quat} out 93 | */ 94 | export function rotateX(out, a, rad) { 95 | rad *= 0.5; 96 | 97 | let ax = a[0], ay = a[1], az = a[2], aw = a[3]; 98 | let bx = Math.sin(rad), bw = Math.cos(rad); 99 | 100 | out[0] = ax * bw + aw * bx; 101 | out[1] = ay * bw + az * bx; 102 | out[2] = az * bw - ay * bx; 103 | out[3] = aw * bw - ax * bx; 104 | return out; 105 | } 106 | 107 | /** 108 | * Rotates a quaternion by the given angle about the Y axis 109 | * 110 | * @param {quat} out quat receiving operation result 111 | * @param {quat} a quat to rotate 112 | * @param {number} rad angle (in radians) to rotate 113 | * @returns {quat} out 114 | */ 115 | export function rotateY(out, a, rad) { 116 | rad *= 0.5; 117 | 118 | let ax = a[0], ay = a[1], az = a[2], aw = a[3]; 119 | let by = Math.sin(rad), bw = Math.cos(rad); 120 | 121 | out[0] = ax * bw - az * by; 122 | out[1] = ay * bw + aw * by; 123 | out[2] = az * bw + ax * by; 124 | out[3] = aw * bw - ay * by; 125 | return out; 126 | } 127 | 128 | /** 129 | * Rotates a quaternion by the given angle about the Z axis 130 | * 131 | * @param {quat} out quat receiving operation result 132 | * @param {quat} a quat to rotate 133 | * @param {number} rad angle (in radians) to rotate 134 | * @returns {quat} out 135 | */ 136 | export function rotateZ(out, a, rad) { 137 | rad *= 0.5; 138 | 139 | let ax = a[0], ay = a[1], az = a[2], aw = a[3]; 140 | let bz = Math.sin(rad), bw = Math.cos(rad); 141 | 142 | out[0] = ax * bw + ay * bz; 143 | out[1] = ay * bw - ax * bz; 144 | out[2] = az * bw + aw * bz; 145 | out[3] = aw * bw - az * bz; 146 | return out; 147 | } 148 | 149 | /** 150 | * Calculates the W component of a quat from the X, Y, and Z components. 151 | * Assumes that quaternion is 1 unit in length. 152 | * Any existing W component will be ignored. 153 | * 154 | * @param {quat} out the receiving quaternion 155 | * @param {quat} a quat to calculate W component of 156 | * @returns {quat} out 157 | */ 158 | export function calculateW(out, a) { 159 | let x = a[0], y = a[1], z = a[2]; 160 | 161 | out[0] = x; 162 | out[1] = y; 163 | out[2] = z; 164 | out[3] = Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); 165 | return out; 166 | } 167 | 168 | /** 169 | * Performs a spherical linear interpolation between two quat 170 | * 171 | * @param {quat} out the receiving quaternion 172 | * @param {quat} a the first operand 173 | * @param {quat} b the second operand 174 | * @param {Number} t interpolation amount between the two inputs 175 | * @returns {quat} out 176 | */ 177 | export function slerp(out, a, b, t) { 178 | // benchmarks: 179 | // http://jsperf.com/quaternion-slerp-implementations 180 | let ax = a[0], ay = a[1], az = a[2], aw = a[3]; 181 | let bx = b[0], by = b[1], bz = b[2], bw = b[3]; 182 | 183 | let omega, cosom, sinom, scale0, scale1; 184 | 185 | // calc cosine 186 | cosom = ax * bx + ay * by + az * bz + aw * bw; 187 | // adjust signs (if necessary) 188 | if (cosom < 0.0) { 189 | cosom = -cosom; 190 | bx = -bx; 191 | by = -by; 192 | bz = -bz; 193 | bw = -bw; 194 | } 195 | // calculate coefficients 196 | if ((1.0 - cosom) > 0.000001) { 197 | // standard case (slerp) 198 | omega = Math.acos(cosom); 199 | sinom = Math.sin(omega); 200 | scale0 = Math.sin((1.0 - t) * omega) / sinom; 201 | scale1 = Math.sin(t * omega) / sinom; 202 | } else { 203 | // "from" and "to" quaternions are very close 204 | // ... so we can do a linear interpolation 205 | scale0 = 1.0 - t; 206 | scale1 = t; 207 | } 208 | // calculate final values 209 | out[0] = scale0 * ax + scale1 * bx; 210 | out[1] = scale0 * ay + scale1 * by; 211 | out[2] = scale0 * az + scale1 * bz; 212 | out[3] = scale0 * aw + scale1 * bw; 213 | 214 | return out; 215 | } 216 | 217 | /** 218 | * Calculates the inverse of a quat 219 | * 220 | * @param {quat} out the receiving quaternion 221 | * @param {quat} a quat to calculate inverse of 222 | * @returns {quat} out 223 | */ 224 | export function invert(out, a) { 225 | let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; 226 | let dot = a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3; 227 | let invDot = dot ? 1.0 / dot : 0; 228 | 229 | // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0 230 | 231 | out[0] = -a0 * invDot; 232 | out[1] = -a1 * invDot; 233 | out[2] = -a2 * invDot; 234 | out[3] = a3 * invDot; 235 | return out; 236 | } 237 | 238 | /** 239 | * Calculates the conjugate of a quat 240 | * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result. 241 | * 242 | * @param {quat} out the receiving quaternion 243 | * @param {quat} a quat to calculate conjugate of 244 | * @returns {quat} out 245 | */ 246 | export function conjugate(out, a) { 247 | out[0] = -a[0]; 248 | out[1] = -a[1]; 249 | out[2] = -a[2]; 250 | out[3] = a[3]; 251 | return out; 252 | } 253 | 254 | /** 255 | * Creates a quaternion from the given 3x3 rotation matrix. 256 | * 257 | * NOTE: The resultant quaternion is not normalized, so you should be sure 258 | * to renormalize the quaternion yourself where necessary. 259 | * 260 | * @param {quat} out the receiving quaternion 261 | * @param {mat3} m rotation matrix 262 | * @returns {quat} out 263 | * @function 264 | */ 265 | export function fromMat3(out, m) { 266 | // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes 267 | // article "Quaternion Calculus and Fast Animation". 268 | let fTrace = m[0] + m[4] + m[8]; 269 | let fRoot; 270 | 271 | if (fTrace > 0.0) { 272 | // |w| > 1/2, may as well choose w > 1/2 273 | fRoot = Math.sqrt(fTrace + 1.0); // 2w 274 | out[3] = 0.5 * fRoot; 275 | fRoot = 0.5 / fRoot; // 1/(4w) 276 | out[0] = (m[5] - m[7]) * fRoot; 277 | out[1] = (m[6] - m[2]) * fRoot; 278 | out[2] = (m[1] - m[3]) * fRoot; 279 | } else { 280 | // |w| <= 1/2 281 | let i = 0; 282 | if (m[4] > m[0]) 283 | i = 1; 284 | if (m[8] > m[i * 3 + i]) 285 | i = 2; 286 | let j = (i + 1) % 3; 287 | let k = (i + 2) % 3; 288 | 289 | fRoot = Math.sqrt(m[i * 3 + i] - m[j * 3 + j] - m[k * 3 + k] + 1.0); 290 | out[i] = 0.5 * fRoot; 291 | fRoot = 0.5 / fRoot; 292 | out[3] = (m[j * 3 + k] - m[k * 3 + j]) * fRoot; 293 | out[j] = (m[j * 3 + i] + m[i * 3 + j]) * fRoot; 294 | out[k] = (m[k * 3 + i] + m[i * 3 + k]) * fRoot; 295 | } 296 | 297 | return out; 298 | } 299 | 300 | /** 301 | * Creates a quaternion from the given euler angle x, y, z. 302 | * 303 | * @param {quat} out the receiving quaternion 304 | * @param {vec3} euler Angles to rotate around each axis in degrees. 305 | * @param {String} order detailing order of operations. Default 'XYZ'. 306 | * @returns {quat} out 307 | * @function 308 | */ 309 | export function fromEuler(out, euler, order = 'YXZ') { 310 | let sx = Math.sin(euler[0] * 0.5); 311 | let cx = Math.cos(euler[0] * 0.5); 312 | let sy = Math.sin(euler[1] * 0.5); 313 | let cy = Math.cos(euler[1] * 0.5); 314 | let sz = Math.sin(euler[2] * 0.5); 315 | let cz = Math.cos(euler[2] * 0.5); 316 | 317 | if (order === 'XYZ') { 318 | out[0] = sx * cy * cz + cx * sy * sz; 319 | out[1] = cx * sy * cz - sx * cy * sz; 320 | out[2] = cx * cy * sz + sx * sy * cz; 321 | out[3] = cx * cy * cz - sx * sy * sz; 322 | } else if (order === 'YXZ') { 323 | out[0] = sx * cy * cz + cx * sy * sz; 324 | out[1] = cx * sy * cz - sx * cy * sz; 325 | out[2] = cx * cy * sz - sx * sy * cz; 326 | out[3] = cx * cy * cz + sx * sy * sz; 327 | } else if (order === 'ZXY') { 328 | out[0] = sx * cy * cz - cx * sy * sz; 329 | out[1] = cx * sy * cz + sx * cy * sz; 330 | out[2] = cx * cy * sz + sx * sy * cz; 331 | out[3] = cx * cy * cz - sx * sy * sz; 332 | } else if (order === 'ZYX') { 333 | out[0] = sx * cy * cz - cx * sy * sz; 334 | out[1] = cx * sy * cz + sx * cy * sz; 335 | out[2] = cx * cy * sz - sx * sy * cz; 336 | out[3] = cx * cy * cz + sx * sy * sz; 337 | } else if (order === 'YZX') { 338 | out[0] = sx * cy * cz + cx * sy * sz; 339 | out[1] = cx * sy * cz + sx * cy * sz; 340 | out[2] = cx * cy * sz - sx * sy * cz; 341 | out[3] = cx * cy * cz - sx * sy * sz; 342 | } else if (order === 'XZY') { 343 | out[0] = sx * cy * cz - cx * sy * sz; 344 | out[1] = cx * sy * cz - sx * cy * sz; 345 | out[2] = cx * cy * sz + sx * sy * cz; 346 | out[3] = cx * cy * cz + sx * sy * sz; 347 | } 348 | 349 | return out; 350 | } 351 | 352 | /** 353 | * Returns a string representation of a quatenion 354 | * 355 | * @param {quat} a vector to represent as a string 356 | * @returns {String} string representation of the vector 357 | */ 358 | export function str(a) { 359 | return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; 360 | } 361 | 362 | 363 | /** 364 | * Copy the values from one quat to another 365 | * 366 | * @param {quat} out the receiving quaternion 367 | * @param {quat} a the source quaternion 368 | * @returns {quat} out 369 | * @function 370 | */ 371 | export const copy = vec4.copy; 372 | 373 | /** 374 | * Set the components of a quat to the given values 375 | * 376 | * @param {quat} out the receiving quaternion 377 | * @param {Number} x X component 378 | * @param {Number} y Y component 379 | * @param {Number} z Z component 380 | * @param {Number} w W component 381 | * @returns {quat} out 382 | * @function 383 | */ 384 | export const set = vec4.set; 385 | 386 | /** 387 | * Adds two quat's 388 | * 389 | * @param {quat} out the receiving quaternion 390 | * @param {quat} a the first operand 391 | * @param {quat} b the second operand 392 | * @returns {quat} out 393 | * @function 394 | */ 395 | export const add = vec4.add; 396 | 397 | /** 398 | * Scales a quat by a scalar number 399 | * 400 | * @param {quat} out the receiving vector 401 | * @param {quat} a the vector to scale 402 | * @param {Number} b amount to scale the vector by 403 | * @returns {quat} out 404 | * @function 405 | */ 406 | export const scale = vec4.scale; 407 | 408 | /** 409 | * Calculates the dot product of two quat's 410 | * 411 | * @param {quat} a the first operand 412 | * @param {quat} b the second operand 413 | * @returns {Number} dot product of a and b 414 | * @function 415 | */ 416 | export const dot = vec4.dot; 417 | 418 | /** 419 | * Performs a linear interpolation between two quat's 420 | * 421 | * @param {quat} out the receiving quaternion 422 | * @param {quat} a the first operand 423 | * @param {quat} b the second operand 424 | * @param {Number} t interpolation amount between the two inputs 425 | * @returns {quat} out 426 | * @function 427 | */ 428 | export const lerp = vec4.lerp; 429 | 430 | /** 431 | * Calculates the length of a quat 432 | * 433 | * @param {quat} a vector to calculate length of 434 | * @returns {Number} length of a 435 | */ 436 | export const length = vec4.length; 437 | 438 | /** 439 | * Calculates the squared length of a quat 440 | * 441 | * @param {quat} a vector to calculate squared length of 442 | * @returns {Number} squared length of a 443 | * @function 444 | */ 445 | export const squaredLength = vec4.squaredLength; 446 | 447 | /** 448 | * Normalize a quat 449 | * 450 | * @param {quat} out the receiving quaternion 451 | * @param {quat} a quaternion to normalize 452 | * @returns {quat} out 453 | * @function 454 | */ 455 | export const normalize = vec4.normalize; 456 | 457 | /** 458 | * Returns whether or not the quaternions have exactly the same elements in the same position (when compared with ===) 459 | * 460 | * @param {quat} a The first quaternion. 461 | * @param {quat} b The second quaternion. 462 | * @returns {Boolean} True if the vectors are equal, false otherwise. 463 | */ 464 | export const exactEquals = vec4.exactEquals; 465 | 466 | /** 467 | * Returns whether or not the quaternions have approximately the same elements in the same position. 468 | * 469 | * @param {quat} a The first vector. 470 | * @param {quat} b The second vector. 471 | * @returns {Boolean} True if the vectors are equal, false otherwise. 472 | */ 473 | export const equals = vec4.equals; 474 | 475 | /** 476 | * Sets a quaternion to represent the shortest rotation from one 477 | * vector to another. 478 | * 479 | * Both vectors are assumed to be unit length. 480 | * 481 | * @param {quat} out the receiving quaternion. 482 | * @param {vec3} a the initial vector 483 | * @param {vec3} b the destination vector 484 | * @returns {quat} out 485 | */ 486 | export const rotationTo = (function () { 487 | let tmpvec3 = new Float32Array([0, 0, 0]); 488 | let xUnitVec3 = new Float32Array([1, 0, 0]); 489 | let yUnitVec3 = new Float32Array([0, 1, 0]); 490 | 491 | return function (out, a, b) { 492 | let dot = vec3.dot(a, b); 493 | if (dot < -0.999999) { 494 | vec3.cross(tmpvec3, xUnitVec3, a); 495 | if (vec3.length(tmpvec3) < 0.000001) 496 | vec3.cross(tmpvec3, yUnitVec3, a); 497 | vec3.normalize(tmpvec3, tmpvec3); 498 | setAxisAngle(out, tmpvec3, Math.PI); 499 | return out; 500 | } else if (dot > 0.999999) { 501 | out[0] = 0; 502 | out[1] = 0; 503 | out[2] = 0; 504 | out[3] = 1; 505 | return out; 506 | } else { 507 | vec3.cross(tmpvec3, a, b); 508 | out[0] = tmpvec3[0]; 509 | out[1] = tmpvec3[1]; 510 | out[2] = tmpvec3[2]; 511 | out[3] = 1 + dot; 512 | return normalize(out, out); 513 | } 514 | }; 515 | })(); 516 | 517 | /** 518 | * Performs a spherical linear interpolation with two control points 519 | * 520 | * @param {quat} out the receiving quaternion 521 | * @param {quat} a the first operand 522 | * @param {quat} b the second operand 523 | * @param {quat} c the third operand 524 | * @param {quat} d the fourth operand 525 | * @param {Number} t interpolation amount 526 | * @returns {quat} out 527 | */ 528 | export const sqlerp = (function () { 529 | let temp1 = identity(new Float32Array(4)); 530 | let temp2 = identity(new Float32Array(4)); 531 | 532 | return function (out, a, b, c, d, t) { 533 | slerp(temp1, a, d, t); 534 | slerp(temp2, b, c, t); 535 | slerp(out, temp1, temp2, 2 * t * (1 - t)); 536 | 537 | return out; 538 | }; 539 | }()); 540 | 541 | /** 542 | * Sets the specified quaternion with values corresponding to the given 543 | * axes. Each axis is a vec3 and is expected to be unit length and 544 | * perpendicular to all other specified axes. 545 | * 546 | * @param {vec3} view the vector representing the viewing direction 547 | * @param {vec3} right the vector representing the local "right" direction 548 | * @param {vec3} up the vector representing the local "up" direction 549 | * @returns {quat} out 550 | */ 551 | export const setAxes = (function () { 552 | let matr = mat3.identity(new Float32Array(9)); 553 | 554 | return function (out, view, right, up) { 555 | matr[0] = right[0]; 556 | matr[3] = right[1]; 557 | matr[6] = right[2]; 558 | 559 | matr[1] = up[0]; 560 | matr[4] = up[1]; 561 | matr[7] = up[2]; 562 | 563 | matr[2] = -view[0]; 564 | matr[5] = -view[1]; 565 | matr[8] = -view[2]; 566 | 567 | return normalize(out, fromMat3(out, matr)); 568 | }; 569 | })(); 570 | --------------------------------------------------------------------------------