diff --git a/XsenViewer.ipynb b/XsenViewer.ipynb new file mode 100644 index 0000000..b3e6322 --- /dev/null +++ b/XsenViewer.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "npKH6fN4AfYO", + "outputId": "daf581dc-0603-4624-e3d4-980d3bb5c3f7" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloning into 'splat'...\n", + "remote: Enumerating objects: 169, done.\u001b[K\n", + "remote: Counting objects: 100% (98/98), done.\u001b[K\n", + "remote: Compressing objects: 100% (39/39), done.\u001b[K\n", + "remote: Total 169 (delta 80), reused 59 (delta 59), pack-reused 71 (from 2)\u001b[K\n", + "Receiving objects: 100% (169/169), 6.29 MiB | 12.84 MiB/s, done.\n", + "Resolving deltas: 100% (95/95), done.\n", + "--2025-12-23 16:21:02-- https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n", + "Resolving github.com (github.com)... 140.82.112.3\n", + "Connecting to github.com (github.com)|140.82.112.3|:443... connected.\n", + "HTTP request sent, awaiting response... 302 Found\n", + "Location: https://github.com/cloudflare/cloudflared/releases/download/2025.11.1/cloudflared-linux-amd64.deb [following]\n", + "--2025-12-23 16:21:02-- https://github.com/cloudflare/cloudflared/releases/download/2025.11.1/cloudflared-linux-amd64.deb\n", + "Reusing existing connection to github.com:443.\n", + "HTTP request sent, awaiting response... 302 Found\n", + "Location: https://release-assets.githubusercontent.com/github-production-release-asset/106867604/8a32f7c6-649c-4f0d-806d-e14c19d0786d?sp=r&sv=2018-11-09&sr=b&spr=https&se=2025-12-23T17%3A12%3A59Z&rscd=attachment%3B+filename%3Dcloudflared-linux-amd64.deb&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2025-12-23T16%3A12%3A56Z&ske=2025-12-23T17%3A12%3A59Z&sks=b&skv=2018-11-09&sig=aVPaI%2BqTJ0USgLcrjjtNENeD329dNh4x5Q1Y5ZF3iVA%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc2NjUwODU1MywibmJmIjoxNzY2NTA2NzUzLCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.wlvjfm3-tDyGQGRR6i1MeRbFj1CCHXSNSMStrvbcKIE&response-content-disposition=attachment%3B%20filename%3Dcloudflared-linux-amd64.deb&response-content-type=application%2Foctet-stream [following]\n", + "--2025-12-23 16:21:02-- https://release-assets.githubusercontent.com/github-production-release-asset/106867604/8a32f7c6-649c-4f0d-806d-e14c19d0786d?sp=r&sv=2018-11-09&sr=b&spr=https&se=2025-12-23T17%3A12%3A59Z&rscd=attachment%3B+filename%3Dcloudflared-linux-amd64.deb&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2025-12-23T16%3A12%3A56Z&ske=2025-12-23T17%3A12%3A59Z&sks=b&skv=2018-11-09&sig=aVPaI%2BqTJ0USgLcrjjtNENeD329dNh4x5Q1Y5ZF3iVA%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc2NjUwODU1MywibmJmIjoxNzY2NTA2NzUzLCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.wlvjfm3-tDyGQGRR6i1MeRbFj1CCHXSNSMStrvbcKIE&response-content-disposition=attachment%3B%20filename%3Dcloudflared-linux-amd64.deb&response-content-type=application%2Foctet-stream\n", + "Resolving release-assets.githubusercontent.com (release-assets.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n", + "Connecting to release-assets.githubusercontent.com (release-assets.githubusercontent.com)|185.199.108.133|:443... connected.\n", + "HTTP request sent, awaiting response... 200 OK\n", + "Length: 20192500 (19M) [application/octet-stream]\n", + "Saving to: ‘/content/cloudflared-linux-amd64.deb’\n", + "\n", + "/content/cloudflare 100%[===================>] 19.26M --.-KB/s in 0.1s \n", + "\n", + "2025-12-23 16:21:03 (129 MB/s) - ‘/content/cloudflared-linux-amd64.deb’ saved [20192500/20192500]\n", + "\n", + "(Reading database ... 117532 files and directories currently installed.)\n", + "Preparing to unpack cloudflared-linux-amd64.deb ...\n", + "Unpacking cloudflared (2025.11.1) over (2025.11.1) ...\n", + "Setting up cloudflared (2025.11.1) ...\n", + "Processing triggers for man-db (2.10.2-1) ...\n" + ] + } + ], + "source": [ + "!git clone --branch 0.0.1 --single-branch https://github.com/dahyunk-snu/splat\n", + "!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -O /content/cloudflared-linux-amd64.deb\n", + "!dpkg -i cloudflared-linux-amd64.deb" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "88xKxEOcAuwn", + "outputId": "85509625-965d-4b08-c42f-ae783744bba8" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "<>:12: SyntaxWarning: invalid escape sequence '\\/'\n", + "<>:12: SyntaxWarning: invalid escape sequence '\\/'\n", + "/tmp/ipython-input-969851764.py:12: SyntaxWarning: invalid escape sequence '\\/'\n", + " tunnel_url = re.search(\"(?Phttps?:\\/\\/[^\\s]+.trycloudflare.com)\", requests.get(f'http://127.0.0.1:{metrics_port}/metrics').text).group(\"url\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://published-trans-try-newman.trycloudflare.com\n", + "/content/splat\n", + "Serving HTTP on 0.0.0.0 port 7860 (http://0.0.0.0:7860/) ...\n", + "127.0.0.1 - - [23/Dec/2025 16:21:37] \"GET / HTTP/1.1\" 200 -\n", + "127.0.0.1 - - [23/Dec/2025 16:21:38] \"GET /main.js HTTP/1.1\" 200 -\n", + "127.0.0.1 - - [23/Dec/2025 16:21:38] \"GET /output.splat HTTP/1.1\" 200 -\n", + "127.0.0.1 - - [23/Dec/2025 16:21:39] code 404, message File not found\n", + "127.0.0.1 - - [23/Dec/2025 16:21:39] \"GET /favicon.ico HTTP/1.1\" 404 -\n", + "----------------------------------------\n", + "Exception occurred during processing of request from ('127.0.0.1', 55396)\n", + "Traceback (most recent call last):\n", + " File \"/usr/lib/python3.12/socketserver.py\", line 697, in process_request_thread\n", + " self.finish_request(request, client_address)\n", + " File \"/usr/lib/python3.12/http/server.py\", line 1311, in finish_request\n", + " self.RequestHandlerClass(request, client_address, self,\n", + " File \"/usr/lib/python3.12/http/server.py\", line 672, in __init__\n", + " super().__init__(*args, **kwargs)\n", + " File \"/usr/lib/python3.12/socketserver.py\", line 766, in __init__\n", + " self.handle()\n", + " File \"/usr/lib/python3.12/http/server.py\", line 436, in handle\n", + " self.handle_one_request()\n", + " File \"/usr/lib/python3.12/http/server.py\", line 424, in handle_one_request\n", + " method()\n", + " File \"/usr/lib/python3.12/http/server.py\", line 679, in do_GET\n", + " self.copyfile(f, self.wfile)\n", + " File \"/usr/lib/python3.12/http/server.py\", line 878, in copyfile\n", + " shutil.copyfileobj(source, outputfile)\n", + " File \"/usr/lib/python3.12/shutil.py\", line 204, in copyfileobj\n", + " fdst_write(buf)\n", + " File \"/usr/lib/python3.12/socketserver.py\", line 845, in write\n", + " self._sock.sendall(b)\n", + "ConnectionResetError: [Errno 104] Connection reset by peer\n", + "----------------------------------------\n", + "\n", + "Keyboard interrupt received, exiting.\n" + ] + } + ], + "source": [ + "import atexit, requests, subprocess, time, re, os\n", + "from random import randint\n", + "from threading import Timer\n", + "from queue import Queue\n", + "def cloudflared(port, metrics_port, output_queue):\n", + " atexit.register(lambda p: p.terminate(), subprocess.Popen(['cloudflared', 'tunnel', '--url', f'http://127.0.0.1:{port}', '--metrics', f'127.0.0.1:{metrics_port}'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT))\n", + " attempts, tunnel_url = 0, None\n", + " while attempts < 10 and not tunnel_url:\n", + " attempts += 1\n", + " time.sleep(3)\n", + " try:\n", + " tunnel_url = re.search(\"(?Phttps?:\\/\\/[^\\s]+.trycloudflare.com)\", requests.get(f'http://127.0.0.1:{metrics_port}/metrics').text).group(\"url\")\n", + " except:\n", + " pass\n", + " if not tunnel_url:\n", + " raise Exception(\"Can't connect to Cloudflare Edge\")\n", + " output_queue.put(tunnel_url)\n", + "output_queue, metrics_port = Queue(), randint(8100, 9000)\n", + "thread = Timer(2, cloudflared, args=(7860, metrics_port, output_queue))\n", + "thread.start()\n", + "thread.join()\n", + "tunnel_url = output_queue.get()\n", + "os.environ['webui_url'] = tunnel_url\n", + "print(tunnel_url)\n", + "%cd /content/splat\n", + "!python -m http.server 7860" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1dApd29AA-yN" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "authorship_tag": "ABX9TyNHaS27pg6CY4E0cW4ufqoh", + "include_colab_link": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/main.js b/main.js index 8f7f527..cbfc39d 100644 --- a/main.js +++ b/main.js @@ -1,1484 +1,1147 @@ let cameras = [ - { - id: 0, - img_name: "00001", - width: 1959, - height: 1090, - position: [ - -3.0089893469241797, -0.11086489695181866, -3.7527640949141428, - ], - rotation: [ - [0.876134201218856, 0.06925962026449776, 0.47706599800804744], - [-0.04747421839895102, 0.9972110940209488, -0.057586739349882114], - [-0.4797239414934443, 0.027805376500959853, 0.8769787916452908], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 1, - img_name: "00009", - width: 1959, - height: 1090, - position: [ - -2.5199776022057296, -0.09704735754873686, -3.6247725540304545, - ], - rotation: [ - [0.9982731285632193, -0.011928707708098955, -0.05751927260507243], - [0.0065061360949636325, 0.9955928229282383, -0.09355533724430458], - [0.058381769258182864, 0.09301955098900708, 0.9939511719154457], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 2, - img_name: "00017", - width: 1959, - height: 1090, - position: [ - -0.7737533667465242, -0.3364271945329695, -2.9358969417573753, - ], - rotation: [ - [0.9998813418672372, 0.013742375651625236, -0.0069605529394208224], - [-0.014268370388586709, 0.996512943252834, -0.08220929105659476], - [0.00580653013657589, 0.08229885200307129, 0.9965907801935302], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 3, - img_name: "00025", - width: 1959, - height: 1090, - position: [ - 1.2198221749590001, -0.2196687861401182, -2.3183162007028453, - ], - rotation: [ - [0.9208648867765482, 0.0012010625395201253, 0.389880004297208], - [-0.06298204172269357, 0.987319521752825, 0.14571693239364383], - [-0.3847611242348369, -0.1587410451475895, 0.9092635249821667], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 4, - img_name: "00033", - width: 1959, - height: 1090, - position: [ - 1.742387858893817, -0.13848225198886954, -2.0566370113193146, - ], - rotation: [ - [0.24669889292141334, -0.08370189346592856, -0.9654706879349405], - [0.11343747891376445, 0.9919082664242816, -0.05700815184573074], - [0.9624300466054861, -0.09545671285663988, 0.2541976029815521], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 5, - img_name: "00041", - width: 1959, - height: 1090, - position: [ - 3.6567309419223935, -0.16470990600750707, -1.3458085590422042, - ], - rotation: [ - [0.2341293058324528, -0.02968330457755884, -0.9717522161434825], - [0.10270823606832301, 0.99469554638321, -0.005638106875665722], - [0.9667649592295676, -0.09848690996657204, 0.2359360976431732], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 6, - img_name: "00049", - width: 1959, - height: 1090, - position: [ - 3.9013554243203497, -0.2597500978038105, -0.8106154188297828, - ], - rotation: [ - [0.6717235545638952, -0.015718162115524837, -0.7406351366386528], - [0.055627354673906296, 0.9980224478387622, 0.029270992841185218], - [0.7387104058127439, -0.060861588786650656, 0.6712695459756353], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 7, - img_name: "00057", - width: 1959, - height: 1090, - position: [4.742994605467533, -0.05591660945412069, 0.9500365976084458], - rotation: [ - [-0.17042655709210375, 0.01207080756938, -0.9852964448542146], - [0.1165090336695526, 0.9931575292530063, -0.00798543433078162], - [0.9784581921120181, -0.1161568667478904, -0.1706667764862097], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 8, - img_name: "00065", - width: 1959, - height: 1090, - position: [4.34676307626522, 0.08168160516967145, 1.0876221470355405], - rotation: [ - [-0.003575447631888379, -0.044792503246552894, -0.9989899137764799], - [0.10770152645126597, 0.9931680875192705, -0.04491693593046672], - [0.9941768441149182, -0.10775333677534978, 0.0012732004866391048], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, - { - id: 9, - img_name: "00073", - width: 1959, - height: 1090, - position: [3.264984351114202, 0.078974937336732, 1.0117200284114904], - rotation: [ - [-0.026919994628162257, -0.1565891128261527, -0.9872968974090509], - [0.08444552208239385, 0.983768234577625, -0.1583319754069128], - [0.9960643893290491, -0.0876350978794554, -0.013259786205163005], - ], - fy: 1164.6601287484507, - fx: 1159.5880733038064, - }, + { + id: 0, + img_name: "00001", + width: 1959, + height: 1090, + position: [ + -3.0089893469241797, -0.11086489695181866, -3.7527640949141428, + ], + rotation: [ + [0.876134201218856, 0.06925962026449776, 0.47706599800804744], + [-0.04747421839895102, 0.9972110940209488, -0.057586739349882114], + [-0.4797239414934443, 0.027805376500959853, 0.8769787916452908], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 1, + img_name: "00009", + width: 1959, + height: 1090, + position: [ + -2.5199776022057296, -0.09704735754873686, -3.6247725540304545, + ], + rotation: [ + [0.9982731285632193, -0.011928707708098955, -0.05751927260507243], + [0.0065061360949636325, 0.9955928229282383, -0.09355533724430458], + [0.058381769258182864, 0.09301955098900708, 0.9939511719154457], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 2, + img_name: "00017", + width: 1959, + height: 1090, + position: [ + -0.7737533667465242, -0.3364271945329695, -2.9358969417573753, + ], + rotation: [ + [0.9998813418672372, 0.013742375651625236, -0.0069605529394208224], + [-0.014268370388586709, 0.996512943252834, -0.08220929105659476], + [0.00580653013657589, 0.08229885200307129, 0.9965907801935302], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 3, + img_name: "00025", + width: 1959, + height: 1090, + position: [ + 1.2198221749590001, -0.2196687861401182, -2.3183162007028453, + ], + rotation: [ + [0.9208648867765482, 0.0012010625395201253, 0.389880004297208], + [-0.06298204172269357, 0.987319521752825, 0.14571693239364383], + [-0.3847611242348369, -0.1587410451475895, 0.9092635249821667], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 4, + img_name: "00033", + width: 1959, + height: 1090, + position: [ + 1.742387858893817, -0.13848225198886954, -2.0566370113193146, + ], + rotation: [ + [0.24669889292141334, -0.08370189346592856, -0.9654706879349405], + [0.11343747891376445, 0.9919082664242816, -0.05700815184573074], + [0.9624300466054861, -0.09545671285663988, 0.2541976029815521], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 5, + img_name: "00041", + width: 1959, + height: 1090, + position: [ + 3.6567309419223935, -0.16470990600750707, -1.3458085590422042, + ], + rotation: [ + [0.2341293058324528, -0.02968330457755884, -0.9717522161434825], + [0.10270823606832301, 0.99469554638321, -0.005638106875665722], + [0.9667649592295676, -0.09848690996657204, 0.2359360976431732], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 6, + img_name: "00049", + width: 1959, + height: 1090, + position: [ + 3.9013554243203497, -0.2597500978038105, -0.8106154188297828, + ], + rotation: [ + [0.6717235545638952, -0.015718162115524837, -0.7406351366386528], + [0.055627354673906296, 0.9980224478387622, 0.029270992841185218], + [0.7387104058127439, -0.060861588786650656, 0.6712695459756353], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 7, + img_name: "00057", + width: 1959, + height: 1090, + position: [4.742994605467533, -0.05591660945412069, 0.9500365976084458], + rotation: [ + [-0.17042655709210375, 0.01207080756938, -0.9852964448542146], + [0.1165090336695526, 0.9931575292530063, -0.00798543433078162], + [0.9784581921120181, -0.1161568667478904, -0.1706667764862097], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 8, + img_name: "00065", + width: 1959, + height: 1090, + position: [4.34676307626522, 0.08168160516967145, 1.0876221470355405], + rotation: [ + [-0.003575447631888379, -0.044792503246552894, -0.9989899137764799], + [0.10770152645126597, 0.9931680875192705, -0.04491693593046672], + [0.9941768441149182, -0.10775333677534978, 0.0012732004866391048], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, + { + id: 9, + img_name: "00073", + width: 1959, + height: 1090, + position: [3.264984351114202, 0.078974937336732, 1.0117200284114904], + rotation: [ + [-0.026919994628162257, -0.1565891128261527, -0.9872968974090509], + [0.08444552208239385, 0.983768234577625, -0.1583319754069128], + [0.9960643893290491, -0.0876350978794554, -0.013259786205163005], + ], + fy: 1164.6601287484507, + fx: 1159.5880733038064, + }, ]; -let camera = cameras[0]; +const camera = cameras[0]; function getProjectionMatrix(fx, fy, width, height) { - const znear = 0.2; - const zfar = 200; - return [ - [(2 * fx) / width, 0, 0, 0], - [0, -(2 * fy) / height, 0, 0], - [0, 0, zfar / (zfar - znear), 1], - [0, 0, -(zfar * znear) / (zfar - znear), 0], - ].flat(); + const znear = 0.2; + const zfar = 200; + return [ + [(2 * fx) / width, 0, 0, 0], + [0, (2 * fy) / height, 0, 0], + [0, 0, zfar / (zfar - znear), 1], + [0, 0, -(zfar * znear) / (zfar - znear), 0], + ].flat(); } function getViewMatrix(camera) { - const R = camera.rotation.flat(); - const t = camera.position; - const camToWorld = [ - [R[0], R[1], R[2], 0], - [R[3], R[4], R[5], 0], - [R[6], R[7], R[8], 0], - [ - -t[0] * R[0] - t[1] * R[3] - t[2] * R[6], - -t[0] * R[1] - t[1] * R[4] - t[2] * R[7], - -t[0] * R[2] - t[1] * R[5] - t[2] * R[8], - 1, - ], - ].flat(); - return camToWorld; + const R = camera.rotation.flat(); + const t = camera.position; + const camToWorld = [ + [R[0], R[1], R[2], 0], + [R[3], R[4], R[5], 0], + [R[6], R[7], R[8], 0], + [ + -t[0] * R[0] - t[1] * R[3] - t[2] * R[6], + -t[0] * R[1] - t[1] * R[4] - t[2] * R[7], + -t[0] * R[2] - t[1] * R[5] - t[2] * R[8], + 1, + ], + ].flat(); + return camToWorld; } -// function translate4(a, x, y, z) { -// return [ -// ...a.slice(0, 12), -// a[0] * x + a[4] * y + a[8] * z + a[12], -// a[1] * x + a[5] * y + a[9] * z + a[13], -// a[2] * x + a[6] * y + a[10] * z + a[14], -// a[3] * x + a[7] * y + a[11] * z + a[15], -// ]; -// } function multiply4(a, b) { - return [ - b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12], - b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13], - b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14], - b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15], - b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12], - b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13], - b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14], - b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15], - b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12], - b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13], - b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14], - b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15], - b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12], - b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13], - b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14], - b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15], - ]; + return [ + b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12], + b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13], + b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14], + b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15], + b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12], + b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13], + b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14], + b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15], + b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12], + b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13], + b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14], + b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15], + b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12], + b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13], + b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14], + b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15], + ]; } function invert4(a) { - let b00 = a[0] * a[5] - a[1] * a[4]; - let b01 = a[0] * a[6] - a[2] * a[4]; - let b02 = a[0] * a[7] - a[3] * a[4]; - let b03 = a[1] * a[6] - a[2] * a[5]; - let b04 = a[1] * a[7] - a[3] * a[5]; - let b05 = a[2] * a[7] - a[3] * a[6]; - let b06 = a[8] * a[13] - a[9] * a[12]; - let b07 = a[8] * a[14] - a[10] * a[12]; - let b08 = a[8] * a[15] - a[11] * a[12]; - let b09 = a[9] * a[14] - a[10] * a[13]; - let b10 = a[9] * a[15] - a[11] * a[13]; - let b11 = a[10] * a[15] - a[11] * a[14]; - let det = - b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; - if (!det) return null; - return [ - (a[5] * b11 - a[6] * b10 + a[7] * b09) / det, - (a[2] * b10 - a[1] * b11 - a[3] * b09) / det, - (a[13] * b05 - a[14] * b04 + a[15] * b03) / det, - (a[10] * b04 - a[9] * b05 - a[11] * b03) / det, - (a[6] * b08 - a[4] * b11 - a[7] * b07) / det, - (a[0] * b11 - a[2] * b08 + a[3] * b07) / det, - (a[14] * b02 - a[12] * b05 - a[15] * b01) / det, - (a[8] * b05 - a[10] * b02 + a[11] * b01) / det, - (a[4] * b10 - a[5] * b08 + a[7] * b06) / det, - (a[1] * b08 - a[0] * b10 - a[3] * b06) / det, - (a[12] * b04 - a[13] * b02 + a[15] * b00) / det, - (a[9] * b02 - a[8] * b04 - a[11] * b00) / det, - (a[5] * b07 - a[4] * b09 - a[6] * b06) / det, - (a[0] * b09 - a[1] * b07 + a[2] * b06) / det, - (a[13] * b01 - a[12] * b03 - a[14] * b00) / det, - (a[8] * b03 - a[9] * b01 + a[10] * b00) / det, - ]; + let b00 = a[0] * a[5] - a[1] * a[4]; + let b01 = a[0] * a[6] - a[2] * a[4]; + let b02 = a[0] * a[7] - a[3] * a[4]; + let b03 = a[1] * a[6] - a[2] * a[5]; + let b04 = a[1] * a[7] - a[3] * a[5]; + let b05 = a[2] * a[7] - a[3] * a[6]; + let b06 = a[8] * a[13] - a[9] * a[12]; + let b07 = a[8] * a[14] - a[10] * a[12]; + let b08 = a[8] * a[15] - a[11] * a[12]; + let b09 = a[9] * a[14] - a[10] * a[13]; + let b10 = a[9] * a[15] - a[11] * a[13]; + let b11 = a[10] * a[15] - a[11] * a[14]; + let det = + b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + if (!det) return null; + return [ + (a[5] * b11 - a[6] * b10 + a[7] * b09) / det, + (a[2] * b10 - a[1] * b11 - a[3] * b09) / det, + (a[13] * b05 - a[14] * b04 + a[15] * b03) / det, + (a[10] * b04 - a[9] * b05 - a[11] * b03) / det, + (a[6] * b08 - a[4] * b11 - a[7] * b07) / det, + (a[0] * b11 - a[2] * b08 + a[3] * b07) / det, + (a[14] * b02 - a[12] * b05 - a[15] * b01) / det, + (a[8] * b05 - a[10] * b02 + a[11] * b01) / det, + (a[4] * b10 - a[5] * b08 + a[7] * b06) / det, + (a[1] * b08 - a[0] * b10 - a[3] * b06) / det, + (a[12] * b04 - a[13] * b02 + a[15] * b00) / det, + (a[9] * b02 - a[8] * b04 - a[11] * b00) / det, + (a[5] * b07 - a[4] * b09 - a[6] * b06) / det, + (a[0] * b09 - a[1] * b07 + a[2] * b06) / det, + (a[13] * b01 - a[12] * b03 - a[14] * b00) / det, + (a[8] * b03 - a[9] * b01 + a[10] * b00) / det, + ]; } function rotate4(a, rad, x, y, z) { - let len = Math.hypot(x, y, z); - x /= len; - y /= len; - z /= len; - let s = Math.sin(rad); - let c = Math.cos(rad); - let t = 1 - c; - let b00 = x * x * t + c; - let b01 = y * x * t + z * s; - let b02 = z * x * t - y * s; - let b10 = x * y * t - z * s; - let b11 = y * y * t + c; - let b12 = z * y * t + x * s; - let b20 = x * z * t + y * s; - let b21 = y * z * t - x * s; - let b22 = z * z * t + c; - return [ - a[0] * b00 + a[4] * b01 + a[8] * b02, - a[1] * b00 + a[5] * b01 + a[9] * b02, - a[2] * b00 + a[6] * b01 + a[10] * b02, - a[3] * b00 + a[7] * b01 + a[11] * b02, - a[0] * b10 + a[4] * b11 + a[8] * b12, - a[1] * b10 + a[5] * b11 + a[9] * b12, - a[2] * b10 + a[6] * b11 + a[10] * b12, - a[3] * b10 + a[7] * b11 + a[11] * b12, - a[0] * b20 + a[4] * b21 + a[8] * b22, - a[1] * b20 + a[5] * b21 + a[9] * b22, - a[2] * b20 + a[6] * b21 + a[10] * b22, - a[3] * b20 + a[7] * b21 + a[11] * b22, - ...a.slice(12, 16), - ]; + let len = Math.hypot(x, y, z); + x /= len; + y /= len; + z /= len; + let s = Math.sin(rad); + let c = Math.cos(rad); + let t = 1 - c; + let b00 = x * x * t + c; + let b01 = y * x * t + z * s; + let b02 = z * x * t - y * s; + let b10 = x * y * t - z * s; + let b11 = y * y * t + c; + let b12 = z * y * t + x * s; + let b20 = x * z * t + y * s; + let b21 = y * z * t - x * s; + let b22 = z * z * t + c; + return [ + a[0] * b00 + a[4] * b01 + a[8] * b02, + a[1] * b00 + a[5] * b01 + a[9] * b02, + a[2] * b00 + a[6] * b01 + a[10] * b02, + a[3] * b00 + a[7] * b01 + a[11] * b02, + a[0] * b10 + a[4] * b11 + a[8] * b12, + a[1] * b10 + a[5] * b11 + a[9] * b12, + a[2] * b10 + a[6] * b11 + a[10] * b12, + a[3] * b10 + a[7] * b11 + a[11] * b12, + a[0] * b20 + a[4] * b21 + a[8] * b22, + a[1] * b20 + a[5] * b21 + a[9] * b22, + a[2] * b20 + a[6] * b21 + a[10] * b22, + a[3] * b20 + a[7] * b21 + a[11] * b22, + ...a.slice(12, 16), + ]; } function translate4(a, x, y, z) { - return [ - ...a.slice(0, 12), - a[0] * x + a[4] * y + a[8] * z + a[12], - a[1] * x + a[5] * y + a[9] * z + a[13], - a[2] * x + a[6] * y + a[10] * z + a[14], - a[3] * x + a[7] * y + a[11] * z + a[15], - ]; + return [ + ...a.slice(0, 12), + a[0] * x + a[4] * y + a[8] * z + a[12], + a[1] * x + a[5] * y + a[9] * z + a[13], + a[2] * x + a[6] * y + a[10] * z + a[14], + a[3] * x + a[7] * y + a[11] * z + a[15], + ]; } function createWorker(self) { - let buffer; - let vertexCount = 0; - let viewProj; - // 6*4 + 4 + 4 = 8*4 - // XYZ - Position (Float32) - // XYZ - Scale (Float32) - // RGBA - colors (uint8) - // IJKL - quaternion/rot (uint8) - const rowLength = 3 * 4 + 3 * 4 + 4 + 4; - let lastProj = []; - let depthIndex = new Uint32Array(); - let lastVertexCount = 0; - - var _floatView = new Float32Array(1); - var _int32View = new Int32Array(_floatView.buffer); - - function floatToHalf(float) { - _floatView[0] = float; - var f = _int32View[0]; - - var sign = (f >> 31) & 0x0001; - var exp = (f >> 23) & 0x00ff; - var frac = f & 0x007fffff; - - var newExp; - if (exp == 0) { - newExp = 0; - } else if (exp < 113) { - newExp = 0; - frac |= 0x00800000; - frac = frac >> (113 - exp); - if (frac & 0x01000000) { - newExp = 1; - frac = 0; - } - } else if (exp < 142) { - newExp = exp - 112; - } else { - newExp = 31; - frac = 0; - } - - return (sign << 15) | (newExp << 10) | (frac >> 13); - } - - function packHalf2x16(x, y) { - return (floatToHalf(x) | (floatToHalf(y) << 16)) >>> 0; - } - - function generateTexture() { - if (!buffer) return; - const f_buffer = new Float32Array(buffer); - const u_buffer = new Uint8Array(buffer); - - var texwidth = 1024 * 2; // Set to your desired width - var texheight = Math.ceil((2 * vertexCount) / texwidth); // Set to your desired height - var texdata = new Uint32Array(texwidth * texheight * 4); // 4 components per pixel (RGBA) - var texdata_c = new Uint8Array(texdata.buffer); - var texdata_f = new Float32Array(texdata.buffer); - - // Here we convert from a .splat file buffer into a texture - // With a little bit more foresight perhaps this texture file - // should have been the native format as it'd be very easy to - // load it into webgl. - for (let i = 0; i < vertexCount; i++) { - // x, y, z - texdata_f[8 * i + 0] = f_buffer[8 * i + 0]; - texdata_f[8 * i + 1] = f_buffer[8 * i + 1]; - texdata_f[8 * i + 2] = f_buffer[8 * i + 2]; - - // r, g, b, a - texdata_c[4 * (8 * i + 7) + 0] = u_buffer[32 * i + 24 + 0]; - texdata_c[4 * (8 * i + 7) + 1] = u_buffer[32 * i + 24 + 1]; - texdata_c[4 * (8 * i + 7) + 2] = u_buffer[32 * i + 24 + 2]; - texdata_c[4 * (8 * i + 7) + 3] = u_buffer[32 * i + 24 + 3]; - - // quaternions - let scale = [ - f_buffer[8 * i + 3 + 0], - f_buffer[8 * i + 3 + 1], - f_buffer[8 * i + 3 + 2], - ]; - let rot = [ - (u_buffer[32 * i + 28 + 0] - 128) / 128, - (u_buffer[32 * i + 28 + 1] - 128) / 128, - (u_buffer[32 * i + 28 + 2] - 128) / 128, - (u_buffer[32 * i + 28 + 3] - 128) / 128, - ]; - - // Compute the matrix product of S and R (M = S * R) - const M = [ - 1.0 - 2.0 * (rot[2] * rot[2] + rot[3] * rot[3]), - 2.0 * (rot[1] * rot[2] + rot[0] * rot[3]), - 2.0 * (rot[1] * rot[3] - rot[0] * rot[2]), - - 2.0 * (rot[1] * rot[2] - rot[0] * rot[3]), - 1.0 - 2.0 * (rot[1] * rot[1] + rot[3] * rot[3]), - 2.0 * (rot[2] * rot[3] + rot[0] * rot[1]), - - 2.0 * (rot[1] * rot[3] + rot[0] * rot[2]), - 2.0 * (rot[2] * rot[3] - rot[0] * rot[1]), - 1.0 - 2.0 * (rot[1] * rot[1] + rot[2] * rot[2]), - ].map((k, i) => k * scale[Math.floor(i / 3)]); - - const sigma = [ - M[0] * M[0] + M[3] * M[3] + M[6] * M[6], - M[0] * M[1] + M[3] * M[4] + M[6] * M[7], - M[0] * M[2] + M[3] * M[5] + M[6] * M[8], - M[1] * M[1] + M[4] * M[4] + M[7] * M[7], - M[1] * M[2] + M[4] * M[5] + M[7] * M[8], - M[2] * M[2] + M[5] * M[5] + M[8] * M[8], - ]; - - texdata[8 * i + 4] = packHalf2x16(4 * sigma[0], 4 * sigma[1]); - texdata[8 * i + 5] = packHalf2x16(4 * sigma[2], 4 * sigma[3]); - texdata[8 * i + 6] = packHalf2x16(4 * sigma[4], 4 * sigma[5]); - } - - self.postMessage({ texdata, texwidth, texheight }, [texdata.buffer]); - } - - function runSort(viewProj) { - if (!buffer) return; - const f_buffer = new Float32Array(buffer); - if (lastVertexCount == vertexCount) { - let dot = - lastProj[2] * viewProj[2] + - lastProj[6] * viewProj[6] + - lastProj[10] * viewProj[10]; - if (Math.abs(dot - 1) < 0.01) { - return; - } - } else { - generateTexture(); - lastVertexCount = vertexCount; - } - - console.time("sort"); - let maxDepth = -Infinity; - let minDepth = Infinity; - let sizeList = new Int32Array(vertexCount); - for (let i = 0; i < vertexCount; i++) { - let depth = - ((viewProj[2] * f_buffer[8 * i + 0] + - viewProj[6] * f_buffer[8 * i + 1] + - viewProj[10] * f_buffer[8 * i + 2]) * - 4096) | - 0; - sizeList[i] = depth; - if (depth > maxDepth) maxDepth = depth; - if (depth < minDepth) minDepth = depth; - } - - // This is a 16 bit single-pass counting sort - let depthInv = (256 * 256 - 1) / (maxDepth - minDepth); - let counts0 = new Uint32Array(256 * 256); - for (let i = 0; i < vertexCount; i++) { - sizeList[i] = ((sizeList[i] - minDepth) * depthInv) | 0; - counts0[sizeList[i]]++; - } - let starts0 = new Uint32Array(256 * 256); - for (let i = 1; i < 256 * 256; i++) - starts0[i] = starts0[i - 1] + counts0[i - 1]; - depthIndex = new Uint32Array(vertexCount); - for (let i = 0; i < vertexCount; i++) - depthIndex[starts0[sizeList[i]]++] = i; - - console.timeEnd("sort"); - - lastProj = viewProj; - self.postMessage({ depthIndex, viewProj, vertexCount }, [ - depthIndex.buffer, - ]); - } - - function processPlyBuffer(inputBuffer) { - const ubuf = new Uint8Array(inputBuffer); - // 10KB ought to be enough for a header... - const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10)); - const header_end = "end_header\n"; - const header_end_index = header.indexOf(header_end); - if (header_end_index < 0) - throw new Error("Unable to read .ply file header"); - const vertexCount = parseInt(/element vertex (\d+)\n/.exec(header)[1]); - console.log("Vertex Count", vertexCount); - let row_offset = 0, - offsets = {}, - types = {}; - const TYPE_MAP = { - double: "getFloat64", - int: "getInt32", - uint: "getUint32", - float: "getFloat32", - short: "getInt16", - ushort: "getUint16", - uchar: "getUint8", - }; - for (let prop of header - .slice(0, header_end_index) - .split("\n") - .filter((k) => k.startsWith("property "))) { - const [p, type, name] = prop.split(" "); - const arrayType = TYPE_MAP[type] || "getInt8"; - types[name] = arrayType; - offsets[name] = row_offset; - row_offset += parseInt(arrayType.replace(/[^\d]/g, "")) / 8; - } - console.log("Bytes per row", row_offset, types, offsets); - - let dataView = new DataView( - inputBuffer, - header_end_index + header_end.length, - ); - let row = 0; - const attrs = new Proxy( - {}, - { - get(target, prop) { - if (!types[prop]) throw new Error(prop + " not found"); - return dataView[types[prop]]( - row * row_offset + offsets[prop], - true, - ); - }, - }, - ); - - console.time("calculate importance"); - let sizeList = new Float32Array(vertexCount); - let sizeIndex = new Uint32Array(vertexCount); - for (row = 0; row < vertexCount; row++) { - sizeIndex[row] = row; - if (!types["scale_0"]) continue; - const size = - Math.exp(attrs.scale_0) * - Math.exp(attrs.scale_1) * - Math.exp(attrs.scale_2); - const opacity = 1 / (1 + Math.exp(-attrs.opacity)); - sizeList[row] = size * opacity; - } - console.timeEnd("calculate importance"); - - console.time("sort"); - sizeIndex.sort((b, a) => sizeList[a] - sizeList[b]); - console.timeEnd("sort"); - - // 6*4 + 4 + 4 = 8*4 - // XYZ - Position (Float32) - // XYZ - Scale (Float32) - // RGBA - colors (uint8) - // IJKL - quaternion/rot (uint8) - const rowLength = 3 * 4 + 3 * 4 + 4 + 4; - const buffer = new ArrayBuffer(rowLength * vertexCount); - - console.time("build buffer"); - for (let j = 0; j < vertexCount; j++) { - row = sizeIndex[j]; - - const position = new Float32Array(buffer, j * rowLength, 3); - const scales = new Float32Array(buffer, j * rowLength + 4 * 3, 3); - const rgba = new Uint8ClampedArray( - buffer, - j * rowLength + 4 * 3 + 4 * 3, - 4, - ); - const rot = new Uint8ClampedArray( - buffer, - j * rowLength + 4 * 3 + 4 * 3 + 4, - 4, - ); - - if (types["scale_0"]) { - const qlen = Math.sqrt( - attrs.rot_0 ** 2 + - attrs.rot_1 ** 2 + - attrs.rot_2 ** 2 + - attrs.rot_3 ** 2, - ); - - rot[0] = (attrs.rot_0 / qlen) * 128 + 128; - rot[1] = (attrs.rot_1 / qlen) * 128 + 128; - rot[2] = (attrs.rot_2 / qlen) * 128 + 128; - rot[3] = (attrs.rot_3 / qlen) * 128 + 128; - - scales[0] = Math.exp(attrs.scale_0); - scales[1] = Math.exp(attrs.scale_1); - scales[2] = Math.exp(attrs.scale_2); - } else { - scales[0] = 0.01; - scales[1] = 0.01; - scales[2] = 0.01; - - rot[0] = 255; - rot[1] = 0; - rot[2] = 0; - rot[3] = 0; - } - - position[0] = attrs.x; - position[1] = attrs.y; - position[2] = attrs.z; - - if (types["f_dc_0"]) { - const SH_C0 = 0.28209479177387814; - rgba[0] = (0.5 + SH_C0 * attrs.f_dc_0) * 255; - rgba[1] = (0.5 + SH_C0 * attrs.f_dc_1) * 255; - rgba[2] = (0.5 + SH_C0 * attrs.f_dc_2) * 255; - } else { - rgba[0] = attrs.red; - rgba[1] = attrs.green; - rgba[2] = attrs.blue; - } - if (types["opacity"]) { - rgba[3] = (1 / (1 + Math.exp(-attrs.opacity))) * 255; - } else { - rgba[3] = 255; - } - } - console.timeEnd("build buffer"); - return buffer; - } - - const throttledSort = () => { - if (!sortRunning) { - sortRunning = true; - let lastView = viewProj; - runSort(lastView); - setTimeout(() => { - sortRunning = false; - if (lastView !== viewProj) { - throttledSort(); - } - }, 0); - } - }; - - let sortRunning; - self.onmessage = (e) => { - if (e.data.ply) { - vertexCount = 0; - runSort(viewProj); - buffer = processPlyBuffer(e.data.ply); - vertexCount = Math.floor(buffer.byteLength / rowLength); - postMessage({ buffer: buffer, save: !!e.data.save }); - } else if (e.data.buffer) { - buffer = e.data.buffer; - vertexCount = e.data.vertexCount; - } else if (e.data.vertexCount) { - vertexCount = e.data.vertexCount; - } else if (e.data.view) { - viewProj = e.data.view; - throttledSort(); - } - }; + let buffer; + let vertexCount = 0; + let viewProj; + // 6*4 + 4 + 4 = 8*4 + // XYZ - Position (Float32) + // XYZ - Scale (Float32) + // RGBA - colors (uint8) + // IJKL - quaternion/rot (uint8) + const rowLength = 3 * 4 + 3 * 4 + 4 + 4; + + const runSort = (viewProj) => { + if (!buffer) return; + + // console.time("sort"); + const f_buffer = new Float32Array(buffer); + const u_buffer = new Uint8Array(buffer); + + const depthList = new Float32Array(vertexCount); + const depthIndex = new Uint32Array(vertexCount); + for (let j = 0; j < vertexCount; j++) { + // For some reason dividing by wumbo actually causes + // problems, so this is the unnormalized camera space homo + depthList[j] = + viewProj[2] * f_buffer[8 * j + 0] + + viewProj[6] * f_buffer[8 * j + 1] + + viewProj[10] * f_buffer[8 * j + 2]; + depthIndex[j] = j; + } + depthIndex.sort((a, b) => depthList[a] - depthList[b]); + + const quat = new Float32Array(4 * vertexCount); + const scale = new Float32Array(3 * vertexCount); + const center = new Float32Array(3 * vertexCount); + const color = new Float32Array(4 * vertexCount); + + for (let j = 0; j < vertexCount; j++) { + const i = depthIndex[j]; + + quat[4 * j + 0] = (u_buffer[32 * i + 28 + 0] - 128) / 128; + quat[4 * j + 1] = (u_buffer[32 * i + 28 + 1] - 128) / 128; + quat[4 * j + 2] = (u_buffer[32 * i + 28 + 2] - 128) / 128; + quat[4 * j + 3] = (u_buffer[32 * i + 28 + 3] - 128) / 128; + + center[3 * j + 0] = f_buffer[8 * i + 0]; + center[3 * j + 1] = f_buffer[8 * i + 1]; + center[3 * j + 2] = f_buffer[8 * i + 2]; + + color[4 * j + 0] = u_buffer[32 * i + 24 + 0] / 255; + color[4 * j + 1] = u_buffer[32 * i + 24 + 1] / 255; + color[4 * j + 2] = u_buffer[32 * i + 24 + 2] / 255; + color[4 * j + 3] = u_buffer[32 * i + 24 + 3] / 255; + + scale[3 * j + 0] = f_buffer[8 * i + 3 + 0]; + scale[3 * j + 1] = f_buffer[8 * i + 3 + 1]; + scale[3 * j + 2] = f_buffer[8 * i + 3 + 2]; + } + + self.postMessage({ quat, center, color, scale }, [ + quat.buffer, + center.buffer, + color.buffer, + scale.buffer, + ]); + + // console.timeEnd("sort"); + }; + + function processPlyBuffer(inputBuffer) { + const ubuf = new Uint8Array(inputBuffer); + // 10KB ought to be enough for a header... + const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10)); + const header_end = "end_header\n"; + const header_end_index = header.indexOf(header_end); + if (header_end_index < 0) + throw new Error("Unable to read .ply file header"); + const vertexCount = parseInt(/element vertex (\d+)\n/.exec(header)[1]); + console.log("Vertex Count", vertexCount); + let row_offset = 0, + offsets = {}, + types = {}; + const TYPE_MAP = { + double: "getFloat64", + int: "getInt32", + uint: "getUint32", + float: "getFloat32", + short: "getInt16", + ushort: "getUint16", + uchar: "getUint8", + }; + for (let prop of header + .slice(0, header_end_index) + .split("\n") + .filter((k) => k.startsWith("property "))) { + const [p, type, name] = prop.split(" "); + const arrayType = TYPE_MAP[type] || "getInt8"; + types[name] = arrayType; + offsets[name] = row_offset; + row_offset += parseInt(arrayType.replace(/[^\d]/g, "")) / 8; + } + console.log("Bytes per row", row_offset, types, offsets); + + let dataView = new DataView( + inputBuffer, + header_end_index + header_end.length, + ); + let row = 0; + const attrs = new Proxy( + {}, + { + get(target, prop) { + if (!types[prop]) throw new Error(prop + " not found"); + return dataView[types[prop]]( + row * row_offset + offsets[prop], + true, + ); + }, + }, + ); + + console.time("calculate importance"); + let sizeList = new Float32Array(vertexCount); + let sizeIndex = new Uint32Array(vertexCount); + for (row = 0; row < vertexCount; row++) { + sizeIndex[row] = row; + if (!types["scale_0"]) continue; + const size = + Math.exp(attrs.scale_0) * + Math.exp(attrs.scale_1) * + Math.exp(attrs.scale_2); + const opacity = 1 / (1 + Math.exp(-attrs.opacity)); + sizeList[row] = size * opacity; + } + console.timeEnd("calculate importance"); + + console.time("sort"); + sizeIndex.sort((b, a) => sizeList[a] - sizeList[b]); + console.timeEnd("sort"); + + // 6*4 + 4 + 4 = 8*4 + // XYZ - Position (Float32) + // XYZ - Scale (Float32) + // RGBA - colors (uint8) + // IJKL - quaternion/rot (uint8) + const rowLength = 3 * 4 + 3 * 4 + 4 + 4; + const buffer = new ArrayBuffer(rowLength * vertexCount); + + console.time("build buffer"); + for (let j = 0; j < vertexCount; j++) { + row = sizeIndex[j]; + + const position = new Float32Array(buffer, j * rowLength, 3); + const scales = new Float32Array(buffer, j * rowLength + 4 * 3, 3); + const rgba = new Uint8ClampedArray( + buffer, + j * rowLength + 4 * 3 + 4 * 3, + 4, + ); + const rot = new Uint8ClampedArray( + buffer, + j * rowLength + 4 * 3 + 4 * 3 + 4, + 4, + ); + + if (types["scale_0"]) { + const qlen = Math.sqrt( + attrs.rot_0 ** 2 + + attrs.rot_1 ** 2 + + attrs.rot_2 ** 2 + + attrs.rot_3 ** 2, + ); + + rot[0] = (attrs.rot_0 / qlen) * 128 + 128; + rot[1] = (attrs.rot_1 / qlen) * 128 + 128; + rot[2] = (attrs.rot_2 / qlen) * 128 + 128; + rot[3] = (attrs.rot_3 / qlen) * 128 + 128; + + scales[0] = Math.exp(attrs.scale_0); + scales[1] = Math.exp(attrs.scale_1); + scales[2] = Math.exp(attrs.scale_2); + } else { + scales[0] = 0.01; + scales[1] = 0.01; + scales[2] = 0.01; + + rot[0] = 255; + rot[1] = 0; + rot[2] = 0; + rot[3] = 0; + } + + position[0] = attrs.x; + position[1] = attrs.y; + position[2] = attrs.z; + + if (types["f_dc_0"]) { + const SH_C0 = 0.28209479177387814; + rgba[0] = (0.5 + SH_C0 * attrs.f_dc_0) * 255; + rgba[1] = (0.5 + SH_C0 * attrs.f_dc_1) * 255; + rgba[2] = (0.5 + SH_C0 * attrs.f_dc_2) * 255; + } else { + rgba[0] = attrs.red; + rgba[1] = attrs.green; + rgba[2] = attrs.blue; + } + if (types["opacity"]) { + rgba[3] = (1 / (1 + Math.exp(-attrs.opacity))) * 255; + } else { + rgba[3] = 255; + } + } + console.timeEnd("build buffer"); + return buffer; + } + + const throttledSort = () => { + if (!sortRunning) { + sortRunning = true; + let lastView = viewProj; + runSort(lastView); + setTimeout(() => { + sortRunning = false; + if (lastView !== viewProj) { + throttledSort(); + } + }, 0); + } + }; + + let sortRunning; + self.onmessage = (e) => { + if (e.data.ply) { + vertexCount = 0; + runSort(viewProj); + buffer = processPlyBuffer(e.data.ply); + vertexCount = Math.floor(buffer.byteLength / rowLength); + postMessage({ buffer: buffer }); + } else if (e.data.buffer) { + buffer = e.data.buffer; + vertexCount = e.data.vertexCount; + } else if (e.data.vertexCount) { + vertexCount = e.data.vertexCount; + } else if (e.data.view) { + viewProj = e.data.view; + throttledSort(); + } + }; } const vertexShaderSource = ` -#version 300 es -precision highp float; -precision highp int; +precision mediump float; +attribute vec2 position; + +attribute vec4 color; +attribute vec4 quat; +attribute vec3 scale; +attribute vec3 center; -uniform highp usampler2D u_texture; uniform mat4 projection, view; uniform vec2 focal; -uniform vec2 viewport; - -in vec2 position; -in int index; - -out vec4 vColor; -out vec2 vPosition; -void main () { - uvec4 cen = texelFetch(u_texture, ivec2((uint(index) & 0x3ffu) << 1, uint(index) >> 10), 0); - vec4 cam = view * vec4(uintBitsToFloat(cen.xyz), 1); - vec4 pos2d = projection * cam; +varying vec4 vColor; +varying vec3 vConic; +varying vec2 vCenter; +varying vec2 vPosition; +uniform vec2 viewport; - float clip = 1.2 * pos2d.w; - if (pos2d.z < -clip || pos2d.x < -clip || pos2d.x > clip || pos2d.y < -clip || pos2d.y > clip) { - gl_Position = vec4(0.0, 0.0, 2.0, 1.0); - return; - } +mat3 transpose(mat3 m) { return mat3(m[0][0], m[1][0], m[2][0], m[0][1], m[1][1], m[2][1], m[0][2], m[1][2], m[2][2]); } - uvec4 cov = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 1) | 1u, uint(index) >> 10), 0); - vec2 u1 = unpackHalf2x16(cov.x), u2 = unpackHalf2x16(cov.y), u3 = unpackHalf2x16(cov.z); - mat3 Vrk = mat3(u1.x, u1.y, u2.x, u1.y, u2.y, u3.x, u2.x, u3.x, u3.y); +mat3 compute_cov3d(vec3 scale, vec4 rot) { + mat3 S = mat3( + scale.x, 0.0, 0.0, + 0.0, scale.y, 0.0, + 0.0, 0.0, scale.z + ); + mat3 R = mat3( + 1.0 - 2.0 * (rot.z * rot.z + rot.w * rot.w), 2.0 * (rot.y * rot.z - rot.x * rot.w), 2.0 * (rot.y * rot.w + rot.x * rot.z), + 2.0 * (rot.y * rot.z + rot.x * rot.w), 1.0 - 2.0 * (rot.y * rot.y + rot.w * rot.w), 2.0 * (rot.z * rot.w - rot.x * rot.y), + 2.0 * (rot.y * rot.w - rot.x * rot.z), 2.0 * (rot.z * rot.w + rot.x * rot.y), 1.0 - 2.0 * (rot.y * rot.y + rot.z * rot.z) + ); + mat3 M = S * R; + return transpose(M) * M; +} +vec3 compute_cov2d(vec3 center, vec3 scale, vec4 rot){ + mat3 Vrk = compute_cov3d(scale, rot); + vec4 t = view * vec4(center, 1.0); + vec2 lims = 1.3 * 0.5 * viewport / focal; + t.xy = min(lims, max(-lims, t.xy / t.z)) * t.z; mat3 J = mat3( - focal.x / cam.z, 0., -(focal.x * cam.x) / (cam.z * cam.z), - 0., -focal.y / cam.z, (focal.y * cam.y) / (cam.z * cam.z), + focal.x / t.z, 0., -(focal.x * t.x) / (t.z * t.z), + 0., focal.y / t.z, -(focal.y * t.y) / (t.z * t.z), 0., 0., 0. ); + mat3 W = transpose(mat3(view)); + mat3 T = W * J; + mat3 cov = transpose(T) * transpose(Vrk) * T; + return vec3(cov[0][0] + 0.3, cov[0][1], cov[1][1] + 0.3); +} - mat3 T = transpose(mat3(view)) * J; - mat3 cov2d = transpose(T) * Vrk * T; - - float mid = (cov2d[0][0] + cov2d[1][1]) / 2.0; - float radius = length(vec2((cov2d[0][0] - cov2d[1][1]) / 2.0, cov2d[0][1])); - float lambda1 = mid + radius, lambda2 = mid - radius; - - if(lambda2 < 0.0) return; - vec2 diagonalVector = normalize(vec2(cov2d[0][1], lambda1 - cov2d[0][0])); - vec2 majorAxis = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector; - vec2 minorAxis = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x); - - vColor = clamp(pos2d.z/pos2d.w+1.0, 0.0, 1.0) * vec4((cov.w) & 0xffu, (cov.w >> 8) & 0xffu, (cov.w >> 16) & 0xffu, (cov.w >> 24) & 0xffu) / 255.0; - vPosition = position; - - vec2 vCenter = vec2(pos2d) / pos2d.w; - gl_Position = vec4( - vCenter - + position.x * majorAxis / viewport - + position.y * minorAxis / viewport, 0.0, 1.0); - +void main () { + vec4 camspace = view * vec4(center, 1); + vec4 pos2d = projection * mat4(1,0,0,0,0,-1,0,0,0,0,1,0,0,0,0,1) * camspace; + + vec3 cov2d = compute_cov2d(center, scale, quat); + float det = cov2d.x * cov2d.z - cov2d.y * cov2d.y; + vec3 conic = vec3(cov2d.z, cov2d.y, cov2d.x) / det; + float mid = 0.5 * (cov2d.x + cov2d.z); + float lambda1 = mid + sqrt(max(0.1, mid * mid - det)); + float lambda2 = mid - sqrt(max(0.1, mid * mid - det)); + vec2 v1 = 7.0 * sqrt(lambda1) * normalize(vec2(cov2d.y, lambda1 - cov2d.x)); + vec2 v2 = 7.0 * sqrt(lambda2) * normalize(vec2(-(lambda1 - cov2d.x),cov2d.y)); + + vColor = color; + vConic = conic; + vCenter = vec2(pos2d) / pos2d.w; + + vPosition = vec2(vCenter + position.x * (position.y < 0.0 ? v1 : v2) / viewport); + gl_Position = vec4(vPosition, pos2d.z / pos2d.w, 1); } -`.trim(); +`; const fragmentShaderSource = ` -#version 300 es -precision highp float; +precision mediump float; -in vec4 vColor; -in vec2 vPosition; +varying vec4 vColor; +varying vec3 vConic; +varying vec2 vCenter; +uniform vec2 viewport; +uniform vec2 focal; -out vec4 fragColor; +void main () { + vec2 d = (vCenter - 2.0 * (gl_FragCoord.xy/viewport - vec2(0.5, 0.5))) * viewport * 0.5; + float power = -0.5 * (vConic.x * d.x * d.x + vConic.z * d.y * d.y) - vConic.y * d.x * d.y; + if (power > 0.0) discard; + float alpha = min(0.99, vColor.a * exp(power)); + if(alpha < 0.02) discard; -void main () { - float A = -dot(vPosition, vPosition); - if (A < -4.0) discard; - float B = exp(A) * vColor.a; - fragColor = vec4(B * vColor.rgb, B); + gl_FragColor = vec4(alpha * vColor.rgb, alpha); } - -`.trim(); +`; let defaultViewMatrix = [ - 0.47, 0.04, 0.88, 0, -0.11, 0.99, 0.02, 0, -0.88, -0.11, 0.47, 0, 0.07, - 0.03, 6.55, 1, + 0.47, 0.04, 0.88, 0, -0.11, 0.99, 0.02, 0, -0.88, -0.11, 0.47, 0, 0.07, + 0.03, 6.55, 1, ]; let viewMatrix = defaultViewMatrix; -async function main() { - let carousel = true; - const params = new URLSearchParams(location.search); - try { - viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); - carousel = false; - } catch (err) {} - const url = new URL( - // "nike.splat", - // location.href, - params.get("url") || "train.splat", - "https://huggingface.co/cakewalk/splat-data/resolve/main/", - ); - const req = await fetch(url, { - mode: "cors", // no-cors, *cors, same-origin - credentials: "omit", // include, *same-origin, omit - }); - console.log(req); - if (req.status != 200) - throw new Error(req.status + " Unable to load " + req.url); - - const rowLength = 3 * 4 + 3 * 4 + 4 + 4; - const reader = req.body.getReader(); - let splatData = new Uint8Array(req.headers.get("content-length")); - - const downsample = - splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; - console.log(splatData.length / rowLength, downsample); - - const worker = new Worker( - URL.createObjectURL( - new Blob(["(", createWorker.toString(), ")(self)"], { - type: "application/javascript", - }), - ), - ); - const canvas = document.getElementById("canvas"); - const fps = document.getElementById("fps"); - const camid = document.getElementById("camid"); - - let projectionMatrix; - - const gl = canvas.getContext("webgl2", { - antialias: false, - }); - - const vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vertexShaderSource); - gl.compileShader(vertexShader); - if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) - console.error(gl.getShaderInfoLog(vertexShader)); - - const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fragmentShaderSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) - console.error(gl.getShaderInfoLog(fragmentShader)); - - const program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - gl.useProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) - console.error(gl.getProgramInfoLog(program)); - - gl.disable(gl.DEPTH_TEST); // Disable depth testing - - // Enable blending - gl.enable(gl.BLEND); - gl.blendFuncSeparate( - gl.ONE_MINUS_DST_ALPHA, - gl.ONE, - gl.ONE_MINUS_DST_ALPHA, - gl.ONE, - ); - gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); - - const u_projection = gl.getUniformLocation(program, "projection"); - const u_viewport = gl.getUniformLocation(program, "viewport"); - const u_focal = gl.getUniformLocation(program, "focal"); - const u_view = gl.getUniformLocation(program, "view"); - - // positions - const triangleVertices = new Float32Array([-2, -2, 2, -2, 2, 2, -2, 2]); - const vertexBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); - gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW); - const a_position = gl.getAttribLocation(program, "position"); - gl.enableVertexAttribArray(a_position); - gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); - gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0); - - var texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - - var u_textureLocation = gl.getUniformLocation(program, "u_texture"); - gl.uniform1i(u_textureLocation, 0); - - const indexBuffer = gl.createBuffer(); - const a_index = gl.getAttribLocation(program, "index"); - gl.enableVertexAttribArray(a_index); - gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer); - gl.vertexAttribIPointer(a_index, 1, gl.INT, false, 0, 0); - gl.vertexAttribDivisor(a_index, 1); - - const resize = () => { - gl.uniform2fv(u_focal, new Float32Array([camera.fx, camera.fy])); - - projectionMatrix = getProjectionMatrix( - camera.fx, - camera.fy, - innerWidth, - innerHeight, - ); - - gl.uniform2fv(u_viewport, new Float32Array([innerWidth, innerHeight])); - - gl.canvas.width = Math.round(innerWidth / downsample); - gl.canvas.height = Math.round(innerHeight / downsample); - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - - gl.uniformMatrix4fv(u_projection, false, projectionMatrix); - }; - - window.addEventListener("resize", resize); - resize(); - - worker.onmessage = (e) => { - if (e.data.buffer) { - splatData = new Uint8Array(e.data.buffer); - if (e.data.save) { - const blob = new Blob([splatData.buffer], { - type: "application/octet-stream", - }); - const link = document.createElement("a"); - link.download = "model.splat"; - link.href = URL.createObjectURL(blob); - document.body.appendChild(link); - link.click(); - } - } else if (e.data.texdata) { - const { texdata, texwidth, texheight } = e.data; - // console.log(texdata) - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_WRAP_S, - gl.CLAMP_TO_EDGE, - ); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_WRAP_T, - gl.CLAMP_TO_EDGE, - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA32UI, - texwidth, - texheight, - 0, - gl.RGBA_INTEGER, - gl.UNSIGNED_INT, - texdata, - ); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - } else if (e.data.depthIndex) { - const { depthIndex, viewProj } = e.data; - gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer); - gl.bufferData(gl.ARRAY_BUFFER, depthIndex, gl.DYNAMIC_DRAW); - vertexCount = e.data.vertexCount; - } - }; - - let activeKeys = []; - let currentCameraIndex = 0; - - window.addEventListener("keydown", (e) => { - // if (document.activeElement != document.body) return; - carousel = false; - if (!activeKeys.includes(e.code)) activeKeys.push(e.code); - if (/\d/.test(e.key)) { - currentCameraIndex = parseInt(e.key); - camera = cameras[currentCameraIndex]; - viewMatrix = getViewMatrix(camera); - } - if (["-", "_"].includes(e.key)) { - currentCameraIndex = - (currentCameraIndex + cameras.length - 1) % cameras.length; - viewMatrix = getViewMatrix(cameras[currentCameraIndex]); - } - if (["+", "="].includes(e.key)) { - currentCameraIndex = (currentCameraIndex + 1) % cameras.length; - viewMatrix = getViewMatrix(cameras[currentCameraIndex]); - } - camid.innerText = "cam " + currentCameraIndex; - if (e.code == "KeyV") { - location.hash = - "#" + - JSON.stringify( - viewMatrix.map((k) => Math.round(k * 100) / 100), - ); - camid.innerText = ""; - } else if (e.code === "KeyP") { - carousel = true; - camid.innerText = ""; - } - }); - window.addEventListener("keyup", (e) => { - activeKeys = activeKeys.filter((k) => k !== e.code); - }); - window.addEventListener("blur", () => { - activeKeys = []; - }); - - window.addEventListener( - "wheel", - (e) => { - carousel = false; - e.preventDefault(); - const lineHeight = 10; - const scale = - e.deltaMode == 1 - ? lineHeight - : e.deltaMode == 2 - ? innerHeight - : 1; - let inv = invert4(viewMatrix); - if (e.shiftKey) { - inv = translate4( - inv, - (e.deltaX * scale) / innerWidth, - (e.deltaY * scale) / innerHeight, - 0, - ); - } else if (e.ctrlKey || e.metaKey) { - // inv = rotate4(inv, (e.deltaX * scale) / innerWidth, 0, 0, 1); - // inv = translate4(inv, 0, (e.deltaY * scale) / innerHeight, 0); - // let preY = inv[13]; - inv = translate4( - inv, - 0, - 0, - (-10 * (e.deltaY * scale)) / innerHeight, - ); - // inv[13] = preY; - } else { - let d = 4; - inv = translate4(inv, 0, 0, d); - inv = rotate4(inv, -(e.deltaX * scale) / innerWidth, 0, 1, 0); - inv = rotate4(inv, (e.deltaY * scale) / innerHeight, 1, 0, 0); - inv = translate4(inv, 0, 0, -d); - } - - viewMatrix = invert4(inv); - }, - { passive: false }, - ); - - let startX, startY, down; - canvas.addEventListener("mousedown", (e) => { - carousel = false; - e.preventDefault(); - startX = e.clientX; - startY = e.clientY; - down = e.ctrlKey || e.metaKey ? 2 : 1; - }); - canvas.addEventListener("contextmenu", (e) => { - carousel = false; - e.preventDefault(); - startX = e.clientX; - startY = e.clientY; - down = 2; - }); - - canvas.addEventListener("mousemove", (e) => { - e.preventDefault(); - if (down == 1) { - let inv = invert4(viewMatrix); - let dx = (5 * (e.clientX - startX)) / innerWidth; - let dy = (5 * (e.clientY - startY)) / innerHeight; - let d = 4; - - inv = translate4(inv, 0, 0, d); - inv = rotate4(inv, dx, 0, 1, 0); - inv = rotate4(inv, -dy, 1, 0, 0); - inv = translate4(inv, 0, 0, -d); - // let postAngle = Math.atan2(inv[0], inv[10]) - // inv = rotate4(inv, postAngle - preAngle, 0, 0, 1) - // console.log(postAngle) - viewMatrix = invert4(inv); - - startX = e.clientX; - startY = e.clientY; - } else if (down == 2) { - let inv = invert4(viewMatrix); - // inv = rotateY(inv, ); - // let preY = inv[13]; - inv = translate4( - inv, - (-10 * (e.clientX - startX)) / innerWidth, - 0, - (10 * (e.clientY - startY)) / innerHeight, - ); - // inv[13] = preY; - viewMatrix = invert4(inv); - - startX = e.clientX; - startY = e.clientY; - } - }); - canvas.addEventListener("mouseup", (e) => { - e.preventDefault(); - down = false; - startX = 0; - startY = 0; - }); - - let altX = 0, - altY = 0; - canvas.addEventListener( - "touchstart", - (e) => { - e.preventDefault(); - if (e.touches.length === 1) { - carousel = false; - startX = e.touches[0].clientX; - startY = e.touches[0].clientY; - down = 1; - } else if (e.touches.length === 2) { - // console.log('beep') - carousel = false; - startX = e.touches[0].clientX; - altX = e.touches[1].clientX; - startY = e.touches[0].clientY; - altY = e.touches[1].clientY; - down = 1; - } - }, - { passive: false }, - ); - canvas.addEventListener( - "touchmove", - (e) => { - e.preventDefault(); - if (e.touches.length === 1 && down) { - let inv = invert4(viewMatrix); - let dx = (4 * (e.touches[0].clientX - startX)) / innerWidth; - let dy = (4 * (e.touches[0].clientY - startY)) / innerHeight; - - let d = 4; - inv = translate4(inv, 0, 0, d); - // inv = translate4(inv, -x, -y, -z); - // inv = translate4(inv, x, y, z); - inv = rotate4(inv, dx, 0, 1, 0); - inv = rotate4(inv, -dy, 1, 0, 0); - inv = translate4(inv, 0, 0, -d); - - viewMatrix = invert4(inv); - - startX = e.touches[0].clientX; - startY = e.touches[0].clientY; - } else if (e.touches.length === 2) { - // alert('beep') - const dtheta = - Math.atan2(startY - altY, startX - altX) - - Math.atan2( - e.touches[0].clientY - e.touches[1].clientY, - e.touches[0].clientX - e.touches[1].clientX, - ); - const dscale = - Math.hypot(startX - altX, startY - altY) / - Math.hypot( - e.touches[0].clientX - e.touches[1].clientX, - e.touches[0].clientY - e.touches[1].clientY, - ); - const dx = - (e.touches[0].clientX + - e.touches[1].clientX - - (startX + altX)) / - 2; - const dy = - (e.touches[0].clientY + - e.touches[1].clientY - - (startY + altY)) / - 2; - let inv = invert4(viewMatrix); - // inv = translate4(inv, 0, 0, d); - inv = rotate4(inv, dtheta, 0, 0, 1); - - inv = translate4(inv, -dx / innerWidth, -dy / innerHeight, 0); - - // let preY = inv[13]; - inv = translate4(inv, 0, 0, 3 * (1 - dscale)); - // inv[13] = preY; - - viewMatrix = invert4(inv); - - startX = e.touches[0].clientX; - altX = e.touches[1].clientX; - startY = e.touches[0].clientY; - altY = e.touches[1].clientY; - } - }, - { passive: false }, - ); - canvas.addEventListener( - "touchend", - (e) => { - e.preventDefault(); - down = false; - startX = 0; - startY = 0; - }, - { passive: false }, +async function main() { + let carousel = true; + const params = new URLSearchParams(location.search); + try { + viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); + carousel = false; + } catch (err) {} + const url = new URL( + params.get("url") || "output.splat", + location.href, // 중요: 여기를 location.href로 바꿔야 합니다! ); - - let jumpDelta = 0; - let vertexCount = 0; - - let lastFrame = 0; - let avgFps = 0; - let start = 0; - - window.addEventListener("gamepadconnected", (e) => { - const gp = navigator.getGamepads()[e.gamepad.index]; - console.log( - `Gamepad connected at index ${gp.index}: ${gp.id}. It has ${gp.buttons.length} buttons and ${gp.axes.length} axes.`, - ); - }); - window.addEventListener("gamepaddisconnected", (e) => { - console.log("Gamepad disconnected"); - }); - - let leftGamepadTrigger, rightGamepadTrigger; - - const frame = (now) => { - let inv = invert4(viewMatrix); - let shiftKey = - activeKeys.includes("Shift") || - activeKeys.includes("ShiftLeft") || - activeKeys.includes("ShiftRight"); - - if (activeKeys.includes("ArrowUp")) { - if (shiftKey) { - inv = translate4(inv, 0, -0.03, 0); - } else { - inv = translate4(inv, 0, 0, 0.1); - } - } - if (activeKeys.includes("ArrowDown")) { - if (shiftKey) { - inv = translate4(inv, 0, 0.03, 0); - } else { - inv = translate4(inv, 0, 0, -0.1); - } - } - if (activeKeys.includes("ArrowLeft")) - inv = translate4(inv, -0.03, 0, 0); - // - if (activeKeys.includes("ArrowRight")) - inv = translate4(inv, 0.03, 0, 0); - // inv = rotate4(inv, 0.01, 0, 1, 0); - if (activeKeys.includes("KeyA")) inv = rotate4(inv, -0.01, 0, 1, 0); - if (activeKeys.includes("KeyD")) inv = rotate4(inv, 0.01, 0, 1, 0); - if (activeKeys.includes("KeyQ")) inv = rotate4(inv, 0.01, 0, 0, 1); - if (activeKeys.includes("KeyE")) inv = rotate4(inv, -0.01, 0, 0, 1); - if (activeKeys.includes("KeyW")) inv = rotate4(inv, 0.005, 1, 0, 0); - if (activeKeys.includes("KeyS")) inv = rotate4(inv, -0.005, 1, 0, 0); - - const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; - let isJumping = activeKeys.includes("Space"); - for (let gamepad of gamepads) { - if (!gamepad) continue; - - const axisThreshold = 0.1; // Threshold to detect when the axis is intentionally moved - const moveSpeed = 0.06; - const rotateSpeed = 0.02; - - // Assuming the left stick controls translation (axes 0 and 1) - if (Math.abs(gamepad.axes[0]) > axisThreshold) { - inv = translate4(inv, moveSpeed * gamepad.axes[0], 0, 0); - carousel = false; - } - if (Math.abs(gamepad.axes[1]) > axisThreshold) { - inv = translate4(inv, 0, 0, -moveSpeed * gamepad.axes[1]); - carousel = false; - } - if (gamepad.buttons[12].pressed || gamepad.buttons[13].pressed) { - inv = translate4( - inv, - 0, - -moveSpeed * - (gamepad.buttons[12].pressed - - gamepad.buttons[13].pressed), - 0, - ); - carousel = false; - } - - if (gamepad.buttons[14].pressed || gamepad.buttons[15].pressed) { - inv = translate4( - inv, - -moveSpeed * - (gamepad.buttons[14].pressed - - gamepad.buttons[15].pressed), - 0, - 0, - ); - carousel = false; - } - - // Assuming the right stick controls rotation (axes 2 and 3) - if (Math.abs(gamepad.axes[2]) > axisThreshold) { - inv = rotate4(inv, rotateSpeed * gamepad.axes[2], 0, 1, 0); - carousel = false; - } - if (Math.abs(gamepad.axes[3]) > axisThreshold) { - inv = rotate4(inv, -rotateSpeed * gamepad.axes[3], 1, 0, 0); - carousel = false; - } - - let tiltAxis = gamepad.buttons[6].value - gamepad.buttons[7].value; - if (Math.abs(tiltAxis) > axisThreshold) { - inv = rotate4(inv, rotateSpeed * tiltAxis, 0, 0, 1); - carousel = false; - } - if (gamepad.buttons[4].pressed && !leftGamepadTrigger) { - camera = - cameras[(cameras.indexOf(camera) + 1) % cameras.length]; - inv = invert4(getViewMatrix(camera)); - carousel = false; - } - if (gamepad.buttons[5].pressed && !rightGamepadTrigger) { - camera = - cameras[ - (cameras.indexOf(camera) + cameras.length - 1) % - cameras.length - ]; - inv = invert4(getViewMatrix(camera)); - carousel = false; - } - leftGamepadTrigger = gamepad.buttons[4].pressed; - rightGamepadTrigger = gamepad.buttons[5].pressed; - if (gamepad.buttons[0].pressed) { - isJumping = true; - carousel = false; - } - if (gamepad.buttons[3].pressed) { - carousel = true; - } - } - - if ( - ["KeyJ", "KeyK", "KeyL", "KeyI"].some((k) => activeKeys.includes(k)) - ) { - let d = 4; - inv = translate4(inv, 0, 0, d); - inv = rotate4( - inv, - activeKeys.includes("KeyJ") - ? -0.05 - : activeKeys.includes("KeyL") - ? 0.05 - : 0, - 0, - 1, - 0, - ); - inv = rotate4( - inv, - activeKeys.includes("KeyI") - ? 0.05 - : activeKeys.includes("KeyK") - ? -0.05 - : 0, - 1, - 0, - 0, - ); - inv = translate4(inv, 0, 0, -d); - } - - viewMatrix = invert4(inv); - - if (carousel) { - let inv = invert4(defaultViewMatrix); - - const t = Math.sin((Date.now() - start) / 5000); - inv = translate4(inv, 2.5 * t, 0, 6 * (1 - Math.cos(t))); - inv = rotate4(inv, -0.6 * t, 0, 1, 0); - - viewMatrix = invert4(inv); - } - - if (isJumping) { - jumpDelta = Math.min(1, jumpDelta + 0.05); - } else { - jumpDelta = Math.max(0, jumpDelta - 0.05); - } - - let inv2 = invert4(viewMatrix); - inv2 = translate4(inv2, 0, -jumpDelta, 0); - inv2 = rotate4(inv2, -0.1 * jumpDelta, 1, 0, 0); - let actualViewMatrix = invert4(inv2); - - const viewProj = multiply4(projectionMatrix, actualViewMatrix); - worker.postMessage({ view: viewProj }); - - const currentFps = 1000 / (now - lastFrame) || 0; - avgFps = avgFps * 0.9 + currentFps * 0.1; - - if (vertexCount > 0) { - document.getElementById("spinner").style.display = "none"; - gl.uniformMatrix4fv(u_view, false, actualViewMatrix); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, vertexCount); - } else { - gl.clear(gl.COLOR_BUFFER_BIT); - document.getElementById("spinner").style.display = ""; - start = Date.now() + 2000; - } - const progress = (100 * vertexCount) / (splatData.length / rowLength); - if (progress < 100) { - document.getElementById("progress").style.width = progress + "%"; - } else { - document.getElementById("progress").style.display = "none"; - } - fps.innerText = Math.round(avgFps) + " fps"; - if (isNaN(currentCameraIndex)) { - camid.innerText = ""; - } - lastFrame = now; - requestAnimationFrame(frame); - }; - - frame(); - - const isPly = (splatData) => - splatData[0] == 112 && - splatData[1] == 108 && - splatData[2] == 121 && - splatData[3] == 10; - - const selectFile = (file) => { - const fr = new FileReader(); - if (/\.json$/i.test(file.name)) { - fr.onload = () => { - cameras = JSON.parse(fr.result); - viewMatrix = getViewMatrix(cameras[0]); - projectionMatrix = getProjectionMatrix( - camera.fx / downsample, - camera.fy / downsample, - canvas.width, - canvas.height, - ); - gl.uniformMatrix4fv(u_projection, false, projectionMatrix); - - console.log("Loaded Cameras"); - }; - fr.readAsText(file); - } else { - stopLoading = true; - fr.onload = () => { - splatData = new Uint8Array(fr.result); - console.log("Loaded", Math.floor(splatData.length / rowLength)); - - if (isPly(splatData)) { - // ply file magic header means it should be handled differently - worker.postMessage({ ply: splatData.buffer, save: true }); - } else { - worker.postMessage({ - buffer: splatData.buffer, - vertexCount: Math.floor(splatData.length / rowLength), - }); - } - }; - fr.readAsArrayBuffer(file); - } - }; - - window.addEventListener("hashchange", (e) => { - try { - viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); - carousel = false; - } catch (err) {} - }); - - const preventDefault = (e) => { - e.preventDefault(); - e.stopPropagation(); - }; - document.addEventListener("dragenter", preventDefault); - document.addEventListener("dragover", preventDefault); - document.addEventListener("dragleave", preventDefault); - document.addEventListener("drop", (e) => { - e.preventDefault(); - e.stopPropagation(); - selectFile(e.dataTransfer.files[0]); - }); - - let bytesRead = 0; - let lastVertexCount = -1; - let stopLoading = false; - - while (true) { - const { done, value } = await reader.read(); - if (done || stopLoading) break; - - splatData.set(value, bytesRead); - bytesRead += value.length; - - if (vertexCount > lastVertexCount) { - if (!isPly(splatData)) { - worker.postMessage({ - buffer: splatData.buffer, - vertexCount: Math.floor(bytesRead / rowLength), - }); - } - lastVertexCount = vertexCount; - } - } - if (!stopLoading) { - if (isPly(splatData)) { - // ply file magic header means it should be handled differently - worker.postMessage({ ply: splatData.buffer, save: false }); - } else { - worker.postMessage({ - buffer: splatData.buffer, - vertexCount: Math.floor(bytesRead / rowLength), - }); - } - } + const req = await fetch(url, { + mode: "cors", // no-cors, *cors, same-origin + credentials: "omit", // include, *same-origin, omit + }); + console.log(req); + if (req.status != 200) + throw new Error(req.status + " Unable to load " + req.url); + + const rowLength = 3 * 4 + 3 * 4 + 4 + 4; + const reader = req.body.getReader(); + let splatData = new Uint8Array(req.headers.get("content-length")); + + const worker = new Worker( + URL.createObjectURL( + new Blob(["(", createWorker.toString(), ")(self)"], { + type: "application/javascript", + }), + ), + ); + + const canvas = document.createElement("canvas"); + canvas.width = innerWidth / 2; + canvas.height = innerHeight / 2; + canvas.style.position = "absolute"; + canvas.style.top = 0; + canvas.style.left = 0; + canvas.style.width = "100%"; + canvas.style.height = "100%"; + document.body.appendChild(canvas); + + const fps = document.createElement("div"); + fps.style.position = "absolute"; + fps.style.bottom = "10px"; + fps.style.right = "10px"; + document.body.appendChild(fps); + + let projectionMatrix = getProjectionMatrix( + camera.fx / 2, + camera.fy / 2, + canvas.width, + canvas.height, + ); + + const gl = canvas.getContext("webgl"); + const ext = gl.getExtension("ANGLE_instanced_arrays"); + + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexShaderSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) + console.error(gl.getShaderInfoLog(vertexShader)); + + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentShaderSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) + console.error(gl.getShaderInfoLog(fragmentShader)); + + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + gl.useProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) + console.error(gl.getProgramInfoLog(program)); + + gl.disable(gl.DEPTH_TEST); // Disable depth testing + + // Enable blending + gl.enable(gl.BLEND); + + // Set blending function + gl.blendFuncSeparate( + gl.ONE_MINUS_DST_ALPHA, + gl.ONE, + gl.ONE_MINUS_DST_ALPHA, + gl.ONE, + ); + + // Set blending equation + gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); + + // projection + const u_projection = gl.getUniformLocation(program, "projection"); + gl.uniformMatrix4fv(u_projection, false, projectionMatrix); + + // viewport + const u_viewport = gl.getUniformLocation(program, "viewport"); + gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height])); + + // focal + const u_focal = gl.getUniformLocation(program, "focal"); + gl.uniform2fv(u_focal, new Float32Array([camera.fx / 2, camera.fy / 2])); + + // view + const u_view = gl.getUniformLocation(program, "view"); + gl.uniformMatrix4fv(u_view, false, viewMatrix); + + // positions + const triangleVertices = new Float32Array([1, -1, 1, 1, -1, 1, -1, -1]); + const vertexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW); + const a_position = gl.getAttribLocation(program, "position"); + gl.enableVertexAttribArray(a_position); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0); + + // center + const centerBuffer = gl.createBuffer(); + // gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer); + // gl.bufferData(gl.ARRAY_BUFFER, center, gl.STATIC_DRAW); + const a_center = gl.getAttribLocation(program, "center"); + gl.enableVertexAttribArray(a_center); + gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer); + gl.vertexAttribPointer(a_center, 3, gl.FLOAT, false, 0, 0); + ext.vertexAttribDivisorANGLE(a_center, 1); // Use the extension here + + // color + const colorBuffer = gl.createBuffer(); + // gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + // gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW); + const a_color = gl.getAttribLocation(program, "color"); + gl.enableVertexAttribArray(a_color); + gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + gl.vertexAttribPointer(a_color, 4, gl.FLOAT, false, 0, 0); + ext.vertexAttribDivisorANGLE(a_color, 1); // Use the extension here + + // quat + const quatBuffer = gl.createBuffer(); + // gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer); + // gl.bufferData(gl.ARRAY_BUFFER, quat, gl.STATIC_DRAW); + const a_quat = gl.getAttribLocation(program, "quat"); + gl.enableVertexAttribArray(a_quat); + gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer); + gl.vertexAttribPointer(a_quat, 4, gl.FLOAT, false, 0, 0); + ext.vertexAttribDivisorANGLE(a_quat, 1); // Use the extension here + + // scale + const scaleBuffer = gl.createBuffer(); + // gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer); + // gl.bufferData(gl.ARRAY_BUFFER, scale, gl.STATIC_DRAW); + const a_scale = gl.getAttribLocation(program, "scale"); + gl.enableVertexAttribArray(a_scale); + gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer); + gl.vertexAttribPointer(a_scale, 3, gl.FLOAT, false, 0, 0); + ext.vertexAttribDivisorANGLE(a_scale, 1); // Use the extension here + + worker.onmessage = (e) => { + if (e.data.buffer) { + splatData = new Uint8Array(e.data.buffer); + const blob = new Blob([splatData.buffer], { + type: "application/octet-stream", + }); + const link = document.createElement("a"); + link.download = "model.splat"; + link.href = URL.createObjectURL(blob); + document.body.appendChild(link); + link.click(); + } else { + let { quat, scale, center, color } = e.data; + vertexCount = quat.length / 4; + + gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer); + gl.bufferData(gl.ARRAY_BUFFER, center, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer); + gl.bufferData(gl.ARRAY_BUFFER, quat, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer); + gl.bufferData(gl.ARRAY_BUFFER, scale, gl.STATIC_DRAW); + } + }; + + let activeKeys = []; + + window.addEventListener("keydown", (e) => { + carousel = false; + if (!activeKeys.includes(e.key)) activeKeys.push(e.key); + if (/\d/.test(e.key)) { + viewMatrix = getViewMatrix(cameras[parseInt(e.key)]); + } + if (e.key == "v") { + location.hash = + "#" + + JSON.stringify( + viewMatrix.map((k) => Math.round(k * 100) / 100), + ); + } else if (e.key === "p") { + carousel = true; + } + }); + window.addEventListener("keyup", (e) => { + activeKeys = activeKeys.filter((k) => k !== e.key); + }); + + window.addEventListener( + "wheel", + (e) => { + carousel = false; + e.preventDefault(); + const lineHeight = 10; + const scale = + e.deltaMode == 1 + ? lineHeight + : e.deltaMode == 2 + ? innerHeight + : 1; + const dy = e.deltaY * scale; + let inv = invert4(viewMatrix); + inv = translate4(inv, 0, 0, (-5 * dy) / innerHeight); + viewMatrix = invert4(inv); + }, + { passive: false }, + ); + + let startX, startY, down; + document.addEventListener("mousedown", (e) => { + carousel = false; + e.preventDefault(); + startX = e.clientX; + startY = e.clientY; + down = 1; + }); + document.addEventListener("contextmenu", (e) => { + carousel = false; + e.preventDefault(); + startX = e.clientX; + startY = e.clientY; + down = 2; + }); + document.addEventListener("mousemove", (e) => { + e.preventDefault(); + if (down == 1) { + let inv = invert4(viewMatrix); + inv = rotate4( + inv, + (-2 * (e.clientX - startX)) / innerWidth, + 0, + 1, + 0, + ); + inv = translate4( + inv, + (5 * (e.clientX - startX)) / innerWidth, + 0, + 0, + ); + inv = translate4( + inv, + 0, + 0, + (10 * (e.clientY - startY)) / innerHeight, + ); + viewMatrix = invert4(inv); + + startX = e.clientX; + startY = e.clientY; + } else if (down == 2) { + let inv = invert4(viewMatrix); + // inv = rotateY(inv, ); + inv = translate4( + inv, + (-10 * (e.clientX - startX)) / innerWidth, + (-10 * (e.clientY - startY)) / innerHeight, + 0, + ); + viewMatrix = invert4(inv); + + startX = e.clientX; + startY = e.clientY; + } + }); + document.addEventListener("mouseup", (e) => { + e.preventDefault(); + down = false; + startX = 0; + startY = 0; + }); + + document.addEventListener( + "touchstart", + (e) => { + e.preventDefault(); + if (e.touches.length === 1) { + carousel = false; + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + down = 1; + } + }, + { passive: false }, + ); + document.addEventListener( + "touchmove", + (e) => { + e.preventDefault(); + if (e.touches.length === 1 && down) { + let inv = invert4(viewMatrix); + inv = rotate4( + inv, + (-0.6 * (e.touches[0].clientX - startX)) / innerWidth, + 0, + 1, + 0, + ); + inv = translate4( + inv, + 0, + 0, + (25 * (e.touches[0].clientY - startY)) / innerHeight, + ); + viewMatrix = invert4(inv); + + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + } + }, + { passive: false }, + ); + document.addEventListener( + "touchend", + (e) => { + e.preventDefault(); + down = false; + startX = 0; + startY = 0; + }, + { passive: false }, + ); + + let jumpDelta = 0; + let vertexCount = 0; + + let lastFrame = 0; + let avgFps = 0; + let start = Date.now() + 2000; + + const frame = (now) => { + let inv = invert4(viewMatrix); + // let preY = inv[13]; + if (activeKeys.includes("ArrowUp")) inv = translate4(inv, 0, 0, 0.1); + if (activeKeys.includes("ArrowDown")) inv = translate4(inv, 0, 0, -0.1); + if (activeKeys.includes("ArrowLeft")) + inv = rotate4(inv, -0.01, 0, 1, 0); + if (activeKeys.includes("ArrowRight")) + inv = rotate4(inv, 0.01, 0, 1, 0); + if (activeKeys.includes("a")) inv = translate4(inv, -0.02, 0, 0); + if (activeKeys.includes("d")) inv = translate4(inv, 0.02, 0, 0); + if (activeKeys.includes("q")) inv = rotate4(inv, 0.01, 0, 0, 1); + if (activeKeys.includes("e")) inv = rotate4(inv, -0.01, 0, 0, 1); + if (activeKeys.includes("w")) inv = rotate4(inv, 0.005, 1, 0, 0); + if (activeKeys.includes("s")) inv = rotate4(inv, -0.005, 1, 0, 0); + // inv[13] = preY; + viewMatrix = invert4(inv); + + if (carousel) { + let inv = invert4(defaultViewMatrix); + + const t = Math.sin((Date.now() - start) / 5000); + inv = translate4(inv, 2.5 * t, 0, 10 * (1 - Math.cos(t))); + inv = rotate4(inv, -0.4 * t, 0, 1, 0); + + viewMatrix = invert4(inv); + } + + if (activeKeys.includes(" ")) { + jumpDelta = Math.min(1, jumpDelta + 0.05); + } else { + jumpDelta = Math.max(0, jumpDelta - 0.05); + } + + let inv2 = invert4(viewMatrix); + inv2[13] -= jumpDelta; + inv2 = rotate4(inv2, -0.2 * jumpDelta, 1, 0, 0); + let actualViewMatrix = invert4(inv2); + + const viewProj = multiply4(projectionMatrix, actualViewMatrix); + worker.postMessage({ view: viewProj }); + + const currentFps = 1000 / (now - lastFrame) || 0; + avgFps = avgFps * 0.9 + currentFps * 0.1; + + if (vertexCount > 0) { + document.getElementById("spinner").style.display = "none"; + gl.uniformMatrix4fv(u_view, false, actualViewMatrix); + ext.drawArraysInstancedANGLE(gl.TRIANGLE_STRIP, 0, 4, vertexCount); + } else { + gl.clear(gl.COLOR_BUFFER_BIT); + document.getElementById("spinner").style.display = ""; + } + const progress = (100 * vertexCount) / (splatData.length / rowLength); + if (progress < 100) { + document.getElementById("progress").style.width = progress + "%"; + } else { + document.getElementById("progress").style.display = "none"; + } + fps.innerText = Math.round(avgFps) + " fps"; + lastFrame = now; + requestAnimationFrame(frame); + }; + + frame(); + + const selectFile = (file) => { + const fr = new FileReader(); + if (/\.json$/i.test(file.name)) { + fr.onload = () => { + cameras = JSON.parse(fr.result); + viewMatrix = getViewMatrix(cameras[0]); + projectionMatrix = getProjectionMatrix( + camera.fx / 2, + camera.fy / 2, + canvas.width, + canvas.height, + ); + gl.uniformMatrix4fv(u_projection, false, projectionMatrix); + + console.log("Loaded Cameras"); + }; + fr.readAsText(file); + } else { + stopLoading = true; + fr.onload = () => { + splatData = new Uint8Array(fr.result); + console.log("Loaded", Math.floor(splatData.length / rowLength)); + + if ( + splatData[0] == 112 && + splatData[1] == 108 && + splatData[2] == 121 && + splatData[3] == 10 + ) { + // ply file magic header means it should be handled differently + worker.postMessage({ ply: splatData.buffer }); + } else { + worker.postMessage({ + buffer: splatData.buffer, + vertexCount: Math.floor(splatData.length / rowLength), + }); + } + }; + fr.readAsArrayBuffer(file); + } + }; + + window.addEventListener("hashchange", (e) => { + try { + viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); + carousel = false; + } catch (err) {} + }); + + const preventDefault = (e) => { + e.preventDefault(); + e.stopPropagation(); + } + document.addEventListener("dragenter", preventDefault); + document.addEventListener("dragover", preventDefault); + document.addEventListener("dragleave", preventDefault); + document.addEventListener("drop", (e) => { + e.preventDefault(); + e.stopPropagation(); + selectFile(e.dataTransfer.files[0]); + }); + + let bytesRead = 0; + let lastVertexCount = -1; + let stopLoading = false; + + while (true) { + const { done, value } = await reader.read(); + if (done || stopLoading) break; + + splatData.set(value, bytesRead); + bytesRead += value.length; + + if (vertexCount > lastVertexCount) { + worker.postMessage({ + buffer: splatData.buffer, + vertexCount: Math.floor(bytesRead / rowLength), + }); + lastVertexCount = vertexCount; + } + } + if (!stopLoading) + worker.postMessage({ + buffer: splatData.buffer, + vertexCount: Math.floor(bytesRead / rowLength), + }); } main().catch((err) => { - document.getElementById("spinner").style.display = "none"; - document.getElementById("message").innerText = err.toString(); + document.getElementById("spinner").style.display = "none"; + document.getElementById("message").innerText = err.toString(); }); diff --git a/output.splat b/output.splat new file mode 100644 index 0000000..a567066 Binary files /dev/null and b/output.splat differ diff --git a/point_cloud_3.ply b/point_cloud_3.ply new file mode 100644 index 0000000..3f169b5 Binary files /dev/null and b/point_cloud_3.ply differ