├── .gitignore ├── 3rdparty ├── LICENSE └── interact.min.js ├── LICENSE ├── README.md ├── embed.html ├── gausssplatrenderer.d.ts ├── gausssplatrenderer.js ├── img ├── controls-desktop.svg ├── controls-mobile.svg ├── logo.svg └── rotating.svg ├── index.html ├── lib ├── bitonic.js ├── kd-tree.js ├── kernels │ ├── bitonic.js │ ├── depth.js │ ├── endian.js │ ├── get-channel.js │ ├── ordering.js │ ├── permute.js │ ├── shader.js │ ├── to-array.js │ └── to-texture.js ├── package.json ├── pipeline.js ├── pointarray.js ├── quickselect.js ├── rendering │ ├── sphere.js │ └── vpshaders.js ├── splatfile.js └── utils │ ├── linalg.js │ ├── rotors.js │ └── view.js ├── package.json ├── styles.css ├── test ├── assets │ └── test.splat ├── browser │ └── kernel-test.html ├── kd-tree.test.js ├── package.json ├── quickselect.test.js └── splatfile.test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.splat 4 | 5 | -------------------------------------------------------------------------------- /3rdparty/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-present Taye Adeyemi 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to 9 | whom the Software is furnished to do so, subject to the 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall 13 | be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 17 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 19 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Portality Splat Viewer 2 | 3 | This is a WebGL2 implementation of a browser-based renderer for Gaussian Splats, see [3D Gaussian Splatting for Real-Time Radiance Field Rendering](https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/). Unlike some previous renderers, this renderer uses a gpu-based bitonic sort algorithm that makes it much faster and eliminates jerky animations. Also, it is fully WebGL 2 compatible, making it available across a wide variety of browsers, including mobile browsers. 4 | 5 | This can be hosted locally using a webserver pointing to this folder or it can be accessed at [https://viewer.portality.ai](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Fnike.splat%3Fdownload%3Dtrue&camera=-1.4896482414801953%2C3.390495575032005%2C-0.7877998849693162&lookAt=-0.39199591430028635%2C1.6863556266639323%2C1.3527185719665666&up=-0.02041277475655079%2C-0.7872545123100281%2C-0.6162927746772766) 6 | 7 | Coming soon on our product website at [https://portality.ai](https://portality.ai) is a way to learn these splats easily with your data, and much more. Join our discord server: [Panverse Robotics](https://discord.gg/kmFUXWw5Um), or contact us at the email: portality at panverserobotics.com or here on github for feedback and suggestions. 8 | 9 | ## Controls 10 | 11 | The controls are based on the position of the camera and focus point (where the camera is looking at). 12 | 13 | Left mouse + drag: Orbit camera around the focus point. 14 | Right mouse + drag: Pan camera by moving focus point around camera's origin. 15 | Mouse wheel: Dolly camera closer/farther from the focus point. 16 | 17 | ## Quality Configuration 18 | 19 | Our implementation has two different gpu-based sorting methods, which can be controlled by a parameter on the url. The default is a global bitonic sort which introduces minimal artifacts but can be slightly slower than a tiled bitonic sort that uses kd-trees internally, which introduces artifacts along the tiling boundaries. Expect about a ~2x difference in fps between the sort configurations from our testing. 20 | 21 | To switch between them use the query parameter like so: 22 | 23 | quality=high, default 24 | ``` 25 | https://viewer.portality.ai/index.html?quality=high 26 | ``` 27 | 28 | and 29 | 30 | quality=fast 31 | ``` 32 | https://viewer.portality.ai/index.html?quality=fast 33 | ``` 34 | 35 | 36 | ## Setting default camera params 37 | 38 | The camera parameters on startup, such as camera position, the location where the camera is looking, and the up vector, can be specified as parameters, e.g. 39 | 40 | ``` 41 | https://viewer.portality.ai/index.html?camera=5.0,0.0,0.0&lookAt=0.1,0.0,0.08&up=0.0,-1.0,0.0 42 | ``` 43 | 44 | ## Examples 45 | 46 | The splat format is compatible with the .splat format introduced by https://github.com/antimatter15/splat. 47 | 48 | To load from a URL, specify the link as a `url` argument, e.g. https://viewer.portality.ai/index.html?url=... 49 | 50 | ## Selected Demo Scenes 51 | Small- and medium-sized scenes are suited to mobile viewing 52 | 53 | - [Nike (Small)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Fnike.splat%3Fdownload%3Dtrue&camera=-1.4896482414801953%2C3.390495575032005%2C-0.7877998849693162&lookAt=-0.39199591430028635%2C1.6863556266639323%2C1.3527185719665666&up=-0.02041277475655079%2C-0.7872545123100281%2C-0.6162927746772766) 54 | - [Plush (Small)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Fplush.splat%3Fdownload%3Dtrue&camera=0.5565675836106072%2C2.667193492722922%2C-1.9308635603699218&lookAt=0.4023664948021303%2C1.8369706127766905%2C1.2489360953059658&up=-0.17141437530517578%2C-0.9511821866035461%2C-0.2566594183444977) 55 | - [Chess (Small)](https://viewer.portality.ai/?url=https%3A%2F%2Fd3c617x64bvo7w.cloudfront.net%2Fchess.splat&camera=1.0791208253019446%2C3.0959993382319606%2C-1.1840614891643058&lookAt=0.3718318514897351%2C0.48511337896742135%2C1.6929692567777628&up=-0.05236946791410446%2C-0.7330717444419861%2C-0.678132176399231) 56 | - [Train (Small)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Ftrain.splat%3Fdownload%3Dtrue&camera=-2.1270068327473646%2C0.16104565057569387%2C2.809400294222233&lookAt=-0.5589713026570335%2C0.0409163255285586%2C0.203083103813528&up=0.008334717713296413%2C-0.9986659288406372%2C0.05104399472475052) 57 | - [Truck (Medium)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Ftruck.splat%3Fdownload%3Dtrue&camera=0.8299992664605993%2C0.7349339491049119%2C-3.401283966294055&lookAt=-0.13387062965327384%2C0.3533649188647336%2C0.4972497243969419&up=0.03285346180200577%2C-0.9954638481140137%2C-0.08930841088294983) 58 | - [DomeGreenhouse (Medium)](https://viewer.portality.ai/?url=https%3A%2F%2Fd3c617x64bvo7w.cloudfront.net%2Fdomegreenhouse.splat&camera=-3.5806928884435965%2C0.8933851880858525%2C-1.6083174733309864&lookAt=0.8558180672183671%2C-0.05253012660170886%2C0.5930712310114736&up=0.26420512795448303%2C-0.570544958114624%2C-0.7776140570640564) 59 | - [Dumpster (Large)](https://viewer.portality.ai/?url=https%3A%2F%2Fd3c617x64bvo7w.cloudfront.net%2Fdumpster.splat&camera=-3.151375547361921%2C-2.016319888117259%2C3.0729341535122643&lookAt=0.9058803055535136%2C0.4402297148762391%2C1.490492734170797&up=0.16068656742572784%2C-0.7082292437553406%2C-0.6874526739120483) 60 | - [Bicycle (Large)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Fbicycle.splat%3Fdownload%3Dtrue&camera=-2.8849047698183004%2C0.9222252267609595%2C-0.7220571605742392&lookAt=0.521883814175611%2C0.8171271406869649%2C0.44293577341433193&up=0.04069226235151291%2C-0.9774585962295532%2C-0.20717640221118927) 61 | - [Garden (Large)](https://viewer.portality.ai/?url=https%3A%2F%2Fhuggingface.co%2Fcakewalk%2Fsplat-data%2Fresolve%2Fmain%2Fgarden.splat%3Fdownload%3Dtrue&camera=-2.053353162001457%2C2.286297111737985%2C-2.890513076551497&lookAt=0.02551149613996362%2C0.7813343402799947%2C1.3989496256153457&up=0.07344545423984528%2C-0.9294056296348572%2C-0.36167794466018677) 62 | 63 | 64 | 65 | ## 3rd party code 66 | 67 | This code uses [interactjs](https://github.com/taye/interact.js) for touch gestures. Attribution for interactjs goes to Taye Ademi. 68 | -------------------------------------------------------------------------------- /embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Portality Splat Viewer 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 |
18 | 19 | 20 | 21 | 41 | 42 | -------------------------------------------------------------------------------- /gausssplatrenderer.d.ts: -------------------------------------------------------------------------------- 1 | export function renderMain(canvas: any, data: any, cameraParams: any, pipelineType: any, interactMod?: any): (now: any) => void; 2 | export function readParams(params: any, canvas: any, interactMod?: any): void; 3 | export namespace cameraParams { 4 | let position: number[]; 5 | let lookAt: number[]; 6 | let up: number[]; 7 | } 8 | export let pipelineType: string; 9 | export function loadSplatData(data: any): ({ positions: Float32Array, scales: Float32Array, colors: Float32Array, rotors: Float32Array }); 10 | export function mat4multiply(out: Float32Array, a: Float32Array, b: Float32Array): void; 11 | export function viewMatGetPoseParams(viewMat: Float32Array, radius: number): ({ camera: Float32Array, lookAt: Float32Array, up: Float32Array }); 12 | -------------------------------------------------------------------------------- /gausssplatrenderer.js: -------------------------------------------------------------------------------- 1 | import './lib/utils/linalg.js'; 2 | import './lib/pipeline.js'; 3 | 4 | import { mat3transpose, mat3multiply, mat4multiply, mat4perspective, mat4ortho, mat4lookAt } from './lib/utils/linalg.js'; 5 | import { viewUpdate, viewAutoSpin, stopAutoSpin, initializeViewMatrix, viewMatGetLookAt, viewMatGetPoseParams } from './lib/utils/view.js'; 6 | import { rotorToRotationMatrix, rotorsToCov3D } from './lib/utils/rotors.js'; 7 | import { createPipeline, applyPipeline, createFullSortPipeline, applyFullSortPipeline, toTexture } from './lib/pipeline.js'; 8 | import { permuteArray } from './lib/pointarray.js'; 9 | import createRenderProgram from './lib/rendering/vpshaders.js'; 10 | import { createSphereRenderProgram, createSphereCircles, renderSphereCircles } from './lib/rendering/sphere.js'; 11 | import loadSplatData from './lib/splatfile.js'; 12 | 13 | let fpsData = { 14 | then: 0, 15 | frameTimes: [], 16 | frameCursor: 0, 17 | numFrames: 0, 18 | maxFrames: 20, 19 | totalFPS: 0 20 | }; 21 | 22 | let cameraParams = { 23 | position: [0, -1, 0], 24 | lookAt: [0, 0.25, 0], 25 | up: [0, 0, 1] 26 | }; 27 | 28 | let pipelineType = 'full'; 29 | 30 | 31 | 32 | const mouseControlMap = { 33 | // Left mouse button 34 | 1: { 35 | "ShiftLeft": "pan", 36 | "ShiftRight": "pan", 37 | "ControlLeft": "strafe", 38 | "ControlRight": "strafe", 39 | "AltLeft": "dollyRoll", 40 | "AltRight": "dollyRoll", 41 | "": "orbit" 42 | }, 43 | 44 | // Right mouse button 45 | 2: { 46 | "": "strafe" 47 | }, 48 | 49 | // Middle mouse button 50 | 4: { 51 | "ControlLeft": "changeRadius", 52 | "ControlRight": "changeRadius", 53 | "": "dollyRoll" 54 | } 55 | }; 56 | 57 | 58 | function getRadius(cameraParams) { 59 | let ep = cameraParams.position; 60 | let fp = cameraParams.lookAt; 61 | 62 | return Math.sqrt((ep[0] - fp[0]) ** 2 + (ep[1] - fp[1]) ** 2 + (ep[2] - fp[2]) ** 2); 63 | } 64 | 65 | function getViewDelta(cursorPosition, lastCursorPosition, lookSensitivity) { 66 | const dx = (cursorPosition[0] - lastCursorPosition[0]) * lookSensitivity; 67 | const dy = (cursorPosition[1] - lastCursorPosition[1]) * lookSensitivity; 68 | return [dx, dy]; 69 | } 70 | 71 | 72 | 73 | 74 | 75 | function initWebgl(canvas) { 76 | var gl = canvas.getContext("webgl2"); 77 | 78 | if (!gl) { 79 | console.error("WebGL 2 not available"); 80 | document.body.innerHTML = "This example requires WebGL 2 which is unavailable on this system." 81 | } 82 | 83 | gl.clearColor(0.0, 0.0, 0.0, 0.0); 84 | // gl.enable(gl.BLEND); 85 | // gl.depthMask(false); 86 | gl.clear(gl.COLOR_BUFFER_BIT); 87 | 88 | if (!gl.getExtension("EXT_color_buffer_float")) { 89 | console.error("FLOAT color buffer not available"); 90 | document.body.innerHTML = "This example requires EXT_color_buffer_float which is unavailable on this system." 91 | } 92 | 93 | return gl; 94 | } 95 | 96 | function updateFPSDisplay(fps, averageFPS) { 97 | const fpsElem = document.querySelector("#fps"); 98 | if (!fpsElem) return; 99 | fpsElem.textContent = fps.toFixed(1); // update fps display 100 | const avgElem = document.querySelector("#avg"); 101 | if (!avgElem) return; 102 | avgElem.textContent = averageFPS.toFixed(1); // update avg display 103 | } 104 | 105 | function calcFPS(now) { 106 | const deltaTime = now - fpsData.then; 107 | fpsData.then = now; 108 | if (deltaTime == 0) return; 109 | 110 | const fps = 1000 / deltaTime; 111 | 112 | // add the current fps and remove the oldest fps 113 | fpsData.totalFPS += fps - (fpsData.frameTimes[fpsData.frameCursor] || 0); 114 | 115 | // record the newest fps 116 | fpsData.frameTimes[fpsData.frameCursor++] = fps; 117 | 118 | // needed so the first N frames, before we have maxFrames, is correct. 119 | fpsData.numFrames = Math.max(fpsData.numFrames, fpsData.frameCursor); 120 | 121 | // wrap the cursor 122 | fpsData.frameCursor %= fpsData.maxFrames; 123 | 124 | updateFPSDisplay(fps, fpsData.totalFPS / fpsData.numFrames); 125 | } 126 | 127 | function getCameraTransform(canvas, viewParams) { 128 | var projMatrix = new Float32Array(16); 129 | 130 | let viewMatrix = viewParams.matrix; 131 | var viewProjMatrix = new Float32Array(16); 132 | 133 | mat4perspective(projMatrix, Math.PI / 3, canvas.width / canvas.height, 0.1, 20.0); 134 | mat4multiply(viewProjMatrix, projMatrix, viewMatrix); 135 | 136 | return { 137 | proj: projMatrix, 138 | view: viewMatrix, 139 | viewProj: viewProjMatrix 140 | } 141 | } 142 | 143 | 144 | // pad to change positions from [x1, y1, z1, x2, y2, z2, ...] 145 | // to [x1, y1, z1, 0, x1, y1, z2, 0, ...] 146 | function padPositions(positions) { 147 | let n = Math.ceil((positions.length / 3) | 0); 148 | 149 | let paddedPositions = new Float32Array(4 * n); 150 | 151 | for (let i = 0; i < n; i += 1) { 152 | paddedPositions[4 * i] = positions[3 * i]; 153 | paddedPositions[4 * i + 1] = positions[3 * i + 1]; 154 | paddedPositions[4 * i + 2] = positions[3 * i + 2]; 155 | paddedPositions[4 * i + 3] = 0; 156 | } 157 | 158 | return paddedPositions; 159 | } 160 | 161 | function makeTextures(gl, position, color, covP2, covP1, group_size, n_groups) { 162 | return { 163 | position: toTexture(gl, position, group_size, n_groups, 'float', 4), 164 | color: toTexture(gl, color, group_size, n_groups, 'float', 4), 165 | covP1: toTexture(gl, covP1, group_size, n_groups, 'float', 4), 166 | covP2: toTexture(gl, covP2, group_size, n_groups, 'float', 2) 167 | } 168 | } 169 | 170 | function bindTextures(gl, program, permTextures, vertexTextures, pipelineType) { 171 | if (pipelineType == 'kdtree') { 172 | gl.activeTexture(gl.TEXTURE2); 173 | gl.bindTexture(gl.TEXTURE_2D, permTextures.outer.texture); 174 | gl.uniform1i(gl.getUniformLocation(program, 'perm_outer_idx'), 2); 175 | 176 | gl.activeTexture(gl.TEXTURE0); 177 | gl.bindTexture(gl.TEXTURE_2D, permTextures.inner.texture); 178 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 179 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 180 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 181 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 182 | const permInnerIdxLoc = gl.getUniformLocation(program, 'perm_inner_idx'); 183 | gl.uniform1i(permInnerIdxLoc, 0); 184 | } 185 | 186 | gl.activeTexture(gl.TEXTURE3); 187 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.position.texture); 188 | gl.uniform1i(gl.getUniformLocation(program, 'positionTexture'), 3); 189 | 190 | gl.activeTexture(gl.TEXTURE4); 191 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.color.texture); 192 | gl.uniform1i(gl.getUniformLocation(program, 'colorTexture'), 4); 193 | 194 | gl.activeTexture(gl.TEXTURE5); 195 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.covP1.texture); 196 | gl.uniform1i(gl.getUniformLocation(program, 'covP1Texture'), 5); 197 | 198 | gl.activeTexture(gl.TEXTURE6); 199 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.covP2.texture); 200 | gl.uniform1i(gl.getUniformLocation(program, 'covP2Texture'), 6); 201 | } 202 | 203 | function setTransform(gl, program, cameraXform) { 204 | gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uView'), false, cameraXform.view); 205 | gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uViewProj'), false, cameraXform.viewProj); 206 | gl.uniform2fv(gl.getUniformLocation(program, 'uViewportScale'), cameraXform.viewportScale); 207 | } 208 | 209 | function setSphereTransform(gl, program, viewProjMatrix, radius, lookAtPoint) { 210 | gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uViewProj'), false, viewProjMatrix); 211 | gl.uniform1f(gl.getUniformLocation(program, 'uRadius'), radius); 212 | gl.uniform3fv(gl.getUniformLocation(program, 'uLookAtPoint'), lookAtPoint); 213 | } 214 | 215 | // pipelineType can be 'full' or 'kdtree' 216 | function renderMain(canvas, data, cameraParams, pipelineType, interactMod=null) { 217 | let gl = initWebgl(canvas); 218 | 219 | canvas.setAttribute('tabindex', '0'); 220 | canvas.focus() 221 | 222 | let shaderProgram = createRenderProgram(gl, pipelineType); 223 | let sphereProgram = createSphereRenderProgram(gl); 224 | 225 | let circleVerts = createSphereCircles(1.0, 64); 226 | 227 | // Create objects 228 | const GROUP_SIZE = 1024; //gl.getParameter(gl.MAX_TEXTURE_SIZE); 229 | const N_GROUPS = Math.floor(Math.floor(data.positions.length / 3) / GROUP_SIZE); 230 | const NUM_PARTICLES = GROUP_SIZE * N_GROUPS; 231 | 232 | const SORT_INTERVAL = 6; 233 | 234 | let positionData = padPositions(data.positions); 235 | let colorData = data.colors; 236 | 237 | let covData = rotorsToCov3D(data.scales, data.rotors); 238 | let covP1Data = covData.p1; 239 | let covP2Data = covData.p2; 240 | 241 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 242 | 243 | gl.useProgram(shaderProgram); 244 | 245 | var buffer = gl.createBuffer(); 246 | // make this buffer the current 'ELEMENT_ARRAY_BUFFER' 247 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); 248 | gl.bufferData( 249 | gl.ELEMENT_ARRAY_BUFFER, 250 | new Uint32Array(0), 251 | gl.STATIC_DRAW 252 | ); 253 | 254 | let pipeline; 255 | if (pipelineType == 'full') { 256 | pipeline = createFullSortPipeline(gl, GROUP_SIZE, N_GROUPS); 257 | } else { 258 | pipeline = createPipeline(gl, positionData, GROUP_SIZE, N_GROUPS); 259 | 260 | positionData = permuteArray(positionData, pipeline.perm, 4); 261 | colorData = permuteArray(colorData, pipeline.perm, 4); 262 | covP1Data = permuteArray(covP1Data, pipeline.perm, 4); 263 | covP2Data = permuteArray(covP2Data, pipeline.perm, 2); 264 | } 265 | 266 | let vertexTextures = makeTextures(gl, positionData, colorData, covP2Data, covP1Data, GROUP_SIZE, N_GROUPS); 267 | 268 | var animationFrameId; 269 | 270 | var i = 0; 271 | let isMouseDown = false; 272 | let lastMousePosition = [0, 0]; 273 | let pressedKeys = new Set(); 274 | 275 | var viewParams = { 276 | radius: getRadius(cameraParams), 277 | matrix: initializeViewMatrix(cameraParams), 278 | lookSensitivity: 0.003, 279 | sphereRadius: 3.0, 280 | showSphere: false, 281 | } 282 | 283 | var permTextures; 284 | 285 | let draw = function (now) { 286 | // Check if the canvas still exists 287 | if (!document.body.contains(gl.canvas)) { 288 | cancelAnimationFrame(animationFrameId); 289 | return; 290 | } 291 | 292 | // Set scene transforms. 293 | let cameraXform = getCameraTransform(canvas, viewParams); 294 | 295 | // apply sorting pipeline. 296 | if (pipelineType == 'full') { 297 | applyFullSortPipeline(gl, pipeline, vertexTextures, cameraXform.viewProj, Math.ceil(pipeline.sortSteps.length / SORT_INTERVAL)); 298 | permTextures = []; 299 | } else { 300 | if (((i % SORT_INTERVAL) | 0) == 0) { 301 | permTextures = applyPipeline(gl, pipeline, viewParams.eyePosition, cameraXform.viewProj); 302 | } 303 | i += 1; 304 | } 305 | 306 | cameraXform.viewportScale = new Float32Array([canvas.width, canvas.height]); 307 | 308 | // Set scene transform uniforms. 309 | gl.useProgram(shaderProgram); 310 | setTransform(gl, shaderProgram, cameraXform); 311 | 312 | // Set viewport params. 313 | gl.viewport(0, 0, canvas.width, canvas.height); 314 | gl.enable(gl.SCISSOR_TEST); 315 | gl.scissor(0, 0, canvas.width, canvas.height); 316 | 317 | bindTextures(gl, shaderProgram, permTextures, vertexTextures, pipelineType); 318 | gl.uniform2i(gl.getUniformLocation(shaderProgram, 'textureSize'), GROUP_SIZE, N_GROUPS); 319 | 320 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 321 | 322 | // Prepare viewport for rendering and blending. 323 | gl.clear(gl.DEPTH_BUFFER_BIT); 324 | gl.disable(gl.DEPTH_TEST); 325 | gl.enable(gl.BLEND); 326 | gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE); //gl.drawArrays(gl.POINTS, 0, NUM_PARTICLES); 327 | 328 | // Draw all the vertices as points, in the order given in the element array buffer. 329 | gl.drawArrays(gl.POINTS, 0, NUM_PARTICLES); 330 | 331 | let lookAtPos = viewMatGetLookAt(viewParams.matrix, viewParams.radius); 332 | 333 | // Draw the sphere circles 334 | if (viewParams.showSphere) { 335 | gl.useProgram(sphereProgram); 336 | setSphereTransform(gl, sphereProgram, cameraXform.viewProj, viewParams.sphereRadius, lookAtPos); 337 | renderSphereCircles(gl, sphereProgram, circleVerts); 338 | } 339 | 340 | // Reset values of variables so that other shaders can run. 341 | gl.enable(gl.DEPTH_TEST); 342 | gl.disable(gl.BLEND); 343 | 344 | calcFPS(now); 345 | 346 | // Request next animation frame 347 | animationFrameId = requestAnimationFrame(draw); 348 | } 349 | 350 | function handleVisibilityChange() { 351 | if (document.hidden) { 352 | cancelAnimationFrame(animationFrameId); 353 | } else { 354 | animationFrameId = requestAnimationFrame(draw); 355 | } 356 | } 357 | 358 | // Event listener for tab visibility 359 | document.addEventListener("visibilitychange", handleVisibilityChange, false); 360 | 361 | // Function to create the camera query string 362 | // TODO: get this from camera matrix 363 | function createQueryString(viewParams) { 364 | // Parse existing query parameters 365 | const params = new URLSearchParams(window.location.search); 366 | let cameraParams = viewMatGetPoseParams(viewParams.matrix, viewParams.radius); 367 | 368 | // Update with new parameters from cameraObject 369 | params.set('camera', cameraParams.camera.join(',')); 370 | params.set('lookAt', cameraParams.lookAt.join(',')); 371 | params.set('up', cameraParams.up.join(',')); 372 | 373 | // Return the full query string 374 | return params.toString(); 375 | } 376 | 377 | // event listener for the button to get the url link 378 | const getURLButton = document.getElementById('getURLButton'); 379 | if (getURLButton !== null) { 380 | getURLButton.addEventListener('click', () => { 381 | const queryString = createQueryString(viewParams); 382 | 383 | const fullUrl = `${window.location.origin}${window.location.pathname}?${queryString}`; 384 | alert(fullUrl); 385 | console.log(fullUrl); 386 | // why doesn't this work??!?!??! 387 | navigator.clipboard.writeText(fullUrl).then(() => { 388 | console.log('URL copied to clipboard'); 389 | }).catch(err => { 390 | console.error('Error in copying text: ', err); 391 | }); 392 | }); 393 | } 394 | 395 | canvas.addEventListener("keydown", (event) => { 396 | pressedKeys.add(event.code); 397 | }, 398 | true 399 | ); 400 | canvas.addEventListener("keyup", (event) => { 401 | pressedKeys.delete(event.code); 402 | }, 403 | false, 404 | ); 405 | 406 | const sensitivitySlider = document.getElementById('controlSensitivity'); 407 | if (sensitivitySlider !== null) { 408 | sensitivitySlider.addEventListener('input', (event) => { 409 | viewParams.lookSensitivity = 0.0001 * parseFloat(event.target.value); 410 | }); 411 | } 412 | 413 | const sphereSizeSlider = document.getElementById('sphereSize'); 414 | if (sphereSizeSlider !== null) { 415 | sphereSizeSlider.addEventListener('input', (event) => { 416 | viewParams.sphereRadius = 0.03*Math.exp(0.05*parseFloat(event.target.value)); 417 | }); 418 | } 419 | 420 | const showSpheresCheckBox = document.getElementById('showSpheres'); 421 | if (showSpheresCheckBox !== null) { 422 | showSpheresCheckBox.addEventListener('change', (event) => { 423 | viewParams.showSphere = event.target.checked; 424 | }); 425 | } 426 | 427 | // Start the animation loop 428 | animationFrameId = requestAnimationFrame(draw); 429 | 430 | // Prevent default right-click context menu on the canvas 431 | canvas.addEventListener('contextmenu', function (e) { 432 | e.preventDefault(); // Prevents the default context menu from appearing 433 | }); 434 | 435 | canvas.addEventListener('mousedown', function (event) { 436 | isMouseDown = true; 437 | lastMousePosition = [event.clientX, event.clientY]; 438 | }); 439 | 440 | canvas.addEventListener('mousemove', function (event) { 441 | viewParams.viewSpin = false; 442 | let mousePosition = [event.clientX, event.clientY]; 443 | 444 | // determine if event.button is in the keys of mouseControlMap 445 | if (event.buttons in mouseControlMap) { 446 | // determine if the button number key is in mouseControlMap[event.button] 447 | let keyMap = mouseControlMap[event.buttons]; 448 | // Is any key in both the set pressedKeys and the keys of keyMap? 449 | let keyPressed = Array.from(pressedKeys).find(key => key in keyMap); 450 | 451 | let action = keyMap[""]; 452 | if (keyPressed in keyMap) { 453 | // if it is, call the corresponding function 454 | action = keyMap[keyPressed]; 455 | } 456 | 457 | let delta = getViewDelta(mousePosition, lastMousePosition, viewParams.lookSensitivity); 458 | viewParams = viewUpdate(action, delta, viewParams); 459 | } else { 460 | if (isMouseDown) { 461 | let delta = getViewDelta(mousePosition, lastMousePosition, viewParams.lookSensitivity); 462 | viewParams = viewUpdate('orbit', delta, viewParams); 463 | } 464 | } 465 | 466 | lastMousePosition = mousePosition; 467 | }); 468 | 469 | canvas.addEventListener('wheel', function (event) { 470 | event.preventDefault(); // Prevents the default scrolling behavior 471 | 472 | let dy = -event.deltaY * viewParams.lookSensitivity; 473 | 474 | viewParams = viewUpdate('dolly', dy, viewParams); 475 | }, { passive: false }); 476 | 477 | canvas.addEventListener('mouseup', function (event) { 478 | isMouseDown = false; 479 | }); 480 | 481 | canvas.addEventListener('mouseleave', function (event) { 482 | isMouseDown = false; 483 | }); 484 | 485 | 486 | canvas.addEventListener('touchmove', function (event) { 487 | if (event.touches.length == 1) { 488 | let mousePosition = [event.touches[0].clientX, event.touches[0].clientY]; 489 | 490 | let delta = getViewDelta(mousePosition, lastMousePosition, viewParams.lookSensitivity); 491 | 492 | if (isMouseDown) { 493 | viewParams = viewUpdate('orbit', delta, viewParams); 494 | } 495 | 496 | lastMousePosition = mousePosition; 497 | } 498 | }); 499 | 500 | canvas.addEventListener('touchstart', function (event) { 501 | isMouseDown = true; 502 | if (event.touches.length == 1) { 503 | lastMousePosition = [event.touches[0].clientX, event.touches[0].clientY]; 504 | } 505 | }); 506 | 507 | canvas.addEventListener('touchend', function (event) { 508 | isMouseDown = false; 509 | lastMousePosition = [0, 0]; 510 | }); 511 | 512 | // check if interactMod is not null 513 | if(interactMod !== null) { 514 | interactMod('#gl-canvas') 515 | .gesturable({ 516 | onmove: function (event) { 517 | // Panning 518 | const dx = event.dx; 519 | const dy = event.dy; 520 | viewParams = viewUpdate('strafe', [dx * viewParams.lookSensitivity, dy * viewParams.lookSensitivity], viewParams); 521 | 522 | // Pinch zooming 523 | const scale = event.ds; 524 | viewParams = viewUpdate('dolly', -5 * scale, viewParams); 525 | 526 | // Pinch rotation 527 | const rotation = event.da; 528 | viewParams = viewUpdate('roll', -3.14159 * rotation / 180, viewParams); 529 | } 530 | }); 531 | } 532 | 533 | return draw; 534 | } 535 | 536 | function readParams(params, canvas, interactMod=null) { 537 | // Function to parse a comma-separated string into an array of numbers 538 | const parseVector = (param, defaultValue) => { 539 | return param ? param.split(',').map(Number) : defaultValue; 540 | }; 541 | 542 | // Extract and parse each parameter, or use default values if not present 543 | cameraParams.position = parseVector(params.get('camera'), cameraParams.position); 544 | cameraParams.lookAt = parseVector(params.get('lookAt'), cameraParams.lookAt); 545 | cameraParams.up = parseVector(params.get('up'), cameraParams.up); 546 | 547 | let qualityParam = params.get('quality') || 'high'; 548 | if (qualityParam === 'high') { 549 | pipelineType = 'full'; 550 | } else if (qualityParam === 'fast') { 551 | pipelineType = 'kdtree'; 552 | } else { 553 | console.error('Invalid quality parameter: ' + qualityParam); 554 | } 555 | 556 | const url = params.get('url'); // Get the 'url' parameter 557 | if (url) { 558 | document.getElementById('loadingSymbol').style.display = 'block'; 559 | 560 | // If the url parameter is set, load the file from the url 561 | fetch(url) 562 | .then(response => response.arrayBuffer()) 563 | .then(data => { 564 | // Process the file contents 565 | let splatData = loadSplatData(data); 566 | 567 | document.getElementById('loadingSymbol').style.display = 'none'; 568 | 569 | renderMain(canvas, splatData, cameraParams, pipelineType, interactMod); 570 | }); 571 | } 572 | } 573 | 574 | 575 | export { renderMain, readParams, cameraParams, pipelineType, mouseControlMap, loadSplatData, mat4multiply, viewMatGetPoseParams }; 576 | 577 | -------------------------------------------------------------------------------- /img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 50 | 54 | 58 | 62 | 66 | 67 | 69 | 74 | 82 | 88 | 93 | 98 | 103 | 108 | 113 | 114 | 120 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /img/rotating.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 40 | 42 | 47 | 50 | 54 | 58 | 62 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Portality Splat Viewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 20 | 21 | 22 |
23 |
fps:
24 |
average fps:
25 |
26 |
27 | 30 |
31 | 32 |
33 | 34 | 35 | 36 | Desktop controls: LMB: Orbit, MMB: Dolly/Roll, RMB: Strafe. Mobile controls: Orbit with one finger. Dolly and roll with two finger gestures. Strafe with two finger gesture. 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 | Control sensitivity: 47 |
48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 | 64 | 65 | 66 | 67 | 212 | 213 | -------------------------------------------------------------------------------- /lib/bitonic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sorts an array of length nSort using the bitonic sort algorithm. 3 | * @param {Kernel} ker - A webgl kernel that sorts an array of length n. 4 | * @param {Array} inp - The input array to be sorted. 5 | * @param {number} nSort - Length of the array to be sorted. 6 | * @returns {Array} - The sorted array. 7 | */ 8 | function bitonicSort(ker, inp, nSort) 9 | { 10 | if (nSort < 2) 11 | { 12 | return inp; 13 | } 14 | 15 | var c = 1; 16 | var result = ker[c](inp, 1, 2); 17 | 18 | for (var k = 4; k < (nSort<<1); k <<= 1) 19 | { 20 | for (var j = k>>1; j > 0; j >>= 1) 21 | { 22 | c = (c + 1) % 2; 23 | result = ker[c](result, j, k); 24 | } 25 | } 26 | 27 | return result; 28 | } 29 | 30 | function sortSteps(nSort) 31 | { 32 | var steps = []; 33 | 34 | if (nSort < 2) { 35 | return steps; 36 | } 37 | 38 | for (var k = 2; k < (nSort<<1); k <<= 1) 39 | { 40 | steps.push([k-1, k]); 41 | for (var j = k>>1; j > 0; j >>= 1) 42 | { 43 | steps.push([j, k]); 44 | } 45 | } 46 | 47 | return steps; 48 | } 49 | 50 | function applySteps(ker, inp, steps, startStep, stopStep) 51 | { 52 | var c = (startStep + 1) % 2; 53 | 54 | var result = ker[c](inp, steps[startStep][0], steps[startStep][1]); 55 | 56 | for (var i=startStep+1; i=0 && !orderFlag) || (dist<=0 && orderFlag)) { 113 | return rl.concat(rr); 114 | } else { 115 | return rr.concat(rl); 116 | } 117 | 118 | 119 | 120 | } 121 | 122 | throw new Error(`Unknown node type.`); 123 | } 124 | 125 | export function depthFirstCollect(kdt, indices, groupIdx) { 126 | if (kdt == null) { 127 | return []; 128 | } 129 | 130 | if (kdt.hasOwnProperty('range')) { 131 | let thisIdx = new Array(kdt.range[1]-kdt.range[0]); 132 | 133 | for (var i=kdt.range[0]; i', 'reverse': '<'}[order]; 6 | 7 | const fragSrc = 8 | `#version 300 es 9 | precision highp float; 10 | precision highp int; 11 | precision highp sampler2D; 12 | precision highp sampler2DArray; 13 | 14 | uniform highp sampler2D uInputTexture; 15 | 16 | uniform uint uStepj; 17 | uniform uint uStepk; 18 | 19 | out float data0; 20 | 21 | void main(void) { 22 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 23 | 24 | uint idx = uint(fragCoord.x); 25 | 26 | uint l = idx ^ uStepj; 27 | uint idxk = idx & uStepk; 28 | bool ik = int(idxk)==0; 29 | 30 | bool islow = l ${op} idx; 31 | 32 | float xl = texelFetch(uInputTexture, ivec2(int(l), fragCoord.y), 0)[0]; 33 | float xi = texelFetch(uInputTexture, fragCoord, 0)[0]; 34 | 35 | bool isorder = xi > xl; 36 | bool swap = ik == isorder; 37 | 38 | data0 = (xl == xi) ? xi : (islow==swap ? xl : xi); 39 | }` 40 | 41 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m)); 42 | 43 | const innerKernel = function (x, uStepj, uStepk) { 44 | /** end setup uploads for kernel values **/ 45 | gl.useProgram(shdr.program); 46 | gl.scissor(0, 0, n, m); 47 | 48 | gl.activeTexture(gl.TEXTURE0); 49 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 50 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uInputTexture'); 51 | gl.uniform1i(glVariable95, 0); 52 | 53 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uStepj'); 54 | gl.uniform1ui(glVariable17, uStepj); 55 | 56 | const glVariable18 = gl.getUniformLocation(shdr.program, 'uStepk'); 57 | gl.uniform1ui(glVariable18, uStepk); 58 | 59 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 60 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 61 | return { 62 | texture: shdr.outTextures[0], 63 | type: 'ArrayTexture(1)', 64 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m) 65 | }; 66 | }; 67 | return innerKernel; 68 | } 69 | 70 | function createFullBitonicKernel(gl, n, m, order='forward') { 71 | let op = {'forward': '>', 'reverse': '<'}[order]; 72 | 73 | const fragSrc = 74 | `#version 300 es 75 | precision highp float; 76 | precision highp int; 77 | precision highp usampler2D; 78 | precision highp usampler2DArray; 79 | 80 | uniform highp usampler2D uInputTexture; 81 | 82 | uniform uint uStepjx; 83 | uniform uint uStepjy; 84 | uniform uint uTexSizeX; 85 | uniform uint uTexSizeY; 86 | 87 | out uvec2 data; 88 | 89 | void main(void) { 90 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 91 | 92 | uint idxx = uint(fragCoord.x); 93 | uint idxy = uint(fragCoord.y); 94 | 95 | uint lx = idxx ^ uStepjx; 96 | uint ly = idxy ^ uStepjy; 97 | 98 | bool inbounds = ly < uTexSizeY; 99 | 100 | uvec2 x_idx_i = texelFetch(uInputTexture, fragCoord, 0).xy; 101 | 102 | if (inbounds){ 103 | bool islow = idxy < ly || (idxy == ly && idxx < lx); 104 | 105 | ivec2 idx_l = ivec2(int(lx), int(ly)); 106 | 107 | uvec2 x_idx_l = texelFetch(uInputTexture, idx_l, 0).xy; 108 | 109 | uint xl = x_idx_l.x; 110 | uint xi = x_idx_i.x; 111 | 112 | bool isnotorder = xi ${op} xl; 113 | 114 | bool swap = (isnotorder == islow) && (xl != xi); 115 | 116 | data = swap ? x_idx_l : x_idx_i; 117 | } else { 118 | data = x_idx_i; 119 | } 120 | 121 | }` 122 | 123 | 124 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.RG32UI])); 125 | 126 | const innerKernel = function (x, uStepj, uStepk) { 127 | /** end setup uploads for kernel values **/ 128 | gl.useProgram(shdr.program); 129 | gl.scissor(0, 0, n, m); 130 | 131 | gl.activeTexture(gl.TEXTURE0); 132 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 133 | const uixVar = gl.getUniformLocation(shdr.program, 'uInputTexture'); 134 | gl.uniform1i(uixVar, 0); 135 | 136 | const usjxVar = gl.getUniformLocation(shdr.program, 'uStepjx'); 137 | gl.uniform1ui(usjxVar, uStepj % n); 138 | 139 | const usjyVar = gl.getUniformLocation(shdr.program, 'uStepjy'); 140 | gl.uniform1ui(usjyVar, Math.floor(uStepj / n)); 141 | 142 | const utsxVar = gl.getUniformLocation(shdr.program, 'uTexSizeX'); 143 | gl.uniform1ui(utsxVar, n); 144 | 145 | const utsyVar = gl.getUniformLocation(shdr.program, 'uTexSizeY'); 146 | gl.uniform1ui(utsyVar, m); 147 | 148 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 149 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 150 | return { 151 | texture: shdr.outTextures[0], 152 | type: 'ArrayTexture(1)', 153 | // No toArrayFunc here 154 | }; 155 | }; 156 | return innerKernel; 157 | } 158 | 159 | 160 | 161 | function createFullEvenOddKernel(gl, n, m, order='forward') { 162 | let op = {'forward': '>', 'reverse': '<'}[order]; 163 | 164 | 165 | const fragSrc = 166 | `#version 300 es 167 | precision highp float; 168 | precision highp int; 169 | precision highp usampler2D; 170 | precision highp usampler2DArray; 171 | 172 | uniform highp usampler2D uInputTexture; 173 | 174 | uniform uint uStepj; // smaller one 175 | uniform uint uStepk; // bigger one 176 | uniform uint uStart; 177 | uniform uint uTotalLen; 178 | 179 | out uvec2 data; 180 | 181 | void main(void) { 182 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 183 | ivec2 texSize = textureSize(uInputTexture, 0); 184 | 185 | uint idx = uint(fragCoord.x) + uint(fragCoord.y)*uint(texSize.x); 186 | 187 | int j = int(idx) - int(uStart); 188 | uint l = (uint(j) ^ uStepj) + uStart; 189 | 190 | bool inbounds = (j >= 0) && (l < uTotalLen); 191 | 192 | uint group_i = idx & uStepk; 193 | uint group_l = l & uStepk; 194 | bool samegroup = (group_i == group_l); 195 | 196 | uvec2 x_idx_i = texelFetch(uInputTexture, fragCoord, 0).xy; 197 | 198 | if (inbounds && samegroup){ 199 | bool islow = idx < l; 200 | 201 | ivec2 idx_l = ivec2(int(l) % texSize.x, int(l) / texSize.x); 202 | 203 | uvec2 x_idx_l = texelFetch(uInputTexture, idx_l, 0).xy; 204 | 205 | uint xl = x_idx_l.x; 206 | uint xi = x_idx_i.x; 207 | 208 | bool isnotorder = ((xi ${op} xl) == islow); 209 | 210 | data = (xl == xi) ? x_idx_i : (isnotorder ? x_idx_l : x_idx_i); 211 | } else { 212 | data = x_idx_i; 213 | } 214 | }` 215 | 216 | 217 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.RG32UI])); 218 | 219 | const innerKernel = function (x, uStepj, uStepk) { 220 | /** end setup uploads for kernel values **/ 221 | gl.useProgram(shdr.program); 222 | gl.scissor(0, 0, n, m); 223 | 224 | gl.activeTexture(gl.TEXTURE0); 225 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 226 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uInputTexture'); 227 | gl.uniform1i(glVariable95, 0); 228 | 229 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uStepj'); 230 | gl.uniform1ui(glVariable17, uStepj); 231 | 232 | const glVariable18 = gl.getUniformLocation(shdr.program, 'uStepk'); 233 | gl.uniform1ui(glVariable18, uStepk); 234 | 235 | const glVariable19 = gl.getUniformLocation(shdr.program, 'uStart'); 236 | gl.uniform1ui(glVariable19, (uStepj % (uStepk>>1)) | 0); 237 | 238 | const glVariable20 = gl.getUniformLocation(shdr.program, 'uTotalLen'); 239 | gl.uniform1ui(glVariable20, n*m); 240 | 241 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 242 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 243 | return { 244 | texture: shdr.outTextures[0], 245 | type: 'ArrayTexture(1)', 246 | // No toArrayFunc here 247 | }; 248 | }; 249 | return innerKernel; 250 | } 251 | 252 | function createTexturePermuteKernel0(gl, n, m){ 253 | const fragSrc = 254 | `#version 300 es 255 | precision highp float; 256 | precision highp int; 257 | // precision highp sampler2D; 258 | // precision highp sampler2DArray; 259 | // precision highp usampler2D; 260 | // precision highp usampler2DArray; 261 | 262 | uniform highp usampler2D perm_idx; 263 | 264 | uniform sampler2D uInputPosition; 265 | uniform sampler2D uInputColor; 266 | uniform sampler2D uInputCovP1; 267 | uniform sampler2D uInputCovP2; 268 | 269 | layout(location = 0) out vec4 outPosition; 270 | layout(location = 1) out vec4 outColor; 271 | layout(location = 2) out vec4 outCovP1; 272 | layout(location = 3) out vec2 outCovP2; 273 | 274 | void main(void) { 275 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 276 | ivec2 texSize = textureSize(perm_idx, 0); 277 | 278 | uint idx = texelFetch(perm_idx, fragCoord, 0)[0]; 279 | ivec2 texCoord = ivec2(int(idx) % texSize.x, int(idx) / texSize.x); 280 | 281 | outPosition = texelFetch(uInputPosition, texCoord, 0).xyzw; 282 | outColor = texelFetch(uInputColor, texCoord, 0).xyzw; 283 | outCovP1 = texelFetch(uInputCovP1, texCoord, 0).xyzw; 284 | outCovP2 = texelFetch(uInputCovP2, texCoord, 0).xy; 285 | }` 286 | 287 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.RGBA32F, gl.RGBA32F, gl.RGBA32F, gl.RG32F])); 288 | //let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.RGBA32F, gl.RGBA32F])); 289 | 290 | const innerKernel = function (perm_idx, vertexTextures) { 291 | /** end setup uploads for kernel values **/ 292 | gl.useProgram(shdr.program); 293 | gl.scissor(0, 0, n, m); 294 | 295 | gl.activeTexture(gl.TEXTURE0); 296 | gl.bindTexture(gl.TEXTURE_2D, perm_idx.texture); 297 | const perm_idxLoc = gl.getUniformLocation(shdr.program, 'perm_idx'); 298 | gl.uniform1i(perm_idxLoc, 0); 299 | 300 | gl.activeTexture(gl.TEXTURE3); 301 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.position.texture); 302 | const positionLoc = gl.getUniformLocation(shdr.program, 'uInputPosition'); 303 | gl.uniform1i(positionLoc, 3); 304 | 305 | gl.activeTexture(gl.TEXTURE4); 306 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.color.texture); 307 | const colorLoc = gl.getUniformLocation(shdr.program, 'uInputColor'); 308 | gl.uniform1i(colorLoc, 4); 309 | 310 | gl.activeTexture(gl.TEXTURE5); 311 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.covP1.texture); 312 | const covP1Loc = gl.getUniformLocation(shdr.program, 'uInputCovP1'); 313 | gl.uniform1i(covP1Loc, 5); 314 | 315 | gl.activeTexture(gl.TEXTURE6); 316 | gl.bindTexture(gl.TEXTURE_2D, vertexTextures.covP2.texture); 317 | const covP2Loc = gl.getUniformLocation(shdr.program, 'uInputCovP2'); 318 | gl.uniform1i(covP2Loc, 6); 319 | 320 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 321 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 322 | return { 323 | position: { 324 | texture: shdr.outTextures[0], 325 | type: 'ArrayTexture(1)', 326 | // No toArrayFunc here 327 | }, 328 | color: { 329 | texture: shdr.outTextures[1], 330 | type: 'ArrayTexture(1)', 331 | // No toArrayFunc here 332 | }, 333 | covP1: { 334 | texture: shdr.outTextures[2], 335 | type: 'ArrayTexture(1)', 336 | // No toArrayFunc here 337 | }, 338 | covP2: { 339 | texture: shdr.outTextures[3], 340 | type: 'ArrayTexture(1)', 341 | // No toArrayFunc here 342 | }, 343 | }; 344 | }; 345 | return innerKernel; 346 | } 347 | 348 | 349 | 350 | function createTexturePermuteKernel(gl, n, m, nChannels){ 351 | let swiz = nChannels==4 ? 'xyzw' : 'xy'; 352 | 353 | const fragSrc = 354 | `#version 300 es 355 | precision highp float; 356 | precision highp int; 357 | 358 | uniform highp usampler2D perm_idx; 359 | 360 | uniform sampler2D uInput; 361 | 362 | out vec${nChannels} data; 363 | 364 | void main(void) { 365 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 366 | ivec2 texSize = textureSize(perm_idx, 0); 367 | 368 | uint idx = texelFetch(perm_idx, fragCoord, 0)[0]; 369 | ivec2 texCoord = ivec2(int(idx) % texSize.x, int(idx) / texSize.x); 370 | 371 | data = texelFetch(uInput, texCoord, 0).${swiz}; 372 | }` 373 | 374 | let internalFormat = nChannels==4 ? gl.RGBA32F : gl.RG32F; 375 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [internalFormat])); 376 | 377 | const innerKernel = function (perm_idx, x) { 378 | /** end setup uploads for kernel values **/ 379 | gl.useProgram(shdr.program); 380 | gl.scissor(0, 0, n, m); 381 | 382 | gl.activeTexture(gl.TEXTURE0); 383 | gl.bindTexture(gl.TEXTURE_2D, perm_idx.texture); 384 | const perm_idxLoc = gl.getUniformLocation(shdr.program, 'perm_idx'); 385 | gl.uniform1i(perm_idxLoc, 0); 386 | 387 | gl.activeTexture(gl.TEXTURE3); 388 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 389 | const positionLoc = gl.getUniformLocation(shdr.program, 'uInput'); 390 | gl.uniform1i(positionLoc, 3); 391 | 392 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 393 | 394 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 395 | 396 | gl.bindTexture(gl.TEXTURE_2D, null); 397 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 398 | 399 | return { 400 | texture: shdr.outTextures[0], 401 | type: 'ArrayTexture(1)', 402 | // No toArrayFunc here 403 | }; 404 | }; 405 | return innerKernel; 406 | } 407 | 408 | function createSwapTextureKernel(gl, n, m, nChannels){ 409 | let swiz = nChannels==4 ? 'xyzw' : 'xy'; 410 | 411 | const fragSrc = 412 | `#version 300 es 413 | precision highp float; 414 | precision highp int; 415 | 416 | uniform sampler2D uInput; 417 | 418 | out vec${nChannels} data; 419 | 420 | void main(void) { 421 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 422 | 423 | data = texelFetch(uInput, fragCoord, 0).${swiz}; 424 | }` 425 | 426 | let internalFormat = nChannels==4 ? gl.RGBA32F : gl.RG32F; 427 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [internalFormat])); 428 | 429 | const innerKernel = function (x) { 430 | /** end setup uploads for kernel values **/ 431 | gl.useProgram(shdr.program); 432 | gl.scissor(0, 0, n, m); 433 | 434 | gl.activeTexture(gl.TEXTURE0); 435 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 436 | const positionLoc = gl.getUniformLocation(shdr.program, 'uInput'); 437 | gl.uniform1i(positionLoc, 0); 438 | 439 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 440 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 441 | 442 | gl.bindTexture(gl.TEXTURE_2D, null); 443 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 444 | 445 | return { 446 | texture: shdr.outTextures[0], 447 | type: 'ArrayTexture(1)', 448 | // No toArrayFunc here 449 | }; 450 | }; 451 | return innerKernel; 452 | } 453 | 454 | export { 455 | createBitonicKernel, 456 | createFullBitonicKernel, 457 | createFullEvenOddKernel, 458 | createTexturePermuteKernel, 459 | createSwapTextureKernel 460 | }; -------------------------------------------------------------------------------- /lib/kernels/depth.js: -------------------------------------------------------------------------------- 1 | import { createOutTextures, fullTexShader } from "./shader.js"; 2 | import toArrayFunc from "./to-array.js"; 3 | 4 | function createDepthKernel(gl, n, m) { 5 | const fragSrc = 6 | `#version 300 es 7 | precision highp float; 8 | precision highp int; 9 | precision highp sampler2D; 10 | precision highp sampler2DArray; 11 | 12 | uniform highp sampler2D uCoords; 13 | uniform mat4 uViewProj_dk; 14 | 15 | out float data0; 16 | 17 | void main(void) { 18 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 19 | vec3 position_dk = texelFetch(uCoords, fragCoord, 0).xyz; 20 | 21 | vec4 clipPosition = uViewProj_dk * vec4(position_dk, 1.0); 22 | 23 | data0 = clipPosition.z / clipPosition.w; 24 | }` 25 | 26 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m)); 27 | 28 | const innerKernel = function (coords, viewProjMatrix) { 29 | /** end setup uploads for kernel values **/ 30 | gl.useProgram(shdr.program); 31 | gl.scissor(0, 0, n, m); 32 | 33 | gl.activeTexture(gl.TEXTURE0); 34 | gl.bindTexture(gl.TEXTURE_2D, coords.texture); 35 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uCoords'); 36 | gl.uniform1i(glVariable95, 0); 37 | 38 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uViewProj_dk'); 39 | var sceneUniformData = new Float32Array(viewProjMatrix); 40 | sceneUniformData.set(viewProjMatrix); 41 | gl.uniformMatrix4fv(glVariable17, false, sceneUniformData); 42 | 43 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 44 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 45 | return { 46 | program: shdr.program, 47 | texture: shdr.outTextures[0], 48 | type: 'ArrayTexture(1)', 49 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m) 50 | }; 51 | }; 52 | return innerKernel; 53 | } 54 | 55 | function createDepthIndexedKernel(gl, n, m) { 56 | const fragSrc = 57 | `#version 300 es 58 | precision highp float; 59 | precision highp int; 60 | precision highp sampler2D; 61 | precision highp sampler2DArray; 62 | 63 | uniform highp sampler2D uCoords; 64 | uniform mat4 uViewProj_dk; 65 | 66 | out uvec2 data; 67 | 68 | void main(void) { 69 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 70 | vec3 position_dk = texelFetch(uCoords, fragCoord, 0).xyz; 71 | ivec2 texSize = textureSize(uCoords, 0); 72 | 73 | uint idx = uint(fragCoord.x) + uint(fragCoord.y)*uint(texSize.x); 74 | 75 | vec4 clipPosition = uViewProj_dk * vec4(position_dk, 1.0); 76 | 77 | data = uvec2(floatBitsToUint(clipPosition.z / clipPosition.w), idx); 78 | }` 79 | 80 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.RG32UI])); 81 | 82 | const innerKernel = function (coords, viewProjMatrix) { 83 | /** end setup uploads for kernel values **/ 84 | gl.useProgram(shdr.program); 85 | gl.scissor(0, 0, n, m); 86 | 87 | gl.activeTexture(gl.TEXTURE0); 88 | gl.bindTexture(gl.TEXTURE_2D, coords.texture); 89 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uCoords'); 90 | gl.uniform1i(glVariable95, 0); 91 | 92 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uViewProj_dk'); 93 | var sceneUniformData = new Float32Array(viewProjMatrix); 94 | sceneUniformData.set(viewProjMatrix); 95 | gl.uniformMatrix4fv(glVariable17, false, sceneUniformData); 96 | 97 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 98 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 99 | return { 100 | program: shdr.program, 101 | texture: shdr.outTextures[0], 102 | // No toArrayFunc here 103 | }; 104 | }; 105 | 106 | return innerKernel; 107 | } 108 | 109 | export { createDepthKernel, createDepthIndexedKernel }; -------------------------------------------------------------------------------- /lib/kernels/endian.js: -------------------------------------------------------------------------------- 1 | let endianNess = () => { 2 | let uInt32 = new Uint32Array([0x42000069]); 3 | let uInt8 = new Uint8Array(uInt32.buffer); 4 | 5 | if(uInt8[0] === 0x69) { 6 | return 'le'; 7 | } else if (uInt8[0] === 0x42) { 8 | return 'be'; 9 | } else { 10 | return 'unknown'; 11 | } 12 | }; 13 | 14 | export default endianNess; -------------------------------------------------------------------------------- /lib/kernels/get-channel.js: -------------------------------------------------------------------------------- 1 | import { createOutTextures, fullTexShader } from "./shader.js"; 2 | import toArrayFunc from "./to-array.js"; 3 | 4 | /* Create kernel that takes RG32UI texture and gets either the R or G channel as R32UI */ 5 | function createGetChannelKernel(gl, n, m, channel){ 6 | const fragSrc = 7 | `#version 300 es 8 | precision highp float; 9 | precision highp int; 10 | precision highp usampler2D; 11 | precision highp usampler2DArray; 12 | 13 | uniform highp usampler2D uInputTexture; 14 | 15 | out uint data0; 16 | 17 | void main(void) { 18 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 19 | uvec2 x_idx_i = texelFetch(uInputTexture, fragCoord, 0).xy; 20 | data0 = x_idx_i[${channel}]; 21 | }` 22 | 23 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.R32UI])); 24 | 25 | const innerKernel = function (x) { 26 | /** end setup uploads for kernel values **/ 27 | gl.useProgram(shdr.program); 28 | gl.scissor(0, 0, n, m); 29 | 30 | gl.activeTexture(gl.TEXTURE0); 31 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 32 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uInputTexture'); 33 | gl.uniform1i(glVariable95, 0); 34 | 35 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 36 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 37 | return { 38 | texture: shdr.outTextures[0], 39 | type: 'ArrayTexture(1)', 40 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m, gl.UNSIGNED_INT, 1, gl.RED_UNSIGNED) 41 | }; 42 | }; 43 | return innerKernel; 44 | 45 | } 46 | 47 | export default createGetChannelKernel; -------------------------------------------------------------------------------- /lib/kernels/ordering.js: -------------------------------------------------------------------------------- 1 | import { createOutTextures, fullTexShader } from "./shader.js"; 2 | import toArrayFunc from "./to-array.js"; 3 | 4 | function createEncodeKernel(gl, n, m) { 5 | const fragSrc = 6 | `#version 300 es 7 | precision highp float; 8 | precision highp int; 9 | precision highp sampler2D; 10 | precision highp sampler2DArray; 11 | 12 | uniform highp sampler2D uInputVals; 13 | uniform uint uGroupMask; 14 | 15 | out float data; 16 | 17 | void main(void) { 18 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 19 | float val = texelFetch(uInputVals, fragCoord, 0)[0]; 20 | data = uintBitsToFloat((floatBitsToUint(val) & (~uGroupMask)) | uint(fragCoord.x)); 21 | }` 22 | 23 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m)); 24 | 25 | const innerKernel = function (x) { 26 | const uploadValue_group_mask = n-1; 27 | 28 | /** end setup uploads for kernel values **/ 29 | gl.useProgram(shdr.program); 30 | gl.scissor(0, 0, n, m); 31 | 32 | gl.activeTexture(gl.TEXTURE0); 33 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 34 | const glVariable95 = gl.getUniformLocation(shdr.program, 'uInputVals'); 35 | gl.uniform1i(glVariable95, 0); 36 | 37 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uGroupMask'); 38 | gl.uniform1ui(glVariable17, uploadValue_group_mask); 39 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 40 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 41 | return { 42 | texture: shdr.outTextures[0], 43 | type: 'ArrayTexture(1)', 44 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m) 45 | }; 46 | }; 47 | return innerKernel; 48 | } 49 | 50 | 51 | function createDecodeKernel(gl, n, m) { 52 | // TODO: make this work with uint16 output instead of uint32 53 | 54 | const fragSrc = 55 | `#version 300 es 56 | precision highp float; 57 | precision highp int; 58 | precision highp sampler2D; 59 | precision highp sampler2DArray; 60 | 61 | uniform sampler2D aInputEncoding; 62 | uniform uint uGroupMask; 63 | 64 | out uint idx; 65 | 66 | void main(void) { 67 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 68 | float val = texelFetch(aInputEncoding, fragCoord, 0)[0]; 69 | idx = (floatBitsToUint(val) & uGroupMask); 70 | }` 71 | 72 | let shdr = fullTexShader(gl, fragSrc, n, m, createOutTextures(gl, n, m, [gl.R32UI])); 73 | 74 | const innerKernel = function (x) { 75 | const uploadValue_group_mask = n-1; 76 | 77 | /** end setup uploads for kernel values **/ 78 | gl.useProgram(shdr.program); 79 | gl.scissor(0, 0, n, m); 80 | 81 | gl.activeTexture(gl.TEXTURE0); 82 | gl.bindTexture(gl.TEXTURE_2D, x.texture); 83 | const glVariable95 = gl.getUniformLocation(shdr.program, 'aInputEncoding'); 84 | gl.uniform1i(glVariable95, 0); 85 | 86 | const glVariable17 = gl.getUniformLocation(shdr.program, 'uGroupMask'); 87 | gl.uniform1ui(glVariable17, uploadValue_group_mask); 88 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 89 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 90 | return { 91 | texture: shdr.outTextures[0], 92 | type: 'ArrayTexture(1)', 93 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m, gl.UNSIGNED_INT, 1, gl.RGBA_INTEGER) 94 | }; 95 | }; 96 | return innerKernel; 97 | } 98 | 99 | export { createEncodeKernel, createDecodeKernel }; -------------------------------------------------------------------------------- /lib/kernels/permute.js: -------------------------------------------------------------------------------- 1 | import { createOutTextures, fullTexShader } from "./shader.js"; 2 | import toArrayFunc from "./to-array.js"; 3 | import endianNess from "./endian.js"; 4 | 5 | // unpackRGBA8: set to true to unpack the output in terms of an RGBA8 6 | // rather than a single R32 7 | function createPermuteKernel(gl, n, m, unpackRGBA8=false) { 8 | let endian = endianNess(); 9 | 10 | let assignSrc; 11 | let outType; 12 | let internalFormat; 13 | 14 | if (unpackRGBA8) { 15 | switch(endian){ 16 | case 'le': 17 | assignSrc = ` 18 | perm_idx[0] = 0.003921568f*float(uint(0xff) & (out_idx)); 19 | perm_idx[1] = 0.003921568f*float(uint(0xff) & (out_idx >> 8)); 20 | perm_idx[2] = 0.003921568f*float(uint(0xff) & (out_idx >> 16)); 21 | perm_idx[3] = 0.003921568f*float(uint(0xff) & (out_idx >> 24));`; 22 | break; 23 | 24 | case 'be': 25 | assignSrc = ` 26 | perm_idx[0] = 0.003921568f*float(uint(0xff) & (out_idx >> 24)); 27 | perm_idx[1] = 0.003921568f*float(uint(0xff) & (out_idx >> 16)); 28 | perm_idx[2] = 0.003921568f*float(uint(0xff) & (out_idx >> 8)); 29 | perm_idx[3] = 0.003921568f*float(uint(0xff) & (out_idx));`; 30 | break; 31 | 32 | case 'unknown': 33 | default: 34 | throw "Trying to use unpackRGBA8 but cannot determine system endianness; please set unpackRGBA8=false." 35 | } 36 | internalFormat = gl.RGBA8; 37 | outType = 'vec4'; 38 | } else { 39 | assignSrc = `perm_idx = out_idx;` 40 | internalFormat = gl.R32UI; 41 | outType = 'uint'; 42 | } 43 | 44 | const fragSrc = 45 | `#version 300 es 46 | precision highp float; 47 | precision highp int; 48 | precision highp sampler2D; 49 | precision highp sampler2DArray; 50 | 51 | uniform int group_size; 52 | uniform highp usampler2D perm_inner_idx; 53 | uniform highp usampler2D perm_outer_idx; 54 | 55 | out ${outType} perm_idx; 56 | 57 | void main(void) { 58 | int inner_idx = int(gl_FragCoord.x); 59 | int outer_idx = int(gl_FragCoord.y); 60 | 61 | int idx = int(texelFetch(perm_outer_idx, ivec2(0, outer_idx), 0)[0]); 62 | int val = int(texelFetch(perm_inner_idx, ivec2(inner_idx, idx), 0)[0]); 63 | 64 | uint out_idx = uint(group_size)*uint(idx) + uint(val); 65 | ${assignSrc} 66 | }` 67 | 68 | let outTextures = createOutTextures(gl, n, m, [internalFormat]); 69 | let shdr = fullTexShader(gl, fragSrc, n, m, outTextures); 70 | 71 | const innerKernel = function (groupInnerIdx, groupOuterIdx) { 72 | /** end setup uploads for kernel values **/ 73 | gl.useProgram(shdr.program); 74 | gl.scissor(0, 0, n, m); 75 | 76 | gl.activeTexture(gl.TEXTURE2); 77 | gl.bindTexture(gl.TEXTURE_2D, groupOuterIdx.texture); 78 | const permOuterIdxLoc = gl.getUniformLocation(shdr.program, 'perm_outer_idx'); 79 | gl.uniform1i(permOuterIdxLoc, 2); 80 | 81 | gl.activeTexture(gl.TEXTURE0); 82 | gl.bindTexture(gl.TEXTURE_2D, groupInnerIdx.texture); 83 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 84 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 85 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 86 | // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 87 | const permInnerIdxLoc = gl.getUniformLocation(shdr.program, 'perm_inner_idx'); 88 | gl.uniform1i(permInnerIdxLoc, 0); 89 | 90 | const groupSizeLoc = gl.getUniformLocation(shdr.program, 'group_size'); 91 | gl.uniform1i(groupSizeLoc, n); 92 | 93 | gl.bindFramebuffer(gl.FRAMEBUFFER, shdr.frameBuffer); 94 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 95 | 96 | let glFormat = unpackRGBA8 ? gl.RGBA : gl.RGBA_INTEGER; 97 | let glType = unpackRGBA8 ? gl.UNSIGNED_BYTE : gl.UNSIGNED_INT; 98 | let nChannels = unpackRGBA8 ? 4 : 1; 99 | 100 | return { 101 | texture: shdr.outTextures[0], 102 | type: 'ArrayTexture(1)', 103 | //buffer: buffer, 104 | framebuffer: shdr.frameBuffer, 105 | toArray: () => toArrayFunc(gl, shdr.frameBuffer, shdr.outTextures[0], n, m, glType, nChannels, glFormat, unpackRGBA8) 106 | }; 107 | }; 108 | return innerKernel; 109 | } 110 | 111 | export default createPermuteKernel; -------------------------------------------------------------------------------- /lib/kernels/shader.js: -------------------------------------------------------------------------------- 1 | function createOutTextures(gl, n, m, internalFormats=[gl.R32F]) 2 | { 3 | let outTextures = []; 4 | 5 | for (var i=0; i < internalFormats.length; ++i){ 6 | outTextures[i] = gl.createTexture(); 7 | 8 | gl.activeTexture(gl.TEXTURE0+i); 9 | gl.bindTexture(gl.TEXTURE_2D, outTextures[i]); 10 | 11 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 12 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 13 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 14 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 15 | 16 | gl.texStorage2D(gl.TEXTURE_2D, 1, internalFormats[i], n, m); 17 | gl.bindTexture(gl.TEXTURE_2D, null); 18 | 19 | } 20 | 21 | return outTextures; 22 | } 23 | 24 | function fullTexShader(gl, fragSrc, n, m, outTextures){ 25 | const glExtColBuf = gl.getExtension('EXT_color_buffer_float'); 26 | const glWebglDepthTex = gl.getExtension('WEBGL_depth_texture'); 27 | const glWebColBufFloat = gl.getExtension('WEBGL_color_buffer_float'); 28 | 29 | //gl.enable(gl.SCISSOR_TEST); 30 | //gl.viewport(0, 0, n, m); 31 | const vertexShader = gl.createShader(gl.VERTEX_SHADER); 32 | gl.shaderSource(vertexShader, `#version 300 es 33 | precision lowp float; 34 | precision lowp int; 35 | precision lowp sampler2D; 36 | 37 | in vec2 aPos; 38 | 39 | void main(void) { 40 | gl_Position = vec4((aPos + vec2(1)) + vec2(-1), 0, 1); 41 | }` 42 | ); 43 | gl.compileShader(vertexShader); 44 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 45 | gl.shaderSource(fragmentShader, fragSrc); 46 | 47 | gl.compileShader(fragmentShader); 48 | const vertCompileStat = gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS); 49 | const fragCompileStat = gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS); 50 | const shaderProgram = gl.createProgram(); 51 | gl.attachShader(shaderProgram, vertexShader); 52 | gl.attachShader(shaderProgram, fragmentShader); 53 | gl.linkProgram(shaderProgram); 54 | const framebuf = gl.createFramebuffer(); 55 | const glVariable9 = gl.createBuffer(); 56 | gl.bindBuffer(gl.ARRAY_BUFFER, glVariable9); 57 | gl.bufferData(gl.ARRAY_BUFFER, 64, gl.STATIC_DRAW); 58 | const glVariable10 = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); 59 | gl.bufferSubData(gl.ARRAY_BUFFER, 0, glVariable10); 60 | const glVariable11 = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); 61 | gl.bufferSubData(gl.ARRAY_BUFFER, 32, glVariable11); 62 | const glVariable12 = gl.getAttribLocation(shaderProgram, 'aPos'); 63 | gl.enableVertexAttribArray(glVariable12); 64 | gl.vertexAttribPointer(glVariable12, 2, gl.FLOAT, false, 0, 0); 65 | gl.useProgram(shaderProgram); 66 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuf); 67 | gl.activeTexture(gl.TEXTURE1); 68 | 69 | for (var i=0; i < outTextures.length; ++i){ 70 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0+i, gl.TEXTURE_2D, outTextures[i], 0); 71 | } 72 | 73 | return { 74 | program: shaderProgram, 75 | compile_status: { 76 | vertex: vertCompileStat, 77 | fragment: fragCompileStat 78 | }, 79 | frameBuffer: framebuf, 80 | outTextures: outTextures 81 | }; 82 | } 83 | 84 | export { 85 | createOutTextures, 86 | fullTexShader 87 | }; -------------------------------------------------------------------------------- /lib/kernels/to-array.js: -------------------------------------------------------------------------------- 1 | function toArrayFunc(gl, frameBuffer, texture, n, m, texType=gl.FLOAT, nChannels=1, rpFormat=gl.RGBA, packu32=false){ 2 | function erectOutput(array, width){ 3 | if((texType == gl.UNSIGNED_BYTE) && ((rpFormat == gl.RGBA) || (rpFormat == gl.RG)) && (packu32)){ 4 | if (nChannels == 4){ 5 | return new Uint32Array(array); 6 | } 7 | if (nChannels == 2) { 8 | return new Uint16Array(array); 9 | } 10 | throw `Specified number of channels (${nChannels}) unimplemented.`; 11 | } else { 12 | let xResults; 13 | if (texType == gl.FLOAT){ 14 | xResults = new Float32Array(width*nChannels); 15 | } 16 | if (texType == gl.UNSIGNED_INT){ 17 | xResults = new Uint32Array(width*nChannels); 18 | } 19 | 20 | for (let c = 0; c < nChannels; c++) { 21 | let i = 0; 22 | for (let x = 0; (x < nChannels*width); x+=nChannels) { 23 | xResults[x+c] = array[i+c]; 24 | i += 4; 25 | }; 26 | }; 27 | return xResults; 28 | } 29 | 30 | }; 31 | function renderRawOutput() { 32 | gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); 33 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 34 | 35 | if(packu32){ 36 | // Special case for RGBA8UI unpacking, where we unpack to a uint32 37 | let buf = new ArrayBuffer(n * m * nChannels); 38 | let bufu8 = new Uint8Array(buf); 39 | gl.readPixels(0, 0, n, m, rpFormat, texType, bufu8); 40 | return buf; 41 | } 42 | 43 | let result; 44 | if(texType == gl.FLOAT){ 45 | result = new Float32Array(n * m * 4 * nChannels); 46 | } 47 | if(texType == gl.UNSIGNED_INT){ 48 | rpFormat = gl.RGBA_INTEGER; 49 | result = new Uint32Array(n * m * 4 * nChannels); 50 | } 51 | if(texType == gl.UNSIGNED_SHORT){ 52 | result = new Uint16Array(n * m * 4 * nChannels); 53 | } 54 | gl.readPixels(0, 0, n, m, rpFormat, texType, result); 55 | return result; 56 | } 57 | function toArray() { 58 | // const ext1 = gl.getExtension('EXT_color_buffer_float'); 59 | // const ext2 = gl.getExtension('WEBGL_draw_buffers'); 60 | 61 | return erectOutput(renderRawOutput(), n * m); 62 | } 63 | return toArray(); 64 | } 65 | 66 | /* Create kernel that expands a RG32F output texture to RGBA32F */ 67 | function createOutputExpandKernel(gl, n, m){ 68 | 69 | } 70 | 71 | export default toArrayFunc; -------------------------------------------------------------------------------- /lib/kernels/to-texture.js: -------------------------------------------------------------------------------- 1 | import toArrayFunc from "./to-array.js"; 2 | 3 | function toTexture(gl, x, n, m, elType='float', nChannels=1, isBuffer=false) { 4 | if (elType == 'integer' || elType == 'unsigned'){ 5 | const ext = gl.getExtension('OES_element_index_uint'); 6 | } 7 | 8 | if (!(Number.isInteger(nChannels)) || (nChannels < 1) || (nChannels > 4)){ 9 | throw `nChannels must be and integer in the range [1, 4]; ${nChannels} given.`; 10 | } 11 | 12 | if (!(['float', 'unsigned', 'integer'].includes(elType))){ 13 | throw `elType must be one of: float, unsigned, or integer; ${elType} given.`; 14 | } 15 | 16 | let suffix = {'float': 'F', 'integer': 'I', 'unsigned': 'UI'}[elType]; 17 | let texTyp = {'float': 'FLOAT', 'integer': 'INT', 'unsigned': 'UNSIGNED_INT'}[elType]; 18 | let format = ['RED', 'RG', 'RGB', 'RGBA'][nChannels-1]; 19 | let internalFormat = nChannels==1 ? `R32${suffix}` : `${format}32${suffix}` 20 | let glFormat = elType=='float' ? gl[format] : gl[`${format}_INTEGER`]; 21 | let toArrayGLFormat = elType=='float' ? gl.RGBA : gl.RGBA_INTEGER; 22 | 23 | const inTexture = gl.createTexture(); 24 | gl.activeTexture(gl.TEXTURE0); 25 | 26 | if (isBuffer){ 27 | let buffer = gl.createBuffer(); 28 | 29 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 30 | gl.bufferData(gl.ARRAY_BUFFER, x, gl.STREAM_COPY); 31 | 32 | gl.bindTexture(gl.TEXTURE_BUFFER, inTexture); 33 | 34 | gl.texBuffer(gl.TEXTURE_BUFFER, gl[internalFormat], buffer); 35 | } else { 36 | gl.bindTexture(gl.TEXTURE_2D, inTexture); 37 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 38 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 39 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 40 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 41 | 42 | gl.bindTexture(gl.TEXTURE_2D, inTexture); 43 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); 44 | gl.texImage2D(gl.TEXTURE_2D, 0, gl[internalFormat], n, m, 0, glFormat, gl[texTyp], x); 45 | } 46 | 47 | return { 48 | texture: inTexture, 49 | type: 'ArrayTexture(1)', 50 | toArray: () => toArrayFunc(gl, gl.createFramebuffer(), inTexture, n, m, gl[texTyp], nChannels, toArrayGLFormat) 51 | }; 52 | } 53 | 54 | export default toTexture; -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /lib/pipeline.js: -------------------------------------------------------------------------------- 1 | import toTexture from "./kernels/to-texture.js"; 2 | import createGetChannelKernel from "./kernels/get-channel.js"; 3 | import { createEncodeKernel, createDecodeKernel } from "./kernels/ordering.js"; 4 | import { createDepthKernel, createDepthIndexedKernel } from "./kernels/depth.js"; 5 | import { createBitonicKernel, createFullBitonicKernel, createFullEvenOddKernel, createTexturePermuteKernel, createSwapTextureKernel } from "./kernels/bitonic.js"; 6 | 7 | import kdTree, { viewDepthSort, depthFirstCollect } from './kd-tree.js'; 8 | import IndexedPointArray, { permutePointArray, permuteArray } from './pointarray.js'; 9 | import bitonicSort, { sortSteps, applySteps } from './bitonic.js'; 10 | 11 | function idxPointArrayFromVerts(verts, n_verts) { 12 | let x = new Float32Array(n_verts); 13 | let y = new Float32Array(n_verts); 14 | let z = new Float32Array(n_verts); 15 | for (var i=0; i pipeline.sortSteps.length) { 159 | stopStep = pipeline.sortSteps.length; 160 | } 161 | pipeline.depthsIndexed = applySteps(pipeline.bitKer, pipeline.depthsIndexed, pipeline.sortSteps, pipeline.stepsDone, stopStep); 162 | pipeline.stepsDone = stopStep % pipeline.sortSteps.length; 163 | 164 | // periodically rearrange vertices for better fetch order 165 | if (stopStep == pipeline.sortSteps.length) { 166 | pipeline.stepsDone = 0; 167 | 168 | let indices = pipeline.getChannelKer(pipeline.depthsIndexed); 169 | 170 | vertexTextures.position = pipeline.swapPosKer(pipeline.permKerPos(indices, vertexTextures.position)); 171 | vertexTextures.color = pipeline.swapColorKer(pipeline.permKerColor(indices, vertexTextures.color)); 172 | vertexTextures.covP1 = pipeline.swapCovP1Ker(pipeline.permKerCovP1(indices, vertexTextures.covP1)); 173 | vertexTextures.covP2 = pipeline.swapCovP2Ker(pipeline.permKerCovP2(indices, vertexTextures.covP2)); 174 | } 175 | 176 | } 177 | 178 | export { 179 | createPipeline, 180 | applyPipeline, 181 | createFullSortPipeline, 182 | applyFullSortPipeline, 183 | toTexture, 184 | createGetChannelKernel, 185 | createBitonicKernel, 186 | createFullBitonicKernel, 187 | bitonicSort 188 | }; 189 | -------------------------------------------------------------------------------- /lib/pointarray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class representing an array of indexed points. 3 | * @class IndexedPointArray 4 | */ 5 | export default class IndexedPointArray { 6 | /** 7 | * Creates an instance of IndexedPointArray. 8 | * @constructor 9 | * @param {Array} x - The x-coordinates of the points. 10 | * @param {Array} y - The y-coordinates of the points. 11 | * @param {Array} z - The z-coordinates of the points. 12 | */ 13 | constructor(x, y, z) { 14 | this.x = x; 15 | this.y = y; 16 | this.z = z; 17 | this.idx = new Uint32Array(x.length); 18 | for (var i=0; i= idx.length)) 36 | { 37 | throw new Error(`kth element index out of bounds: ${k}`); 38 | } 39 | 40 | while (right > left) { 41 | if (right - left > 600) { 42 | var n = right - left + 1; 43 | var m = k - left + 1; 44 | var z = Math.log(n); 45 | var s = 0.5 * Math.exp(2 * z / 3); 46 | var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); 47 | var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); 48 | var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); 49 | quickselectStep(arr, idx, k, newLeft, newRight, compare); 50 | } 51 | 52 | var t = arr[idx[k]]; 53 | var i = left; 54 | var j = right; 55 | 56 | swap(idx, left, k); 57 | if (compare(arr[idx[right]], t) > 0) swap(idx, left, right); 58 | 59 | while (i < j) { 60 | swap(idx, i, j); 61 | i++; 62 | j--; 63 | while (compare(arr[idx[i]], t) < 0) i++; 64 | while (compare(arr[idx[j]], t) > 0) j--; 65 | } 66 | 67 | if (compare(arr[idx[left]], t) === 0) swap(idx, left, j); 68 | else { 69 | j++; 70 | swap(idx, j, right); 71 | } 72 | 73 | if (j <= k) left = j + 1; 74 | if (k <= j) right = j - 1; 75 | } 76 | 77 | return arr[idx[k]]; 78 | } 79 | 80 | function swap(idx, i, j) { 81 | var tmp = idx[i]; 82 | idx[i] = idx[j]; 83 | idx[j] = tmp; 84 | } 85 | 86 | function defaultCompare(a, b) { 87 | return a < b ? -1 : a > b ? 1 : 0; 88 | } 89 | -------------------------------------------------------------------------------- /lib/rendering/sphere.js: -------------------------------------------------------------------------------- 1 | const APOS_ATTRIB_LOC = 1; 2 | 3 | function createSphereRenderProgram(gl) { 4 | // Vertex shader source code 5 | const vertexShaderSource = `#version 300 es 6 | 7 | in vec3 aPosition; 8 | uniform vec4 aColor; 9 | 10 | uniform mat4 uViewProj; 11 | uniform float uRadius; 12 | uniform vec3 uLookAtPoint; 13 | 14 | out vec4 vColor; 15 | 16 | void main() { 17 | gl_Position = uViewProj * vec4((uRadius*aPosition) + uLookAtPoint, 1.0); 18 | gl_PointSize = 4.0; 19 | 20 | vColor = aColor; 21 | } 22 | `; 23 | 24 | // Fragment shader source code 25 | const fragmentShaderSource = `#version 300 es 26 | precision mediump float; 27 | 28 | in vec4 vColor; 29 | layout(location=0) out vec4 fragColor; 30 | 31 | void main() { 32 | fragColor = vColor; 33 | } 34 | `; 35 | 36 | // Initialize shaders 37 | const vertexShader = gl.createShader(gl.VERTEX_SHADER); 38 | gl.shaderSource(vertexShader, vertexShaderSource); 39 | gl.compileShader(vertexShader); 40 | 41 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 42 | gl.shaderSource(fragmentShader, fragmentShaderSource); 43 | gl.compileShader(fragmentShader); 44 | 45 | const program = gl.createProgram(); 46 | gl.attachShader(program, vertexShader); 47 | gl.attachShader(program, fragmentShader); 48 | 49 | gl.bindAttribLocation(program, APOS_ATTRIB_LOC, 'aPosition'); 50 | gl.linkProgram(program); 51 | 52 | return program; 53 | } 54 | 55 | // Function to create a circle's vertices in a specified plane 56 | function createCircleVertices(radius, segments, plane) { 57 | const vertices = []; 58 | const angleStep = 2 * Math.PI / segments; 59 | 60 | for (let i = 0; i <= segments; i++) { 61 | let angle = i * angleStep; 62 | let x = radius * Math.cos(angle); 63 | let y = radius * Math.sin(angle); 64 | 65 | switch (plane) { 66 | case 'xy': 67 | vertices.push(x, y, 0.0); 68 | break; 69 | case 'xz': 70 | vertices.push(x, 0.0, y); 71 | break; 72 | case 'yz': 73 | vertices.push(0.0, x, y); 74 | break; 75 | } 76 | } 77 | 78 | return vertices; 79 | } 80 | 81 | function createDot(){ 82 | return [0, 0, 0]; 83 | } 84 | 85 | function createCrosshairVertices(length, axis) { 86 | let vertices = []; 87 | switch (axis) { 88 | case 'x': 89 | vertices = [-length, 0, 0, length, 0, 0]; 90 | break; 91 | case 'y': 92 | vertices = [0, -length, 0, 0, length, 0]; 93 | break; 94 | case 'z': 95 | vertices = [0, 0, -length, 0, 0, length]; 96 | break; 97 | } 98 | return vertices; 99 | 100 | } 101 | 102 | function createSphereCircles(circleRadius, circleSegments) { 103 | const xCircleVertices = createCircleVertices(circleRadius, circleSegments, 'yz'); 104 | const yCircleVertices = createCircleVertices(circleRadius, circleSegments, 'xz'); 105 | const zCircleVertices = createCircleVertices(circleRadius, circleSegments, 'xy'); 106 | 107 | const xCrosshairVertices = createCrosshairVertices(0.1*circleRadius, 'x'); 108 | const yCrosshairVertices = createCrosshairVertices(0.1*circleRadius, 'y'); 109 | const zCrosshairVertices = createCrosshairVertices(0.1*circleRadius, 'z'); 110 | 111 | const dot = createDot(); 112 | 113 | return [ 114 | xCircleVertices, 115 | yCircleVertices, 116 | zCircleVertices, 117 | xCrosshairVertices, 118 | yCrosshairVertices, 119 | zCrosshairVertices, 120 | dot]; 121 | } 122 | 123 | // Render function (continued) 124 | function renderSphereCircles(gl, program, verts) { 125 | 126 | // Create vertex buffers 127 | const vertexBuffers = []; 128 | for (let i = 0; i < 7; i++) { 129 | vertexBuffers.push(gl.createBuffer()); 130 | } 131 | 132 | // Colors 133 | const colors = [ 134 | [1.0, 0.0, 0.0, 0.75], // Red with transparency 135 | [0.0, 1.0, 0.0, 0.75], // Green with transparency 136 | [0.0, 0.0, 1.0, 0.75], // Blue with transparency 137 | [1.0, 0.0, 0.0, 1.0], // Red 138 | [0.0, 1.0, 0.0, 1.0], // Green 139 | [0.0, 0.0, 1.0, 1.0], // Blue 140 | [1.0, 1.0, 1.0, 1.0] // White 141 | ]; 142 | 143 | // Get attribute locations 144 | const colorAttribLocation = gl.getUniformLocation(program, 'aColor'); 145 | 146 | // // X-axis circle (Red) 147 | gl.enableVertexAttribArray(APOS_ATTRIB_LOC); 148 | 149 | for (let i = 0; i < 7; i++) { 150 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffers[i]); 151 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts[i]), gl.STATIC_DRAW); 152 | gl.vertexAttribPointer(APOS_ATTRIB_LOC, 3, gl.FLOAT, false, 0, 0); 153 | gl.uniform4fv(colorAttribLocation, colors[i]); 154 | if (i == 6){ 155 | gl.drawArrays(gl.POINTS, 0, 1); 156 | } else { 157 | gl.drawArrays(gl.LINE_STRIP, 0, verts[i].length / 3); 158 | } 159 | } 160 | 161 | gl.disableVertexAttribArray(APOS_ATTRIB_LOC); 162 | } 163 | 164 | export {createSphereRenderProgram, createSphereCircles, renderSphereCircles}; 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /lib/rendering/vpshaders.js: -------------------------------------------------------------------------------- 1 | // permType can be 'kdtree' or 'full' 2 | function createRenderProgram(gl, permType='kdtree') { 3 | let samplerDefs 4 | let getTexCoord 5 | 6 | if (permType == 'full') { 7 | samplerDefs = ` 8 | ` 9 | 10 | getTexCoord = ` 11 | // get order index from texture 12 | ivec2 texCoord = ivec2(inner_idx, outer_idx); 13 | ` 14 | } else { 15 | samplerDefs = ` 16 | uniform highp usampler2D perm_inner_idx; 17 | uniform highp usampler2D perm_outer_idx; 18 | ` 19 | 20 | getTexCoord = ` 21 | // get order index from texture 22 | int idx = int(texelFetch(perm_outer_idx, ivec2(0, outer_idx), 0)[0]); 23 | int val = int(texelFetch(perm_inner_idx, ivec2(inner_idx, idx), 0)[0]); 24 | 25 | // Pull vertex attributes from textures 26 | ivec2 texCoord = ivec2(val, idx); 27 | ` 28 | } 29 | 30 | let vertShaderSrc = `#version 300 es 31 | 32 | ${samplerDefs} 33 | 34 | uniform sampler2D positionTexture; // Texture containing position data 35 | uniform sampler2D colorTexture; // Texture containing color data 36 | uniform sampler2D covP1Texture; // Texture containing covP1 data 37 | uniform sampler2D covP2Texture; // Texture containing covP2 data 38 | uniform ivec2 textureSize; // Size of the textures 39 | 40 | out vec2 vPosition; // Output position in pixel coordinates. 41 | out float vDepth; 42 | flat out vec4 vColor; 43 | flat out vec3 vInvCov2d; 44 | 45 | uniform mat4 uViewProj; 46 | uniform mat4 uView; 47 | uniform vec2 uViewportScale; 48 | 49 | 50 | // Forward version of 2D covariance matrix computation 51 | // t: transformed center 52 | vec3 computeCov2D(const vec3 t, const vec2 focal, const vec4 cov3dp1, const vec2 cov3dp2, const mat4 viewmatrix) 53 | { 54 | mat3 J = mat3( 55 | focal.x / t.z, 0.0f, -(focal.x * t.x) / (t.z * t.z), 56 | 0.0f, focal.y / t.z, -(focal.y * t.y) / (t.z * t.z), 57 | 0, 0, 0); 58 | 59 | mat3 W = mat3( 60 | viewmatrix[0][0], viewmatrix[1][0], viewmatrix[2][0], 61 | viewmatrix[0][1], viewmatrix[1][1], viewmatrix[2][1], 62 | viewmatrix[0][2], viewmatrix[1][2], viewmatrix[2][2]); 63 | 64 | mat3 T = W * J; 65 | 66 | mat3 Vrk = mat3( 67 | cov3dp1[0], cov3dp1[3], cov3dp2[0], 68 | cov3dp1[3], cov3dp1[1], cov3dp2[1], 69 | cov3dp2[0], cov3dp2[1], cov3dp1[2]); 70 | 71 | mat3 cov = transpose(T) * Vrk * T; 72 | 73 | // Apply low-pass filter: every Gaussian should be at least 74 | // one pixel wide/high. Discard 3rd row and column. 75 | return vec3(cov[0][0] + 0.3, cov[0][1], cov[1][1] + 0.3); 76 | } 77 | 78 | vec3 invCov2d(vec3 cov2d){ 79 | float det = (cov2d.x * cov2d.z - cov2d.y * cov2d.y) + 1e-9; 80 | // if (det == 0.0f) 81 | // return; 82 | float det_inv = 1.0 / det; 83 | return vec3(cov2d.z * det_inv, -cov2d.y * det_inv, cov2d.x * det_inv); 84 | } 85 | 86 | void main() { 87 | // The fragment shader needs to do its computations in pixel coords. 88 | // Here, in addition to computing clip space position, 89 | // we also precompute the apparent size parameters of the gaussian in pixel coords. 90 | // This is to reduce some of the workload of the fragment shader. 91 | 92 | 93 | // Determine the index into the ordering texture for the current vertex 94 | int index = gl_VertexID; 95 | int inner_idx = index % textureSize.x; 96 | int outer_idx = index / textureSize.x; 97 | 98 | ${getTexCoord} 99 | 100 | vec3 position = texelFetch(positionTexture, texCoord, 0).xyz; 101 | vec4 color = texelFetch(colorTexture, texCoord, 0); 102 | vec4 covP1 = texelFetch(covP1Texture, texCoord, 0).xyzw; 103 | vec2 covP2 = texelFetch(covP2Texture, texCoord, 0).xy; 104 | 105 | vec4 clipPosition = uViewProj * vec4(position, 1.0); 106 | 107 | vec2 ndcPosition = clipPosition.xy / clipPosition.w; 108 | 109 | vec2 pixelPosition = 0.5 * (1.0 + ndcPosition) * uViewportScale; 110 | 111 | vPosition = pixelPosition; 112 | vDepth = clipPosition.z / clipPosition.w; 113 | vColor = color; // TODO 114 | vec4 t = uView*vec4(position, 1.0); 115 | float focal = 0.75*uViewportScale.y; 116 | vec3 vCov2d = computeCov2D(t.xyz, vec2(focal, focal), covP1, covP2, uView); 117 | vInvCov2d = invCov2d(vCov2d); 118 | 119 | gl_Position = clipPosition; 120 | gl_PointSize = 5.0*sqrt(max(vCov2d.x, vCov2d.z)); // 5-sigma 121 | } 122 | ` 123 | 124 | let fragShaderSrc = `#version 300 es 125 | precision highp float; 126 | 127 | layout(std140, column_major) uniform; 128 | 129 | in vec2 vPosition; 130 | in float vDepth; 131 | flat in vec4 vColor; 132 | flat in vec3 vInvCov2d; 133 | 134 | layout(location=0) out vec4 fragColor; 135 | 136 | float gaussian(vec2 d){ 137 | float power = -0.5 * (vInvCov2d.x * d.x * d.x + vInvCov2d.z * d.y * d.y) - vInvCov2d.y * d.x * d.y; 138 | if (power > 0.0) 139 | discard; 140 | return min(0.99f, 1.0f * exp(power)); 141 | } 142 | 143 | void main() { 144 | vec4 baseColor = vColor; 145 | 146 | vec2 d = gl_FragCoord.xy - vPosition; 147 | float gaussian_alpha = gaussian(d); 148 | 149 | vec4 color = vec4(baseColor.rgb, gaussian_alpha*vColor.a); 150 | 151 | fragColor = vec4(color.rgb, color.a); 152 | }` 153 | 154 | const vertexShader = gl.createShader(gl.VERTEX_SHADER); 155 | gl.shaderSource(vertexShader, vertShaderSrc); 156 | gl.compileShader(vertexShader); 157 | 158 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 159 | gl.shaderSource(fragmentShader, fragShaderSrc); 160 | gl.compileShader(fragmentShader); 161 | 162 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 163 | console.error(gl.getShaderInfoLog(vertexShader)); 164 | } 165 | 166 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 167 | console.error(gl.getShaderInfoLog(fragmentShader)); 168 | } 169 | 170 | const shaderProgram = gl.createProgram(); 171 | gl.attachShader(shaderProgram, vertexShader); 172 | gl.attachShader(shaderProgram, fragmentShader); 173 | gl.linkProgram(shaderProgram); 174 | 175 | return shaderProgram; 176 | } 177 | 178 | export default createRenderProgram; -------------------------------------------------------------------------------- /lib/splatfile.js: -------------------------------------------------------------------------------- 1 | function loadSplatData(fileBuffer) { 2 | // 6*4 + 4 + 4 = 8*4 3 | // XYZ - Position (Float32) 4 | // XYZ - Scale (Float32) 5 | // RGBA - colors (uint8) 6 | // IJKL - quaternion/rot (uint8) 7 | 8 | const rowLength = 3 * 4 + 3 * 4 + 4 + 4; 9 | 10 | let splatData = new Uint8Array(fileBuffer); 11 | let buffer = splatData.buffer; 12 | let vertexCount = Math.floor(splatData.length / rowLength); 13 | 14 | const fb = new Float32Array(buffer); 15 | const ub = new Uint8Array(buffer); 16 | 17 | let positions = new Float32Array(3 * vertexCount); 18 | let scales = new Float32Array(3 * vertexCount); 19 | let colors = new Float32Array(4 * vertexCount); 20 | let rotors = new Float32Array(4 * vertexCount); 21 | 22 | for (var i = 0; i < vertexCount; i++) { 23 | positions[3 * i + 0] = fb[8 * i + 0]; 24 | positions[3 * i + 1] = fb[8 * i + 1]; 25 | positions[3 * i + 2] = fb[8 * i + 2]; 26 | 27 | scales[3 * i + 0] = fb[8 * i + 3]; 28 | scales[3 * i + 1] = fb[8 * i + 4]; 29 | scales[3 * i + 2] = fb[8 * i + 5]; 30 | 31 | colors[4 * i + 0] = 0.003921568 * ub[32 * i + 24]; 32 | colors[4 * i + 1] = 0.003921568 * ub[32 * i + 25]; 33 | colors[4 * i + 2] = 0.003921568 * ub[32 * i + 26]; 34 | colors[4 * i + 3] = 0.003921568 * ub[32 * i + 27]; 35 | 36 | rotors[4 * i + 0] = 0.0078125 * (ub[32 * i + 28] - 128); 37 | rotors[4 * i + 1] = 0.0078125 * (ub[32 * i + 29] - 128); 38 | rotors[4 * i + 2] = 0.0078125 * (ub[32 * i + 30] - 128); 39 | rotors[4 * i + 3] = 0.0078125 * (ub[32 * i + 31] - 128); 40 | } 41 | 42 | return { 43 | positions: positions, 44 | scales: scales, 45 | colors: colors, 46 | rotors: rotors 47 | }; 48 | } 49 | 50 | export default loadSplatData; -------------------------------------------------------------------------------- /lib/utils/linalg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transpose the values of a mat3 3 | * 4 | * @param {mat3} out the receiving matrix 5 | * @param {mat3} a the source matrix 6 | * @returns {mat3} out 7 | */ 8 | function mat3transpose(out, a) { 9 | var a01 = a[1], a02 = a[2], a12 = a[5]; 10 | 11 | out[1] = a[3]; 12 | out[2] = a[6]; 13 | out[5] = a[7]; 14 | 15 | out[3] = a01; 16 | out[6] = a02; 17 | out[7] = a12; 18 | 19 | out[0] = a[0]; 20 | out[4] = a[4]; 21 | out[8] = a[8]; 22 | 23 | return out; 24 | }; 25 | 26 | /** 27 | * Generates a perspective projection matrix with the given bounds 28 | * 29 | * @param {mat4} out mat4 frustum matrix will be written into 30 | * @param {number} fovy Vertical field of view in radians 31 | * @param {number} aspect Aspect ratio. typically viewport width/height 32 | * @param {number} near Near bound of the frustum 33 | * @param {number} far Far bound of the frustum 34 | * @returns {mat4} out 35 | */ 36 | function mat4perspective(out, fovy, aspect, near, far) { 37 | var f = 1.0 / Math.tan(fovy / 2), 38 | nf = 1 / (near - far); 39 | out[0] = f / aspect; 40 | out[1] = 0; 41 | out[2] = 0; 42 | out[3] = 0; 43 | out[4] = 0; 44 | out[5] = f; 45 | out[6] = 0; 46 | out[7] = 0; 47 | out[8] = 0; 48 | out[9] = 0; 49 | out[10] = (far + near) * nf; 50 | out[11] = -1; 51 | out[12] = 0; 52 | out[13] = 0; 53 | out[14] = (2 * far * near) * nf; 54 | out[15] = 0; 55 | return out; 56 | }; 57 | 58 | /** 59 | * Generates an orthographic projection matrix with the given bounds 60 | * 61 | * @param {mat4} out mat4 frustum matrix will be written into 62 | * @param {number} left Left bound of the frustum 63 | * @param {number} right Right bound of the frustum 64 | * @param {number} bottom Bottom bound of the frustum 65 | * @param {number} top Top bound of the frustum 66 | * @param {number} near Near bound of the frustum 67 | * @param {number} far Far bound of the frustum 68 | * @returns {mat4} out 69 | */ 70 | function mat4ortho(out, left, right, bottom, top, near, far) { 71 | var lr = 1 / (left - right), 72 | bt = 1 / (bottom - top), 73 | nf = 1 / (near - far); 74 | out[0] = -2 * lr; 75 | out[1] = 0; 76 | out[2] = 0; 77 | out[3] = 0; 78 | out[4] = 0; 79 | out[5] = -2 * bt; 80 | out[6] = 0; 81 | out[7] = 0; 82 | out[8] = 0; 83 | out[9] = 0; 84 | out[10] = 2 * nf; 85 | out[11] = 0; 86 | out[12] = (left + right) * lr; 87 | out[13] = (top + bottom) * bt; 88 | out[14] = (far + near) * nf; 89 | out[15] = 1; 90 | return out; 91 | } 92 | 93 | /** 94 | * Generates a view matrix with the given eye position, focal point, and up axis 95 | * 96 | * @param {mat4} out mat4 frustum matrix will be written into 97 | * @param {vec3} eye Position of the viewer 98 | * @param {vec3} center Point the viewer is looking at 99 | * @param {vec3} up vec3 pointing up 100 | * @returns {mat4} out 101 | */ 102 | function mat4lookAt(out, eye, center, up) { 103 | const EPSILON = 0.000001; 104 | 105 | var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, 106 | eyex = eye[0], 107 | eyey = eye[1], 108 | eyez = eye[2], 109 | upx = up[0], 110 | upy = up[1], 111 | upz = up[2], 112 | centerx = center[0], 113 | centery = center[1], 114 | centerz = center[2]; 115 | 116 | if (Math.abs(eyex - centerx) < EPSILON && 117 | Math.abs(eyey - centery) < EPSILON && 118 | Math.abs(eyez - centerz) < EPSILON) { 119 | for (var i = 0; i < 16; ++i) { 120 | out[i] = 0; 121 | } 122 | out[0] = 1; 123 | out[5] = 1; 124 | out[10] = 1; 125 | out[15] = 1; 126 | return; 127 | } 128 | 129 | z0 = eyex - centerx; 130 | z1 = eyey - centery; 131 | z2 = eyez - centerz; 132 | 133 | len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); 134 | z0 *= len; 135 | z1 *= len; 136 | z2 *= len; 137 | 138 | x0 = upy * z2 - upz * z1; 139 | x1 = upz * z0 - upx * z2; 140 | x2 = upx * z1 - upy * z0; 141 | len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); 142 | if (!len) { 143 | x0 = 0; 144 | x1 = 0; 145 | x2 = 0; 146 | } else { 147 | len = 1 / len; 148 | x0 *= len; 149 | x1 *= len; 150 | x2 *= len; 151 | } 152 | 153 | y0 = z1 * x2 - z2 * x1; 154 | y1 = z2 * x0 - z0 * x2; 155 | y2 = z0 * x1 - z1 * x0; 156 | 157 | len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); 158 | if (!len) { 159 | y0 = 0; 160 | y1 = 0; 161 | y2 = 0; 162 | } else { 163 | len = 1 / len; 164 | y0 *= len; 165 | y1 *= len; 166 | y2 *= len; 167 | } 168 | 169 | out[0] = x0; 170 | out[1] = y0; 171 | out[2] = z0; 172 | out[3] = 0; 173 | out[4] = x1; 174 | out[5] = y1; 175 | out[6] = z1; 176 | out[7] = 0; 177 | out[8] = x2; 178 | out[9] = y2; 179 | out[10] = z2; 180 | out[11] = 0; 181 | out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); 182 | out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); 183 | out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); 184 | out[15] = 1; 185 | 186 | return out; 187 | }; 188 | 189 | /** 190 | * Get the top left 3x3 submatrix of a mat4 191 | * 192 | * @param {mat3} out 193 | * @param {mat4} a 194 | */ 195 | function submat3(out, a) { 196 | out[0] = a[0]; 197 | out[1] = a[1]; 198 | out[2] = a[2]; 199 | out[3] = a[4]; 200 | out[4] = a[5]; 201 | out[5] = a[6]; 202 | out[6] = a[8]; 203 | out[7] = a[9]; 204 | out[8] = a[10]; 205 | return out; 206 | } 207 | 208 | /** 209 | * Multiplies two mat3's 210 | * 211 | * @param {mat3} out the receiving matrix 212 | * @param {mat3} a the first operand 213 | * @param {mat3} b the second operand 214 | * @returns {mat3} out 215 | */ 216 | function mat3multiply(out, a, b) { 217 | var a00 = a[0], a01 = a[1], a02 = a[2], 218 | a10 = a[3], a11 = a[4], a12 = a[5], 219 | a20 = a[6], a21 = a[7], a22 = a[8], 220 | 221 | b00 = b[0], b01 = b[1], b02 = b[2], 222 | b10 = b[3], b11 = b[4], b12 = b[5], 223 | b20 = b[6], b21 = b[7], b22 = b[8]; 224 | 225 | out[0] = b00 * a00 + b01 * a10 + b02 * a20; 226 | out[1] = b00 * a01 + b01 * a11 + b02 * a21; 227 | out[2] = b00 * a02 + b01 * a12 + b02 * a22; 228 | 229 | out[3] = b10 * a00 + b11 * a10 + b12 * a20; 230 | out[4] = b10 * a01 + b11 * a11 + b12 * a21; 231 | out[5] = b10 * a02 + b11 * a12 + b12 * a22; 232 | 233 | out[6] = b20 * a00 + b21 * a10 + b22 * a20; 234 | out[7] = b20 * a01 + b21 * a11 + b22 * a21; 235 | out[8] = b20 * a02 + b21 * a12 + b22 * a22; 236 | return out; 237 | }; 238 | 239 | 240 | /** 241 | * Multiplies a mat3 and a vec 242 | * 243 | * @param {mat3} out the receiving matrix 244 | * @param {mat3} a the first operand 245 | * @param {mat3} b the second operand 246 | * @returns {mat3} out 247 | */ 248 | function mat3vecmultiply(out, a, b) { 249 | var a00 = a[0], a01 = a[1], a02 = a[2], 250 | a10 = a[3], a11 = a[4], a12 = a[5], 251 | a20 = a[6], a21 = a[7], a22 = a[8], 252 | 253 | b00 = b[0], b01 = b[1], b02 = b[2]; 254 | 255 | out[0] = b00 * a00 + b01 * a10 + b02 * a20; 256 | out[1] = b00 * a01 + b01 * a11 + b02 * a21; 257 | out[2] = b00 * a02 + b01 * a12 + b02 * a22; 258 | 259 | return out; 260 | } 261 | 262 | /** 263 | * Multiplies two mat4's explicitly not using SIMD 264 | * 265 | * @param {mat4} out the receiving matrix 266 | * @param {mat4} a the first operand 267 | * @param {mat4} b the second operand 268 | * @returns {mat4} out 269 | */ 270 | function mat4multiply(out, a, b) { 271 | var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], 272 | a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], 273 | a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], 274 | a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; 275 | 276 | // Cache only the current line of the second matrix 277 | var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; 278 | out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 279 | out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 280 | out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 281 | out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 282 | 283 | b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; 284 | out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 285 | out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 286 | out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 287 | out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 288 | 289 | b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; 290 | out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 291 | out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 292 | out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 293 | out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 294 | 295 | b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; 296 | out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 297 | out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 298 | out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 299 | out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 300 | return out; 301 | }; 302 | 303 | 304 | 305 | 306 | 307 | /** 308 | 309 | * Rotates a matrix by the given angle around the X axis 310 | * 311 | * @param {mat4} out the receiving matrix 312 | * @param {ReadonlyMat4} a the matrix to rotate 313 | * @param {Number} rad the angle to rotate the matrix by 314 | * @returns {mat4} out 315 | */ 316 | function mat4rotateX(out, a, rad) { 317 | let s = Math.sin(rad); 318 | let c = Math.cos(rad); 319 | let a10 = a[4]; 320 | let a11 = a[5]; 321 | let a12 = a[6]; 322 | let a13 = a[7]; 323 | let a20 = a[8]; 324 | let a21 = a[9]; 325 | let a22 = a[10]; 326 | let a23 = a[11]; 327 | if (a !== out) { 328 | // If the source and destination differ, copy the unchanged rows 329 | out[0] = a[0]; 330 | out[1] = a[1]; 331 | out[2] = a[2]; 332 | out[3] = a[3]; 333 | out[12] = a[12]; 334 | out[13] = a[13]; 335 | out[14] = a[14]; 336 | out[15] = a[15]; 337 | } 338 | // Perform axis-specific matrix multiplication 339 | out[4] = a10 * c + a20 * s; 340 | out[5] = a11 * c + a21 * s; 341 | out[6] = a12 * c + a22 * s; 342 | out[7] = a13 * c + a23 * s; 343 | out[8] = a20 * c - a10 * s; 344 | out[9] = a21 * c - a11 * s; 345 | out[10] = a22 * c - a12 * s; 346 | out[11] = a23 * c - a13 * s; 347 | return out; 348 | } 349 | 350 | /** 351 | * Rotates a matrix by the given angle around the Y axis 352 | * 353 | * @param {mat4} out the receiving matrix 354 | * @param {ReadonlyMat4} a the matrix to rotate 355 | * @param {Number} rad the angle to rotate the matrix by 356 | * @returns {mat4} out 357 | */ 358 | function mat4rotateY(out, a, rad) { 359 | let s = Math.sin(rad); 360 | let c = Math.cos(rad); 361 | let a00 = a[0]; 362 | let a01 = a[1]; 363 | let a02 = a[2]; 364 | let a03 = a[3]; 365 | let a20 = a[8]; 366 | let a21 = a[9]; 367 | let a22 = a[10]; 368 | let a23 = a[11]; 369 | if (a !== out) { 370 | // If the source and destination differ, copy the unchanged rows 371 | out[4] = a[4]; 372 | out[5] = a[5]; 373 | out[6] = a[6]; 374 | out[7] = a[7]; 375 | out[12] = a[12]; 376 | out[13] = a[13]; 377 | out[14] = a[14]; 378 | out[15] = a[15]; 379 | } 380 | // Perform axis-specific matrix multiplication 381 | out[0] = a00 * c - a20 * s; 382 | out[1] = a01 * c - a21 * s; 383 | out[2] = a02 * c - a22 * s; 384 | out[3] = a03 * c - a23 * s; 385 | out[8] = a00 * s + a20 * c; 386 | out[9] = a01 * s + a21 * c; 387 | out[10] = a02 * s + a22 * c; 388 | out[11] = a03 * s + a23 * c; 389 | return out; 390 | } 391 | 392 | /** 393 | * Rotates a matrix by the given angle around the Z axis 394 | * 395 | * @param {mat4} out the receiving matrix 396 | * @param {ReadonlyMat4} a the matrix to rotate 397 | * @param {Number} rad the angle to rotate the matrix by 398 | * @returns {mat4} out 399 | */ 400 | 401 | function mat4rotateZ(out, a, rad) { 402 | let s = Math.sin(rad); 403 | let c = Math.cos(rad); 404 | let a00 = a[0]; 405 | let a01 = a[1]; 406 | let a02 = a[2]; 407 | let a03 = a[3]; 408 | let a10 = a[4]; 409 | let a11 = a[5]; 410 | let a12 = a[6]; 411 | let a13 = a[7]; 412 | if (a !== out) { 413 | // If the source and destination differ, copy the unchanged last row 414 | out[8] = a[8]; 415 | out[9] = a[9]; 416 | out[10] = a[10]; 417 | out[11] = a[11]; 418 | out[12] = a[12]; 419 | out[13] = a[13]; 420 | out[14] = a[14]; 421 | out[15] = a[15]; 422 | } 423 | // Perform axis-specific matrix multiplication 424 | out[0] = a00 * c + a10 * s; 425 | out[1] = a01 * c + a11 * s; 426 | out[2] = a02 * c + a12 * s; 427 | out[3] = a03 * c + a13 * s; 428 | out[4] = a10 * c - a00 * s; 429 | out[5] = a11 * c - a01 * s; 430 | out[6] = a12 * c - a02 * s; 431 | out[7] = a13 * c - a03 * s; 432 | return out; 433 | } 434 | 435 | export { 436 | mat3transpose, 437 | mat3multiply, 438 | mat3vecmultiply, 439 | mat4multiply, 440 | mat4perspective, 441 | mat4ortho, 442 | mat4lookAt, 443 | mat4rotateX, 444 | mat4rotateY, 445 | mat4rotateZ, 446 | submat3 447 | }; -------------------------------------------------------------------------------- /lib/utils/rotors.js: -------------------------------------------------------------------------------- 1 | import { mat3multiply, mat3transpose } from "./linalg.js"; 2 | 3 | function rotorToRotationMatrix(out, rotor) { 4 | // Extract the quaternion components. 5 | const w = rotor[0]; 6 | const x = rotor[1]; 7 | const y = rotor[2]; 8 | const z = rotor[3]; 9 | 10 | // Calculate the rotation matrix. 11 | out[0] = 1 - 2 * (y * y + z * z); 12 | out[1] = 2 * (x * y - w * z); 13 | out[2] = 2 * (x * z + w * y); 14 | 15 | out[3] = 2 * (x * y + w * z); 16 | out[4] = 1 - 2 * (x * x + z * z); 17 | out[5] = 2 * (y * z - w * x); 18 | 19 | out[6] = 2 * (x * z - w * y); 20 | out[7] = 2 * (y * z + w * x); 21 | out[8] = 1 - 2 * (x * x + y * y); 22 | }; 23 | 24 | function rotorsToCov3D(scales, rotors) { 25 | let vertexCount = Math.floor(scales.length/3); 26 | 27 | let rotMat = new Float32Array(9); 28 | let scaleMat = new Float32Array(9); 29 | let mMat = new Float32Array(9); 30 | let mMatTr = new Float32Array(9); 31 | 32 | let covMat = new Float32Array(9); 33 | 34 | let rotor = new Float32Array(4); 35 | 36 | let covP1 = new Float32Array(4 * vertexCount); 37 | let covP2 = new Float32Array(2 * vertexCount); 38 | 39 | for (var i=0; i < vertexCount; i++) { 40 | rotor[0] = rotors[4*i+0]; 41 | rotor[1] = rotors[4*i+1]; 42 | rotor[2] = rotors[4*i+2]; 43 | rotor[3] = rotors[4*i+3]; 44 | 45 | scaleMat[0] = scales[3*i+0]; 46 | scaleMat[4] = scales[3*i+1]; 47 | scaleMat[8] = scales[3*i+2]; 48 | 49 | rotorToRotationMatrix(rotMat, rotor); 50 | mat3multiply(mMat, scaleMat, rotMat); 51 | mat3transpose(mMatTr, mMat); 52 | mat3multiply(covMat, mMatTr, mMat); 53 | 54 | covP1[4*i+0] = covMat[0]; 55 | covP1[4*i+1] = covMat[4]; 56 | covP1[4*i+2] = covMat[8]; 57 | 58 | covP1[4*i+3] = covMat[1]; 59 | covP2[2*i+0] = covMat[2]; 60 | covP2[2*i+1] = covMat[5]; 61 | } 62 | 63 | return { 64 | p1: covP1, 65 | p2: covP2 66 | } 67 | } 68 | 69 | 70 | function propsToCov3D(props) { 71 | let vertexCount = Math.floor(props.scale_0.length); 72 | 73 | let rotMat = new Float32Array(9); 74 | let scaleMat = new Float32Array(9); 75 | let mMat = new Float32Array(9); 76 | let mMatTr = new Float32Array(9); 77 | 78 | let covMat = new Float32Array(9); 79 | 80 | let rotor = new Float32Array(4); 81 | 82 | let covP1 = new Float32Array(3 * vertexCount); 83 | let covP2 = new Float32Array(3 * vertexCount); 84 | 85 | for (var i=0; i < vertexCount; i++) { 86 | rotor[0] = props.rot_0[i]; 87 | rotor[1] = props.rot_1[i]; 88 | rotor[2] = props.rot_2[i]; 89 | rotor[3] = props.rot_3[i]; 90 | 91 | scaleMat[0] = props.scale_0[i]; 92 | scaleMat[4] = props.scale_1[i]; 93 | scaleMat[8] = props.scale_2[i]; 94 | 95 | rotorToRotationMatrix(rotMat, rotor); 96 | mat3multiply(mMat, scaleMat, rotMat); 97 | mat3transpose(mMatTr, mMat); 98 | mat3multiply(covMat, mMatTr, mMat); 99 | 100 | covP1[3*i+0] = covMat[0]; 101 | covP1[3*i+1] = covMat[4]; 102 | covP1[3*i+2] = covMat[8]; 103 | 104 | covP2[3*i+0] = covMat[1]; 105 | covP2[3*i+1] = covMat[2]; 106 | covP2[3*i+2] = covMat[5]; 107 | } 108 | 109 | return { 110 | diag: covP1, 111 | upper: covP2 112 | } 113 | } 114 | 115 | export { rotorsToCov3D, propsToCov3D, rotorToRotationMatrix }; -------------------------------------------------------------------------------- /lib/utils/view.js: -------------------------------------------------------------------------------- 1 | import { 2 | mat3multiply, 3 | mat3transpose, 4 | mat3vecmultiply, 5 | mat4lookAt, 6 | mat4multiply, 7 | mat4rotateX, 8 | mat4rotateY, 9 | mat4rotateZ, 10 | submat3 11 | } from './linalg.js'; 12 | import { rotorToRotationMatrix } from './rotors.js'; 13 | 14 | function mat4multiplyNew(a, b){ 15 | var out = new Float32Array(16); 16 | mat4multiply(out, a, b); 17 | return out; 18 | } 19 | 20 | function viewMatGetLookAt(viewMatrix, radius) { 21 | // First get coordinates of the focus point in the world frame. 22 | // We know that view_matrix = [R, t] 23 | // and that R*focus_pos + t = [0, 0, -viewParams.radius] 24 | // So we can solve for focus_pos 25 | let r = new Float32Array(9); 26 | let t = [viewMatrix[12], viewMatrix[13], viewMatrix[14]]; 27 | submat3(r, viewMatrix); 28 | let rinv = new Float32Array(9); 29 | mat3transpose(rinv, r); // Invert rotation 30 | let p = [-t[0],-t[1],-t[2]-radius]; // Difference. 31 | mat3vecmultiply(t, rinv, p); // focus point coords in world frame. 32 | 33 | return t; 34 | } 35 | 36 | function viewMatGetPoseParams(viewMatrix, radius) { 37 | let lookAtPos = viewMatGetLookAt(viewMatrix, radius); 38 | let cameraPos = viewMatGetLookAt(viewMatrix, 0.0); 39 | let upVec = [ viewMatrix[1], viewMatrix[5], viewMatrix[9]]; 40 | 41 | return { 42 | camera: cameraPos, 43 | lookAt: lookAtPos, 44 | up: upVec 45 | } 46 | } 47 | 48 | function initializeViewMatrix(cameraParams) { 49 | let viewMatrix = new Float32Array(16); 50 | mat4lookAt(viewMatrix, cameraParams.position, cameraParams.lookAt, cameraParams.up); 51 | return viewMatrix; 52 | } 53 | 54 | function identity(n) { 55 | let out = new Float32Array(n * n); 56 | for (let i = 0; i < n; i++) { 57 | out[i * n + i] = 1; 58 | } 59 | return out; 60 | } 61 | 62 | function buildTransformMatrix(R, p) { 63 | return new Float32Array([ 64 | R[0], R[1], R[2], 0, 65 | R[4], R[5], R[6], 0, 66 | R[8], R[9], R[10], 0, 67 | p[0], p[1], p[2], 1 68 | ]); 69 | } 70 | 71 | 72 | function eulerRotation(angles) { 73 | let rotation = identity(4); 74 | mat4rotateX(rotation, rotation, angles[0]); 75 | mat4rotateY(rotation, rotation, angles[1]); 76 | mat4rotateZ(rotation, rotation, angles[2]); 77 | return rotation; 78 | } 79 | 80 | function updateViewMatrix(viewParams, angles, p) { 81 | let newViewParams = Object.assign({}, viewParams); 82 | 83 | let rotation = eulerRotation(angles); 84 | let transformMatrix = buildTransformMatrix(rotation, p); 85 | newViewParams.matrix = mat4multiplyNew(transformMatrix, viewParams.matrix); 86 | 87 | return newViewParams; 88 | } 89 | 90 | /** 91 | * Updates the view matrix to enable orbit-like rotation around a focus point. 92 | * 93 | * @param {Array} delta - The current cursor position difference from previous (x, y coordinates). 94 | * @param {Object} viewParams - An object containing the following properties: 95 | * * radius {number} - The distance from the camera to the focus point. 96 | * * viewMatrix {Float32Array} - The current view matrix (a 4x4 matrix). 97 | */ 98 | function viewOrbit(delta, viewParams) { 99 | let newViewParams = Object.assign({}, viewParams); 100 | 101 | // First get coordinates of the focus point in the world frame. 102 | // We know that view_matrix = [R, t] 103 | // and that R*focus_pos + t = [0, 0, -viewParams.radius] 104 | // So we can solve for focus_pos 105 | let r = new Float32Array(9); 106 | //let t = [viewParams.matrix[12], viewParams.matrix[13], viewParams.matrix[14]]; 107 | let t = viewParams.matrix.slice(12, 15); 108 | submat3(r, viewParams.matrix); 109 | let rinv = new Float32Array(9); 110 | mat3transpose(rinv, r); // Invert rotation 111 | let p = [-t[0],-t[1],-t[2]-viewParams.radius]; // Difference. 112 | mat3vecmultiply(t, rinv, p); // focus point coords in world frame. 113 | 114 | // Now we can rotate the camera 115 | let s = eulerRotation([delta[1], delta[0], 0.0]); 116 | let transformMatrix = buildTransformMatrix(s, [0, 0, 0]); 117 | let newViewMatrix = mat4multiplyNew(transformMatrix, viewParams.matrix); 118 | 119 | // And then move the camera back to the original focus point, 120 | // Via t2 = -R*t + [0, 0, -viewParams.radius] 121 | submat3(r, newViewMatrix); 122 | mat3vecmultiply(p, r, t); 123 | let t2 = [-p[0],-p[1],-p[2]-viewParams.radius]; 124 | newViewMatrix[12] = t2[0]; 125 | newViewMatrix[13] = t2[1]; 126 | newViewMatrix[14] = t2[2]; 127 | 128 | newViewParams.matrix = newViewMatrix; 129 | return newViewParams; 130 | } 131 | 132 | /** 133 | * Updates the view to allow for dolly (translation along the view axis) and rotation around the Z-axis, controlled by cursor movement. 134 | * 135 | * @param {Array} delta - The current cursor position difference from previous (x, y coordinates). 136 | * @param {Object} viewParams - An object containing the following properties: 137 | * * viewMatrix {Float32Array} - The current view matrix (a 4x4 matrix). 138 | */ 139 | function viewDollyTranslateRotate(delta, viewParams) { 140 | let newViewParams = updateViewMatrix(viewParams, [0, 0, delta[0]], [0, 0, -viewParams.radius*delta[1]]); 141 | newViewParams.radius = viewParams.radius*(1.0 + delta[1]); 142 | return newViewParams; 143 | } 144 | 145 | function viewDolly(posDelta, viewParams) { 146 | let newViewParams = updateViewMatrix(viewParams, [0, 0, 0], [0, 0, -viewParams.radius*0.5*posDelta]); 147 | newViewParams.radius = viewParams.radius*(1.0 + 0.5*posDelta); 148 | return newViewParams 149 | } 150 | 151 | /** 152 | * Updates the view matrix to allow for vertical and horizontal strafing movement based on cursor motion. 153 | * Strafing refers to side-to-side and up/down movement without changing camera orientation. 154 | * 155 | * @param {Array} delta - The current cursor position difference from previous (x, y coordinates). 156 | * @param {Object} viewParams - An object containing the following properties: 157 | * * viewMatrix {Float32Array} - The current view matrix (a 4x4 matrix). 158 | */ 159 | function viewStrafeVert(delta, viewParams) { 160 | return updateViewMatrix(viewParams, [0, 0, 0], [3*delta[0], -3*delta[1], 0]); 161 | } 162 | 163 | /** 164 | * Updates the view matrix to simulate camera panning (pivoting the camera) based on cursor movement. 165 | * 166 | * @param {Array} delta - The current cursor position difference from previous (x, y coordinates). 167 | * @param {Object} viewParams - An object containing the following properties: 168 | * * viewMatrix {Float32Array} - The current view matrix (a 4x4 matrix). 169 | */ 170 | function viewPan(delta, viewParams) { 171 | return updateViewMatrix(viewParams, [delta[1], delta[0], 0], [0, 0, 0]); 172 | } 173 | 174 | function viewRoll(angleDelta, viewParams) { 175 | return updateViewMatrix(viewParams, [0, 0, angleDelta], [0, 0, 0]); 176 | } 177 | 178 | 179 | 180 | function viewChangeRadius(radiusDelta, viewParams) { 181 | let newViewParams = Object.assign({}, viewParams); 182 | 183 | newViewParams.radius = viewParams.radius + radiusDelta[1]; 184 | 185 | return newViewParams; 186 | } 187 | 188 | const actionMap = { 189 | "pan": viewPan, 190 | "orbit": viewOrbit, 191 | "dolly": viewDolly, 192 | "roll": viewRoll, 193 | "dollyRoll": viewDollyTranslateRotate, 194 | "strafe": viewStrafeVert, 195 | "changeRadius": viewChangeRadius 196 | }; 197 | 198 | 199 | function viewUpdate(action, delta, viewParams) { 200 | let f = actionMap[action]; 201 | 202 | return f(delta, viewParams); 203 | } 204 | 205 | function viewAutoSpin(viewParams) { 206 | console.log("spin") 207 | 208 | setInterval(() => { 209 | viewOrbit([1, 0], [0, 0], viewParams); 210 | }, 1000 / 60); 211 | } 212 | function stopAutoSpin() { 213 | clearInterval(); 214 | } 215 | 216 | 217 | export { viewUpdate, viewAutoSpin, stopAutoSpin, initializeViewMatrix, viewMatGetLookAt, viewMatGetPoseParams }; 218 | 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gauss-splat-renderer", 3 | "version": "1.0.0", 4 | "license": "ISC", 5 | "type": "module", 6 | "main": "gausssplatrenderer.js", 7 | "scripts": { 8 | "start": "node app.js", 9 | "test": "mocha \"test/**/*.js\"", 10 | "cover": "istanbul cover node_modules/mocha/bin/_mocha --dir ./reports/coverage" 11 | }, 12 | "dependencies": { 13 | "chai": "^4.3.10", 14 | "mocha": "^10.2.0" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Make the canvas fill the entire screen */ 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | background-color: transparent; 10 | color: white; 11 | touch-action: none; 12 | } 13 | 14 | canvas { 15 | display: block; 16 | /* Removes the margin inside the canvas */ 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | #canvas-container { 22 | position: relative; 23 | width: 100%; 24 | height: 100%; 25 | } 26 | 27 | a { 28 | color: #f7ebec; 29 | } 30 | 31 | .bottomDiv { 32 | position: fixed; 33 | /* Fixed position relative to the viewport */ 34 | bottom: 0; 35 | /* Align to the bottom of the viewport */ 36 | width: 100%; 37 | /* Take full width */ 38 | /* Add other styles like background, padding, etc. as needed */ 39 | } 40 | 41 | #text-overlay { 42 | position: absolute; 43 | top: 10px; 44 | /* Adjust as needed */ 45 | left: 10px; 46 | /* Adjust as needed */ 47 | color: white; 48 | font-size: 18px; 49 | /* Additional styling as needed */ 50 | font-size: 18px; 51 | font-family: 'Montserrat', sans-serif; 52 | } 53 | 54 | #openFileButton { 55 | background-color: #00000060; 56 | /* Black background */ 57 | color: white; 58 | /* White text */ 59 | border: 2px solid white; 60 | /* White border */ 61 | border-radius: 5px; 62 | padding: 10px 20px; 63 | /* Padding around the text */ 64 | font-size: 16px; 65 | /* Font size */ 66 | font-family: 'Montserrat', sans-serif; 67 | cursor: pointer; 68 | /* Change mouse cursor on hover */ 69 | outline: none; 70 | /* Remove outline */ 71 | transition: 0.3s; 72 | /* Smooth transition for hover effects */ 73 | } 74 | 75 | #portalityPlatformButton { 76 | background-color: #00000060; 77 | /* Black background */ 78 | color: white; 79 | /* White text */ 80 | border: 2px solid white; 81 | /* White border */ 82 | border-radius: 5px; 83 | padding: 10px 20px; 84 | /* Padding around the text */ 85 | font-size: 16px; 86 | /* Font size */ 87 | font-family: 'Montserrat', sans-serif; 88 | cursor: pointer; 89 | /* Change mouse cursor on hover */ 90 | outline: none; 91 | /* Remove outline */ 92 | transition: 0.3s; 93 | /* Smooth transition for hover effects */ 94 | } 95 | 96 | #helpButton { 97 | background-color: #00000060; 98 | /* Black background */ 99 | color: white; 100 | /* White text */ 101 | border: 2px solid white; 102 | /* White border */ 103 | border-radius: 5px; 104 | padding: 10px 20px; 105 | /* Padding around the text */ 106 | font-size: 16px; 107 | /* Font size */ 108 | font-family: 'Montserrat', sans-serif; 109 | cursor: pointer; 110 | /* Change mouse cursor on hover */ 111 | outline: none; 112 | /* Remove outline */ 113 | transition: 0.3s; 114 | /* Smooth transition for hover effects */ 115 | } 116 | 117 | #openFileButton:hover { 118 | background-color: white; 119 | /* Change background to white on hover */ 120 | color: black; 121 | /* Change text color to black on hover */ 122 | } 123 | 124 | #loadingSymbol { 125 | position: fixed; 126 | top: 50%; 127 | left: 50%; 128 | transform: translate(-50%, -50%); 129 | animation: rotate 2s linear infinite; 130 | transform-origin: 50% 50%; 131 | /* Adjust if necessary */ 132 | } 133 | 134 | #helpMenu { 135 | display: none; /* Initially hide the menu */ 136 | position: absolute; 137 | top: 50%; left: 50%; 138 | transform: translate(-50%, -50%); 139 | min-width: 380px; 140 | background-color: #1d1e2c80; 141 | color: white; 142 | border: 1px solid #ccc; 143 | border-radius: 10px; 144 | padding: 20px; 145 | font-family: 'Montserrat', sans-serif; 146 | text-align: center; 147 | backdrop-filter: blur(3px); 148 | } 149 | 150 | /* logo placed at top center */ 151 | #logo { 152 | width: 64px; 153 | height: 64px; 154 | margin-bottom: 10px; 155 | } 156 | 157 | /* logo placed at top center */ 158 | #controls { 159 | width: 320px; 160 | margin-bottom: 10px; 161 | } 162 | 163 | #mobileControlsBtn, #desktopControlsBtn { 164 | background-color: transparent; 165 | color: white; 166 | border: 1px solid white; 167 | border-radius: 3px; 168 | padding: 4px 8px; 169 | font-size: 16px; 170 | font-family: 'Montserrat', sans-serif; 171 | outline: none; 172 | } 173 | 174 | #closeButton { 175 | position: absolute; 176 | top: 5px; 177 | right: 5px; 178 | border: none; 179 | background: none; 180 | cursor: pointer; 181 | color: white; 182 | } 183 | 184 | @keyframes rotate { 185 | from { 186 | transform: translate(-50%, -50%) rotate(0deg); 187 | } 188 | 189 | to { 190 | transform: translate(-50%, -50%) rotate(360deg); 191 | } 192 | } -------------------------------------------------------------------------------- /test/assets/test.splat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanverseRobotics/portality-web-viewer/9abf1436c77f397ef669682f8701ce6223523c4f/test/assets/test.splat -------------------------------------------------------------------------------- /test/browser/kernel-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test page. 6 | 7 | 8 | 9 | 10 |

Test Page

11 |

Page containing test scripts for kernels.

12 |

13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 431 | 432 | -------------------------------------------------------------------------------- /test/kd-tree.test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import kdTree, { viewDepthSort, depthFirstCollect } from '../lib/kd-tree.js'; 3 | import IndexedPointArray, { permutePointArray } from '../lib/pointarray.js'; 4 | 5 | const { expect } = chai; 6 | 7 | describe('kdTree', () => { 8 | it('should create a tree correctly', () => { 9 | const iparr = new IndexedPointArray( 10 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0, 21.7, 4.0]), 11 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5, 19.5, 4.5]), 12 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8, 24.5, 20.0]), 13 | ); 14 | 15 | const tree = kdTree(iparr, 2); 16 | 17 | expect(iparr.idx.find((x)=>(x==2)) <= 4); 18 | expect(iparr.idx.find((x)=>(x==3)) <= 4); 19 | expect(iparr.idx.find((x)=>(x==4)) <= 4); 20 | expect(iparr.idx.find((x)=>(x==5)) <= 4); 21 | 22 | expect(tree.d).to.equal(0); 23 | expect(tree.val).to.equal(3.5); 24 | expect(tree.cleft.d).to.equal(1); 25 | expect(tree.cleft.val).to.equal(2.5); 26 | expect(tree.cleft.cleft.range).to.deep.equal([0, 2]); 27 | expect(tree.cleft.cright.range).to.deep.equal([2, 4]); 28 | expect(tree.cright.d).to.equal(1); 29 | expect(tree.cright.val).to.equal(4.5); 30 | expect(tree.cright.cleft.range).to.deep.equal([4, 6]); 31 | expect(tree.cright.cright.range).to.deep.equal([6, 8]); 32 | }); 33 | 34 | it('should create an unbalanced tree correctly', () => { 35 | const iparr = new IndexedPointArray( 36 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0]), 37 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5]), 38 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8]), 39 | ); 40 | 41 | const tree = kdTree(iparr, 2); 42 | 43 | expect(tree.d).to.equal(0); 44 | expect(tree.val).to.equal(3); 45 | expect(tree.cleft.range).to.deep.equal([0, 2]); 46 | expect(tree.cright.d).to.equal(1); 47 | expect(tree.cright.val).to.equal(2.0); 48 | expect(tree.cright.cleft.range).to.deep.equal([2, 4]); 49 | expect(tree.cright.cright.range).to.deep.equal([4, 6]); 50 | }); 51 | 52 | 53 | it.skip('should add a new batch of points to the kdtree correctly.', () => { 54 | const iparr = new IndexedPointArray( 55 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0, 21.7, 4.0]), 56 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5, 19.5, 4.5]), 57 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8, 24.5, 20.0]), 58 | ); 59 | const tree = kdTree(iparr, 2); 60 | 61 | // TODO 62 | }); 63 | 64 | 65 | it('should sort the tree by reverse depth correctly', () => { 66 | const iparr = new IndexedPointArray( 67 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0, 21.7, 4.0]), 68 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5, 19.5, 4.5]), 69 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8, 24.5, 20.0]), 70 | ); 71 | const tree = kdTree(iparr, 2); 72 | 73 | let idx = viewDepthSort(tree, {x: 3, y: -3, z: -3}, 'reverse'); 74 | 75 | expect(idx[0]).to.deep.equal([6, 8]); 76 | expect(idx[1]).to.deep.equal([4, 6]); 77 | expect(idx[2]).to.deep.equal([2, 4]); 78 | expect(idx[3]).to.deep.equal([0, 2]); 79 | }); 80 | 81 | it('should sort the tree by depth correctly', () => { 82 | const iparr = new IndexedPointArray( 83 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0, 21.7, 4.0]), 84 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5, 19.5, 4.5]), 85 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8, 24.5, 20.0]), 86 | ); 87 | const tree = kdTree(iparr, 2); 88 | 89 | let idx = viewDepthSort(tree, {x: 3, y: -3, z: -3}); 90 | 91 | expect(idx[0]).to.deep.equal([0, 2]); 92 | expect(idx[1]).to.deep.equal([2, 4]); 93 | expect(idx[2]).to.deep.equal([4, 6]); 94 | expect(idx[3]).to.deep.equal([6, 8]); 95 | }); 96 | 97 | it('should sort an unbalanced tree by depth correctly', () => { 98 | const iparr = new IndexedPointArray( 99 | new Float32Array([23.0, 12.5, 3.0, 3.5, 3.0, 1.0]), 100 | new Float32Array([17.2, 0.5, 24.0, 2.0, 10.5, 2.5]), 101 | new Float32Array([13.0, 11.0, 13.0, 12.5, 19.7, 1.8]), 102 | ); 103 | const tree = kdTree(iparr, 2); 104 | 105 | let idx = viewDepthSort(tree, {x: 3, y: -3, z: -3}); 106 | 107 | expect(idx[0]).to.deep.equal([0, 2]); 108 | expect(idx[1]).to.deep.equal([2, 4]); 109 | expect(idx[2]).to.deep.equal([4, 6]); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/quickselect.test.js: -------------------------------------------------------------------------------- 1 | // test/quickselect.test.js 2 | 3 | import { expect } from 'chai' 4 | 5 | import quickselect from "../lib/quickselect.js"; 6 | 7 | describe("Quickselect", function() { 8 | it("should return the median element of a sorted array", function() { 9 | const array = [1, 7, 5, 3, 9]; 10 | const idx = [0, 1, 2, 3, 4]; 11 | 12 | var median = quickselect(array, idx, 0); 13 | expect(median).to.equal(1); 14 | 15 | median = quickselect(array, idx, 1); 16 | expect(median).to.equal(3); 17 | 18 | median = quickselect(array, idx, 3); 19 | expect(median).to.equal(7); 20 | }); 21 | 22 | it("should handle empty and single-element arrays", function() { 23 | expect(quickselect([], [], 0)).to.equal(undefined); 24 | expect(quickselect([5], [0], 0)).to.equal(5); 25 | }); 26 | 27 | it("should throw an error if the kth element index is out of bounds", function() { 28 | const array = [1, 3, 5, 7, 9]; 29 | const idx = [0, 1, 2, 3, 4]; 30 | 31 | expect(() => quickselect(array, idx, -1)).to.throw(); 32 | expect(() => quickselect(array, idx, 5)).to.throw(); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/splatfile.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { Buffer } from 'buffer' 3 | import { expect } from 'chai' 4 | import { loadSplatData } from '../lib/splatfile.js'; 5 | import { rotorsToCov3D } from '../lib/utils/rotors.js' 6 | 7 | describe("Splat File", function () { 8 | it("should load the splat file and parse it correctly", function () { 9 | 10 | function loadFileIntoBuffer(filePath) { 11 | const fileContents = fs.readFileSync(filePath); 12 | const buffer = Buffer.from(fileContents); 13 | 14 | return buffer; 15 | } 16 | 17 | // Load the file "test.splat" into a buffer. 18 | const buffer = loadFileIntoBuffer('./test/assets/test.splat'); 19 | 20 | let data = loadSplatData(buffer); 21 | 22 | expect(data.positions.length).to.equal(3*128); 23 | expect(data.colors.length).to.equal(4*128); 24 | expect(data.scales.length).to.equal(3*128); 25 | expect(data.rotors.length).to.equal(4*128); 26 | 27 | expect(data.positions[4]).to.be.closeTo(1.517803907, 1e-5); 28 | expect(data.colors[4]).to.be.closeTo(1, 1e-5); 29 | }); 30 | 31 | it("should process splat format to 3d covariance matrices", function () { 32 | 33 | function loadFileIntoBuffer(filePath) { 34 | const fileContents = fs.readFileSync(filePath); 35 | const buffer = Buffer.from(fileContents); 36 | 37 | return buffer; 38 | } 39 | 40 | // Load the file "test.splat" into a buffer. 41 | const buffer = loadFileIntoBuffer('./test/assets/test.splat'); 42 | 43 | let data = loadSplatData(buffer); 44 | 45 | let cov3d = rotorsToCov3D(data.scales, data.rotors); 46 | 47 | // TODO: insert the 'right' output tests here 48 | }); 49 | 50 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Change this to match your project 3 | "include": ["./*.js"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "declarationMap": false 9 | } 10 | } --------------------------------------------------------------------------------