├── docs ├── js │ ├── dom-main.js │ └── webgl-util.js ├── laz-perf.wasm ├── bundle.js.LICENSE.txt ├── index.html └── WorkerConsole.js ├── .gitignore ├── src ├── utils │ ├── kd-tree.js │ ├── utils.js │ └── loader.js ├── lru-cache │ └── index.js ├── worker │ ├── createWorker.js │ ├── privateOpenWorker.js │ └── fetcher.worker.js ├── webgpu │ ├── material.js │ └── renderer.js ├── styles │ └── main.css ├── private_origin │ ├── cache_manager.js │ └── file_manager.js ├── helper.js ├── shaders │ └── renderShader.js ├── passiveloader.js ├── octree.js └── index.js ├── README.md ├── .env ├── webpack.config.js └── package.json /docs/js/dom-main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /src/utils/kd-tree.js: -------------------------------------------------------------------------------- 1 | // class Node { 2 | 3 | // } 4 | -------------------------------------------------------------------------------- /docs/laz-perf.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputingElevatedLab/WGPU-COPC-Viewer/HEAD/docs/laz-perf.wasm -------------------------------------------------------------------------------- /docs/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2010-2022 Three.js Authors 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | function deepCopy(obj) { 2 | const newObj = {}; 3 | 4 | for (let key in obj) { 5 | const value = obj[key]; 6 | 7 | if (typeof value === "object" && value !== null) { 8 | newObj[key] = deepCopy(value); // Recursively copy nested objects 9 | } else { 10 | newObj[key] = value; // Copy non-object values as-is 11 | } 12 | } 13 | 14 | return newObj; 15 | } 16 | 17 | export { deepCopy }; 18 | -------------------------------------------------------------------------------- /src/lru-cache/index.js: -------------------------------------------------------------------------------- 1 | import LRUCache from "lru-cache"; 2 | const options = { 3 | max: 500, 4 | // ttl: 100 * 60 * 10, i dont think i need this as the node value wont be time dependent 5 | allowStale: false, 6 | updateAgeOnGet: true, 7 | updateAgeOnHas: true, 8 | }; 9 | 10 | const cache = new LRUCache(options); 11 | // ------------------------------------------------------- 12 | // since LRU Cache is not persistant on reload by default and is in-memory cache we dont need to be worried about clearing 13 | 14 | export { cache }; 15 | -------------------------------------------------------------------------------- /src/utils/loader.js: -------------------------------------------------------------------------------- 1 | class Loader { 2 | constructor(url) { 3 | this.url = url; 4 | } 5 | async loadHeader() { 6 | // loadheader 7 | let loaderByteSize = 549; 8 | } 9 | } 10 | 11 | function computeFocalLength(angle) { 12 | let canvas = document.getElementById("screen-canvas"); 13 | let angleRadian = (angle * Math.PI) / 180; 14 | return canvas.clientHeight * 0.5 * (1 / Math.tan(angleRadian / 2)); 15 | } 16 | 17 | function computeSSE(width, distance, focalLength) { 18 | return (width / distance) * focalLength; 19 | } 20 | 21 | export { computeFocalLength, computeSSE }; 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------------------------- 2 | 3 | # WebGPU COPC Viewer 4 | 5 | to install and run: 6 | 7 | ``` 8 | 1. clone the repo 9 | ``` 10 | ``` 11 | 2. npm install 12 | ``` 13 | 14 | ### Note: 15 | 16 | ```Renderer is made using WebGPU and WGSL and does out of core rendering so check for support of WebGPU to run this code in your browser``` 17 | 18 | 19 | ``` 20 | 3. npm run dev 21 | ``` 22 | Current render output: 23 | 24 | ![image](https://user-images.githubusercontent.com/11494733/232173989-1dac50a2-c4b5-4ef8-adf1-123b2418a0ec.png) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | 2 | tree= { 3 | leafCapacity: 16, 4 | bufferCapacity: 16, 5 | } 6 | 7 | # https://media.githubusercontent.com/media/pravinpoudel/LAZ-COPC-Converter/main/morro_20m-reprojected.copc.laz 8 | 9 | # https://media.githubusercontent.com/media/pravinpoudel/LAZ-COPC-Converter/main/u_campus_30m.copc.laz 10 | # https://media.githubusercontent.com/media/sceneserver/copc/main/naarden-vesting.copc.laz 11 | # https://hobu-lidar.s3.amazonaws.com/sofi.copc.laz 12 | filename= "https://media.githubusercontent.com/media/sceneserver/copc/main/naarden-vesting.copc.laz" 13 | p_cache = "cache-holder" 14 | p_cache_capacity = 150 15 | 16 | -------------------------------------------------------------------------------- /src/worker/createWorker.js: -------------------------------------------------------------------------------- 1 | import Worker from "./fetcher.worker.js"; 2 | import { loadCOPC } from "../index"; 3 | 4 | const workers = new Array(5).fill(null); 5 | for (let i = 0; i < workers.length; i++) { 6 | const fetchWorker = new Worker(); 7 | fetchWorker.onmessage = (event) => { 8 | postMessageRes = event.data; 9 | if (postMessageRes == 200) { 10 | loadCOPC(); 11 | } else { 12 | console.log("received"); 13 | let position = postMessageRes[0]; 14 | let color = postMessageRes[1]; 15 | for (let i = 0; i < position.length; i++) { 16 | positions.push(position[i]); 17 | colors.push(colors[i]); 18 | } 19 | } 20 | }; 21 | workers.push(fetchWorker); 22 | } 23 | -------------------------------------------------------------------------------- /src/worker/privateOpenWorker.js: -------------------------------------------------------------------------------- 1 | let write = async () => { 2 | const root = await navigator.storage.getDirectory(); 3 | console.log(root); 4 | const draftHandle = await root.getFileHandle("draft.txt", { create: true }); 5 | console.log(draftHandle); 6 | const accessHandle = await draftHandle.createSyncAccessHandle(); 7 | const encoder = new TextEncoder(); 8 | const encodedMessage = encoder.encode("hi there"); 9 | const writeBuffer = accessHandle.write(encodedMessage, { at: 0 }); 10 | const writeSize = accessHandle.write(writeBuffer, { "at" : 0 }); 11 | accessHandle.flush(); 12 | accessHandle.close(); 13 | console.log("done"); 14 | }; 15 | let read = async()=>{ 16 | 17 | } 18 | 19 | write(); 20 | 21 | onmessage = function (message) { 22 | console.log("hello"); 23 | }; 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const lazPerf = require("laz-perf"); 4 | const Dotenv = require("dotenv-webpack"); 5 | module.exports = { 6 | entry: "./src/index.js", 7 | output: { 8 | filename: "bundle.js", 9 | path: path.resolve(__dirname, "docs"), 10 | }, 11 | resolve: { 12 | fallback: { 13 | fs: false, 14 | }, 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: ["style-loader", "css-loader"], 21 | }, 22 | { 23 | test: /\.worker\.js$/, 24 | exclude: /node_modules/, 25 | use: "worker-loader", 26 | }, 27 | ], 28 | }, 29 | devServer: { 30 | port: 8080, 31 | static: path.resolve(__dirname, "docs"), 32 | hot: true, 33 | }, 34 | mode: "development", 35 | devtool: "cheap-module-source-map", 36 | plugins: [ 37 | new Dotenv(), 38 | // commonjs({ include: /node_modules\/laz-perf/ }), 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "octree", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack server", 8 | "build": "webpack" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/pravinpoudel/octree.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/pravinpoudel/octree/issues" 19 | }, 20 | "homepage": "https://github.com/pravinpoudel/octree#readme", 21 | "dependencies": { 22 | "copc": "^0.0.4", 23 | "cross-fetch": "^3.1.5", 24 | "dotenv-webpack": "^8.0.1", 25 | "json-fn": "^1.1.1", 26 | "laz-perf": "^0.0.5", 27 | "lru-cache": "^8.0.4", 28 | "three": "^0.148.0", 29 | "webpack": "^5.75.0", 30 | "webpack-cli": "^5.0.1", 31 | "webpack-dev-server": "^4.11.1" 32 | }, 33 | "devDependencies": { 34 | "css-loader": "^6.7.3", 35 | "style-loader": "^3.3.1", 36 | "worker-loader": "^3.0.8" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/webgpu/material.js: -------------------------------------------------------------------------------- 1 | async function loadImageBitMap(device, url) { 2 | const response = await fetch(url); 3 | const image = response.blob(); 4 | // const imageData = await 5 | const textureDescriptor = { 6 | size: { 7 | width: image.width, 8 | height: image.height, 9 | }, 10 | format: "rgba8unorm", 11 | usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, 12 | }; 13 | 14 | let texture = device.createTexture(textureDescriptor); 15 | 16 | device.queue.copyExternalImageToTexture( 17 | image, 18 | texture, 19 | textureDescriptor.size 20 | ); 21 | 22 | let viewDescriptor = { 23 | label: "heatmap texture view", 24 | format: "rgba8unorm", 25 | dimension: textureDescriptor.size, 26 | }; 27 | 28 | let view = texture.createView(viewDescriptor); 29 | 30 | let sampler = device.createSampler({ 31 | label: "heatmap texture sampler", 32 | addressModeU: "repeat", 33 | addressModeV: "repeat", 34 | }); 35 | 36 | return [view, sampler]; 37 | } 38 | 39 | export { loadImageBitMap }; 40 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | border: none; 6 | font-family: "Poppins", sans-serif; 7 | } 8 | 9 | body { 10 | overflow: hidden; 11 | } 12 | 13 | .note { 14 | position: absolute; 15 | color: rgb(32, 31, 30); 16 | top: 15px; 17 | left: 20px; 18 | } 19 | 20 | .note title { 21 | font-size: 20px; 22 | } 23 | 24 | .bottom-note { 25 | position: absolute; 26 | bottom: 55px; 27 | left: 50px; 28 | font-weight: 600; 29 | } 30 | .selection-map { 31 | position: absolute; 32 | top: 50px; 33 | right: 50px; 34 | } 35 | 36 | .option-title { 37 | border-bottom: 1px solid #aaa; 38 | margin-bottom: 20px; 39 | } 40 | 41 | select { 42 | padding: 20px; 43 | border: 1px solid #ccc; 44 | } 45 | 46 | option { 47 | text-indent: 15px; 48 | } 49 | 50 | .abort-div { 51 | position: absolute; 52 | top: 50px; 53 | left: 10px; 54 | padding: 20px 30px; 55 | border: 2px solid; 56 | } 57 | 58 | #abort_btn { 59 | background: none; 60 | font-size: 14px; 61 | } 62 | 63 | #stats-div{ 64 | position: absolute; 65 | bottom: 100px; 66 | right: 50px; 67 | background-color: aliceblue; 68 | padding: 20px; 69 | } -------------------------------------------------------------------------------- /src/private_origin/cache_manager.js: -------------------------------------------------------------------------------- 1 | import { doesExist } from "./file_manager"; 2 | 3 | function sortObjectIntoMap(object1) { 4 | let resultMap = new Map(); 5 | if (!object1) return resultMap; 6 | const sortedArray1 = Object.entries(object1).sort((a, b) => a.date - b.date); 7 | sortedArray1.forEach(([key, value]) => resultMap.set(key, value)); 8 | return resultMap; 9 | } 10 | 11 | function mapIntoJSON(map) { 12 | return JSON.stringify(Object.fromEntries(map)); 13 | } 14 | 15 | const p_cache = async (capacity) => { 16 | let [permission, content] = await doesExist(process.env.p_cache); 17 | let cache = sortObjectIntoMap(content); 18 | return cache; 19 | }; 20 | 21 | const get_inCache = (cache, key) => { 22 | if (!cache.has(key)) return cache; 23 | let val = cache.get(key); 24 | cache.delete(key); 25 | cache.set(key, { date: Date.now(), count: val.count + 1 }); 26 | return cache; 27 | }; 28 | 29 | const put_inCache = (cache, key, value) => { 30 | cache.delete(key); 31 | if (cache.size == process.env.p_cache_capacity) { 32 | cache.delete(cache.keys().next().value); 33 | } else { 34 | cache.set(key, value); 35 | } 36 | return cache; 37 | }; 38 | 39 | const getLRU_inCache = (cache) => { 40 | return Array.from(cache)[0]; 41 | }; 42 | 43 | const getMRU_inCache = (cache) => { 44 | return Array.from(cache)[cache.size - 1]; 45 | }; 46 | 47 | export { 48 | p_cache, 49 | get_inCache, 50 | getLRU_inCache, 51 | getMRU_inCache, 52 | sortObjectIntoMap, 53 | mapIntoJSON, 54 | put_inCache, 55 | }; 56 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | import * as Octree from "./octree"; 2 | 3 | function fillArray(points, count, WIDTH, HEIGHT, DEPTH) { 4 | for (let i = 0; i < count; i++) { 5 | let point = new Octree.Point( 6 | i, 7 | Math.floor(Math.random() * WIDTH) - WIDTH / 2, 8 | Math.floor(Math.random() * HEIGHT) - HEIGHT / 2, 9 | Math.floor(Math.random() * DEPTH) - DEPTH / 2 10 | ); 11 | points.push(point); 12 | } 13 | } 14 | 15 | function fillMidNodes(tree) { 16 | if (!tree.isDivided) { 17 | tree.points.splice(0, 1); 18 | return tree.points[0]; 19 | } 20 | 21 | let children = [ 22 | tree.minNE, 23 | tree.minNW, 24 | tree.minSW, 25 | tree.minSE, 26 | tree.maxNE, 27 | tree.maxNW, 28 | tree.maxSW, 29 | tree.maxSE, 30 | ]; 31 | let result = []; 32 | for (let i = 0, _length = children.length; i < _length; i++) { 33 | let result1 = fillMidNodes(children[i]); 34 | if (result1 != null) { 35 | result.push(result1); 36 | } 37 | } 38 | let passIndex = Math.ceil(result.length / 2); 39 | let passingValue = result[passIndex]; 40 | if (tree.level > 0) { 41 | result.splice(passIndex, 1); 42 | } 43 | tree.representativeNodes = [...result]; 44 | return passingValue; 45 | } 46 | 47 | function updateHtmlUI(nodeNotFoundInBuffer, nodeFoundInBuffer, nodeFoundInLRU, nodeFoundInPersistent, nodeToFetch){ 48 | let stats_text = `Among total nodes needed ${nodeFoundInBuffer + nodeNotFoundInBuffer}\b 49 | nodes found in GPU Buffer: ${nodeFoundInBuffer} \b 50 | ---------------------------------------------------- 51 | nodes need to be loaded ${nodeNotFoundInBuffer}\b 52 | \b 53 | nodes found in LRU Cache: ${nodeFoundInLRU} \b 54 | nodes found in Persistent memory: ${nodeFoundInPersistent} \b 55 | nodes that were fetched from host: ${nodeToFetch} ` 56 | document.getElementById("stats-div").innerText = stats_text 57 | } 58 | 59 | export { fillArray, fillMidNodes, updateHtmlUI }; 60 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |

Color-map axis

35 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/worker/fetcher.worker.js: -------------------------------------------------------------------------------- 1 | import { Copc, Key } from "copc"; 2 | import * as THREE from "three"; 3 | const color = new THREE.Color(); 4 | const colors = []; 5 | let maxZ = -999; 6 | let minZ = 1000; 7 | let maxIntensity = -100; 8 | let firstTime = true; 9 | var nodePages, pages, receivedData, copc; 10 | let x_min, 11 | y_min, 12 | z_min, 13 | x_max, 14 | y_max, 15 | z_max, 16 | widthx, 17 | widthy, 18 | widthz, 19 | scaleX, 20 | scaleY, 21 | scaleZ, 22 | level; 23 | let positions = []; 24 | // const filename = process.env.filename; 25 | const filename = process.env.filename; 26 | 27 | const readPoints = (id, getters) => { 28 | let returnPoint = getXyzi(id, getters); 29 | // console.log(returnPoint) 30 | if (returnPoint[2] > maxZ) { 31 | maxZ = returnPoint[2]; 32 | } 33 | if (returnPoint[2] < minZ) { 34 | minZ = returnPoint[2]; 35 | } 36 | 37 | positions.push(returnPoint[0], returnPoint[1], returnPoint[2]); 38 | // console.log("intensity is", returnPoint[3]); 39 | const vx = returnPoint[3]; 40 | if (vx > maxIntensity) { 41 | maxIntensity = vx; 42 | } 43 | color.setRGB(returnPoint[3], returnPoint[4], returnPoint[5]); 44 | colors.push(color.r, color.g, color.b); 45 | firstTime = false; 46 | }; 47 | 48 | function getXyzi(index, getters) { 49 | return getters.map((get) => get(index)); 50 | } 51 | 52 | async function load() { 53 | // copc = await Copc.create(filename); 54 | // let scale = copc.header.scale[0]; 55 | // [x_min, y_min, z_min, x_max, y_max, z_max] = copc.info.cube; 56 | // width = Math.abs(x_max - x_min); 57 | // // let center_x = (x_min + x_max) / 2; 58 | // // let center_y = (y_min + y_max) / 2; 59 | // // let center_z = (z_min + z_max) / 2; 60 | // receivedData = await Copc.loadHierarchyPage( 61 | // filename, 62 | // copc.info.rootHierarchyPage 63 | // ); 64 | // nodePages = receivedData.nodes; 65 | // pages = receivedData.pages; 66 | postMessage(200); 67 | } 68 | 69 | async function loadData(nodes, pages, copc, myRoot, pointCount) { 70 | // console.log(copc, myRoot); 71 | const view = await Copc.loadPointDataView(filename, copc, myRoot); 72 | // let getters = ["X", "Y", "Z", "Intensity"].map(view.getter); 73 | let getters = ["X", "Y", "Z", "Red", "Green", "Blue"].map(view.getter); 74 | for (let j = 0; j < pointCount; j += 1) { 75 | readPoints(j, getters); 76 | } 77 | postMessage([positions, colors, [minZ, maxZ, maxIntensity, level]]); 78 | } 79 | 80 | load(); 81 | 82 | onmessage = function (message) { 83 | let nodePages = message.data[0]; 84 | let nodes = JSON.parse(nodePages); 85 | let pagesStr = message.data[1]; 86 | let pages = JSON.parse(pagesStr); 87 | let copcStr = message.data[2]; 88 | let copc = JSON.parse(copcStr); 89 | 90 | let mapIndex = message.data[3]; 91 | let pointCount = message.data[4]; 92 | let myRoot = nodes[mapIndex]; 93 | x_min = message.data[5][0]; 94 | y_min = message.data[5][1]; 95 | z_min = message.data[5][2]; 96 | widthx = message.data[5][3]; 97 | widthy = message.data[5][4]; 98 | widthz = message.data[5][5]; 99 | scaleX = message.data[5][6]; 100 | scaleY = message.data[5][7]; 101 | scaleZ = message.data[5][8]; 102 | level = message.data[5][9]; 103 | loadData(nodes, pages, copc, myRoot, pointCount); 104 | }; 105 | -------------------------------------------------------------------------------- /src/shaders/renderShader.js: -------------------------------------------------------------------------------- 1 | let vs = ` 2 | struct VertexInput { 3 | @location(0) position: vec4, 4 | @location(1) color: vec3 5 | }; 6 | 7 | struct VertexOut { 8 | @builtin(position) position: vec4, 9 | @location(0) color: vec4, 10 | }; 11 | 12 | struct paramsUniform { 13 | width_x:f32, 14 | width_y:f32, 15 | width_z:f32, 16 | x_min: f32, 17 | y_min: f32, 18 | z_min: f32, 19 | current_Axis: f32, 20 | max_Intensity: f32 21 | }; 22 | 23 | struct cmapUniform { 24 | colors: array, 20> 25 | }; 26 | 27 | @group(0) @binding(0) var MVP_Matrix: mat4x4; 28 | @group(0) @binding(1) var cMap: cmapUniform; 29 | @group(0) @binding(2) var params: paramsUniform; 30 | 31 | const direction = array, 4>( 32 | vec2(-1, -1), 33 | vec2(1, -1), 34 | vec2(-1, 1), 35 | vec2(1, 1) 36 | ); 37 | 38 | const PI: f32 = 3.1415926535897932384626433832795; 39 | 40 | fn getCmapped(cMapIndex: i32)->vec4{ 41 | var cmapped = cMap.colors[cMapIndex]; 42 | if(cMapIndex>19){ 43 | cmapped = cMap.colors[19]; 44 | } 45 | return cmapped; 46 | } 47 | 48 | @vertex 49 | fn main(in: VertexInput, @builtin(instance_index) inst_index:u32, @builtin(vertex_index) vertexIndex : u32)->VertexOut{ 50 | var out:VertexOut; 51 | var cMapIndex:i32; 52 | var level:f32 = in.position.w; 53 | var radius:f32 = 3.0* pow(0.6, level); 54 | radius = max(radius, 1.0); 55 | var position:vec3 = in.position.xyz - vec3(params.x_min, params.y_min, params.z_min) - 0.5*vec3(params.width_x, params.width_y, params.width_z); 56 | var factor = in.color.x/params.max_Intensity; 57 | if(params.current_Axis == 2.0){ 58 | cMapIndex = i32((abs(in.position.z - params.z_min)/params.width_z) *19); 59 | let mappedColor = getCmapped(cMapIndex); 60 | out.color = vec4(mappedColor.xyz, 1.0); 61 | if(cMapIndex < 0){ 62 | out.color = vec4(1.0, 0.0, 0.0, 1.0); 63 | } 64 | out.color = vec4(out.color.x, out.color.y, out.color.z, 1.0)*factor; 65 | 66 | } 67 | else if(params.current_Axis == 1.0){ 68 | cMapIndex = i32(1.25*(abs(in.position.y - params.y_min)/params.width_y) *19); 69 | out.color = getCmapped(cMapIndex); 70 | out.color = vec4(out.color.x, out.color.y, out.color.z, 1.0)*factor; 71 | 72 | } 73 | else if(params.current_Axis == 0.0){ 74 | cMapIndex = i32(1.25*(abs(in.position.x - params.x_min)/params.width_x) *19); 75 | out.color = getCmapped(cMapIndex); 76 | out.color = vec4(out.color.x, out.color.y, out.color.z, 1.0)*factor; 77 | } 78 | else{ 79 | out.color = vec4(in.color.x/255.0, in.color.y/225.0, in.color.z/255.0, 1.0); 80 | } 81 | 82 | if(factor < 0.1){ 83 | factor = 0.35; 84 | } 85 | // if(level <= 1.0){ 86 | // out.color = vec4(0.0, 1.0, 0.0, 1.0); 87 | // } 88 | position = position + vec3(radius*direction[vertexIndex], 0.0); 89 | out.position = MVP_Matrix* vec4(position, 1.0); 90 | return out; 91 | } 92 | `; 93 | 94 | let fs = ` 95 | struct VertexOut { 96 | @builtin(position) position: vec4, 97 | @location(0) color: vec4 98 | }; 99 | 100 | @fragment 101 | fn main(in:VertexOut)->@location(0) vec4{ 102 | return in.color; 103 | } 104 | `; 105 | 106 | export { fs, vs }; 107 | -------------------------------------------------------------------------------- /docs/WorkerConsole.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WorkerConsole.js: 3 | * 4 | * Include this script in your web pages in order to give your worker threads 5 | * a working console.log() function. This file is also loaded by all 6 | * workers you create in order to define the log() function. It is one 7 | * file used in two distinct ways. 8 | * 9 | * This does not work in Firefox, since FF4 does not support MessageChannel. 10 | * 11 | * It appears to work in Chrome, but has not been tested in other browsers. 12 | * Note that Workers don't work in Chrome if you're using the file:// 13 | * protocol, so in order to try this out you have to be running a server. 14 | * 15 | * It does not work for workers nested within other workers, but it could 16 | * probably be made to work in that case. 17 | * 18 | * It has only been tested with very simple directory structures. 19 | * WorkerConsole.js probably needs to be in the same directory as the 20 | * HTML file that includes it. There are likely to be path issues 21 | * for more complicated directory structures. 22 | * 23 | * Copyright 2011 by David Flanagan 24 | * http://creativecommons.org/licenses/by-nc-sa/3.0/ 25 | */ 26 | if (this.console && this.console.log) { 27 | /* 28 | * If there is already a console.log() function defined, then wrap the 29 | * Worker() constructor so that workers get console.log(), too. 30 | */ 31 | // Remember the original Worker() constructor 32 | this._Worker = Worker; 33 | 34 | // Make this.Worker writable, so we can replace it. 35 | Object.defineProperty(this, "Worker", {writable: true}); 36 | 37 | // Replace the Worker() constructor with this augmented version 38 | this.Worker = function Worker(url) { 39 | // Create a real Worker object that first loads this file to define 40 | // console.log() and then loads the requested URL 41 | var w = new _Worker("WorkerConsole.js#" + url); 42 | 43 | // Create a side channel for the worker to send log messages on 44 | var channel = new MessageChannel(); 45 | 46 | // Send one end of the channel to the worker 47 | w.postMessage("console", [channel.port2]); 48 | 49 | // And listen for log messages on the other end of the channel 50 | channel.port1.onmessage = function(e) { 51 | var args = e.data; // Array of args to console.log() 52 | args.unshift(url + ": "); // Add an arg to id the worker 53 | console.log.apply(console, args); // Pass the args to the real log 54 | } 55 | 56 | // Return the real Worker object from this fake constructor 57 | return w; 58 | } 59 | } 60 | else { 61 | /* 62 | * If there wasn't a console.log() function defined, then we're in a 63 | * Worker created with the wrapped Worker() constructor above, and 64 | * we need to define the console. 65 | * 66 | * Wait until we get the event that delivers the MessagePort sent by the 67 | * main thread. Once we get it, we define the console.log() function 68 | * and load and run the original file that was passed to the constructor. 69 | */ 70 | self.onmessage = function(e) { 71 | if (e.data === "console") { 72 | // Define the console object 73 | self.console = { 74 | _port: e.ports[0], // Remember the port we log to 75 | log: function log() { // Define console.log() 76 | // Copy the arguments into a real array 77 | var args = Array.prototype.slice.call(arguments); 78 | // Send the arguments as a message, over our side channel 79 | console._port.postMessage(args); 80 | } 81 | }; 82 | 83 | // Get rid of this event handler 84 | onmessage = null; 85 | 86 | // Now run the script that was originally passed to Worker() 87 | var url = location.hash.substring(1); // Get the real URL to run 88 | importScripts(url); // Load and run it now 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/passiveloader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { computeFocalLength, computeSSE } from "./utils/loader"; 3 | 4 | let direction = [ 5 | [0, 0, 0], 6 | [0, 0, 1], 7 | [0, 1, 0], 8 | [0, 1, 1], 9 | [1, 0, 0], 10 | [1, 0, 1], 11 | [1, 1, 0], 12 | [1, 1, 1], 13 | ]; 14 | 15 | let cameraFocalLength = computeFocalLength(90); 16 | let nodeToPrefetch = []; 17 | 18 | async function* lazyLoad(offsetMap, url) { 19 | while (offsetMap.length > 0) { 20 | let fetchStart = offsetMap.pop(); 21 | let bytesofPointData = offsetMap.pop(); 22 | let fetchEnd = fetchStart + bytesofPointData; 23 | let response = await fetch(url, { 24 | headers: { 25 | "content-type": "multipart/byteranges", 26 | Range: `bytes=${fetchStart}-${fetchEnd}`, 27 | }, 28 | }); 29 | let buffer = await response.arrayBuffer(); 30 | let view = new DataView(buffer); 31 | } 32 | } 33 | 34 | function isLeadfNode(root, nodePages) { 35 | let [level, x, y, z] = root; 36 | for (let i = 0; i < direction.length; i++) { 37 | let [dx, dy, dz] = direction[i]; 38 | let newLevel = level + 1; 39 | let key = `${newLevel}-${2 * x + dx}-${2 * y + dy}-${2 * z + dz}`; 40 | if (key in nodePages) { 41 | return false; 42 | } 43 | } 44 | return true; 45 | } 46 | 47 | let canvas = document.getElementById("screen-canvas"); 48 | canvas.width = window.innerWidth * (window.devicePixelRatio || 1); 49 | canvas.height = window.innerHeight * (window.devicePixelRatio || 1); 50 | let screenWidth = canvas.width; 51 | let screenHeight = canvas.height; 52 | let fovRADIAN = Math.PI / 2; 53 | 54 | function isRendered( 55 | center, 56 | radius, 57 | distance, 58 | projViewMatrix, 59 | level, 60 | key, 61 | nodePages 62 | ) { 63 | let minPoint = [center[0] - radius, center[1] - radius, center[2] - radius]; 64 | let maxPoint = [center[0] + radius, center[1] + radius, center[2] + radius]; 65 | let frustum = new Frustum(projViewMatrix); 66 | if (!frustum.containsBox([...minPoint, ...maxPoint])) { 67 | if (level <= 2) { 68 | nodeToPrefetch.push(key, nodePages[key].pointCount); 69 | } 70 | return false; 71 | } 72 | 73 | // let pixel_size = (2 * Math.tan(fovRADIAN / 2.0) * distance) / screenHeight; 74 | let projectedRadius = 75 | (radius * screenHeight) / (distance * (2 * Math.tan(fovRADIAN / 2.0))); 76 | return Math.abs(projectedRadius) > 90; 77 | } 78 | 79 | function traverseTreeWrapper( 80 | nodePages, 81 | root, 82 | center_x, 83 | center_y, 84 | center_z, 85 | width, 86 | scale, 87 | controls, 88 | projViewMatrix 89 | ) { 90 | let cameraPosition = controls.object.position.toArray(); 91 | let width_x_world = width[0]; 92 | nodeToPrefetch = []; 93 | function traverseTree(root, center_x, center_y, center_z, width) { 94 | let [level, x, y, z] = root; 95 | let newLevel = level + 1; 96 | let key = level + "-" + x + "-" + y + "-" + z; 97 | let distance = Math.sqrt( 98 | Math.pow(Math.abs(cameraPosition[0] - center_x), 2) + 99 | Math.pow(Math.abs(cameraPosition[1] - center_y), 2) + 100 | Math.pow(Math.abs(cameraPosition[2] - center_z), 2) 101 | ); 102 | if ( 103 | !isRendered( 104 | [center_x, center_y, center_z], 105 | Math.max(...width), 106 | distance, 107 | projViewMatrix, 108 | level, 109 | key, 110 | nodePages 111 | ) 112 | ) { 113 | return []; 114 | } 115 | 116 | let center_x_left = center_x - width[0] / 2; 117 | let center_x_right = center_x + width[0] / 2; 118 | let center_y_top = center_y + width[1] / 2; 119 | let center_y_bottom = center_y - width[1] / 2; 120 | let center_z_near = center_z + width[2] / 2; 121 | let center_z_far = center_z - width[2] / 2; 122 | 123 | let result = [key, nodePages[key].pointCount]; 124 | direction.forEach((element, index) => { 125 | let [dx, dy, dz] = element; 126 | let key1 = `${newLevel}-${2 * x + dx}-${2 * y + dy}-${2 * z + dz}`; 127 | if (!(key1 in nodePages && nodePages[key].pointCount > 0)) { 128 | return []; 129 | } 130 | center_x = center_x_left; 131 | center_y = center_y_bottom; 132 | center_z = center_z_far; 133 | if (dx == 1) { 134 | center_x = center_x_right; 135 | } 136 | if (dy == 1) { 137 | center_y = center_y_top; 138 | } 139 | if (dz == 1) { 140 | center_z = center_z_near; 141 | } 142 | let result1 = traverseTree( 143 | [newLevel, 2 * x + dx, 2 * y + dy, 2 * z + dz], 144 | center_x, 145 | center_y, 146 | center_z, 147 | [width[0] / 2, width[1] / 2, width[2] / 2] 148 | ); 149 | result.push(...result1); 150 | }); 151 | return result; 152 | } 153 | let finalPoints = traverseTree(root, center_x, center_y, center_z, [ 154 | width[0], 155 | width[1], 156 | width[2], 157 | ]); 158 | return [finalPoints, nodeToPrefetch]; 159 | } 160 | 161 | export { traverseTreeWrapper }; 162 | -------------------------------------------------------------------------------- /src/private_origin/file_manager.js: -------------------------------------------------------------------------------- 1 | import { throttle } from "../webgpu/renderer"; 2 | 3 | let total_ops = 0; 4 | let used_ops = 0; 5 | navigator.webkitPersistentStorage.queryUsageAndQuota( 6 | function (used, total) { 7 | total_ops = total; 8 | used_ops = used; 9 | }, 10 | function (error) { 11 | console.error("Error getting origin-private file system size:", error); 12 | } 13 | ); 14 | 15 | let available_ops = total_ops - used_ops; 16 | //------------------------------------------------ for persistent cache capacity is number of files for now -------------------------------------------- 17 | let create_P_Meta_Cache = async () => { 18 | const root = await navigator.storage.getDirectory(); 19 | let fileToCheck = `${process.env.p_cache}.json`; 20 | let [already_exist, content] = await doesExist(fileToCheck); 21 | if (!already_exist) { 22 | const root = await navigator.storage.getDirectory(); 23 | const fileHandle = await root.getFileHandle(fileToCheck, { 24 | create: true, 25 | }); 26 | } else { 27 | console.log("meta cache file already exist"); 28 | } 29 | }; 30 | 31 | let updatePersCache = async (updatD_data) => { 32 | let fileToCheck = `${process.env.p_cache}.json`; 33 | const root = await navigator.storage.getDirectory(); 34 | const fileHandle = await root.getFileHandle(fileToCheck, { 35 | create: true, 36 | }); 37 | const writableStream = await fileHandle.createWritable(); 38 | await writableStream.write(updatD_data); 39 | await writableStream.close(); 40 | console.log("cache updating is done"); 41 | }; 42 | 43 | let throttled_Update_Pers_Cache = throttle(updatePersCache, 30000); 44 | 45 | // let update_cache = async (method, data) => { 46 | // const cache_name = "cache-holder"; 47 | // let [isAvailable, content] = doesExist(cache_name); 48 | // // if (!isAvailable) { 49 | // // console.error("cache meta ile does not exist"); 50 | // // return; 51 | // // } 52 | // // content = JSON.parse(content); 53 | // // switch (method) { 54 | // // case "life_update": 55 | // // content[data.fileName].useCounter = content[data.fileName].useCounter + 1; 56 | 57 | // // break; 58 | // // case "put": 59 | // // } 60 | // }; 61 | 62 | let clear = async () => { 63 | const root = await navigator.storage.getDirectory(); 64 | const fileNames = await root.keys(); 65 | let x = await fileNames.next(); 66 | while (!x.done) { 67 | let fileName = x.value; 68 | const fileHandle = await root.getFileHandle(fileName); 69 | await fileHandle.remove(); 70 | x = await fileNames.next(); 71 | } 72 | }; 73 | 74 | // clear() 75 | // --------------------------- indepedent readBinary file code wrote to test before putting this part inside doesExist -------------------------- 76 | let readBin = async (fileName) => { 77 | let fileToCheck = `${fileName}.bin`; 78 | const root = await navigator.storage.getDirectory(); 79 | const fileHandle = await root.getFileHandle(fileToCheck); 80 | let retrived_blob = await fileHandle.getFile(); 81 | var reader = new FileReader(); 82 | return await new Promise((resolve, reject) => { 83 | reader.onload = function () { 84 | resolve(JSON.parse(reader.result)); 85 | }; 86 | reader.readAsText(retrived_blob); 87 | }); 88 | }; 89 | 90 | //-------------------------------------------------------------------------------- 91 | 92 | let write = async (fileName, data1) => { 93 | let fileToCheck = `${fileName}.bin`; 94 | const blob = new Blob([data1], { type: "application/octet-stream" }); 95 | const root = await navigator.storage.getDirectory(); 96 | const fileHandle = await root.getFileHandle(fileToCheck, { 97 | create: true, 98 | }); 99 | const writableStream = await fileHandle.createWritable(); 100 | await writableStream.write(blob); 101 | await writableStream.close(); 102 | }; 103 | 104 | let read = async (fileName) => { 105 | const root = await navigator.storage.getDirectory(); 106 | const fileHandle = await root.getFileHandle(`${fileName}.bin`, { 107 | create: false, 108 | }); 109 | let file = await fileHandle.getFile(); 110 | let content = await file.text(); 111 | if (content) { 112 | return JSON.parse(content); 113 | } 114 | return null; 115 | }; 116 | 117 | let doesExist = async (fileName) => { 118 | try { 119 | let fileToCheck = `${fileName}.bin`; 120 | const root = await navigator.storage.getDirectory(); 121 | const fileHandle = await root.getFileHandle(fileToCheck); 122 | const permissionStatus = await fileHandle.queryPermission(); 123 | let found = permissionStatus == "granted" ? true : false; 124 | let retrived_blob = await fileHandle.getFile(); 125 | if(retrived_blob.size>0){ 126 | var reader = new FileReader(); 127 | return await new Promise((resolve, reject) => { 128 | reader.onload = function () { 129 | resolve([true, JSON.parse(reader.result)]); 130 | }; 131 | reader.readAsText(retrived_blob); 132 | }); 133 | 134 | } 135 | else{ 136 | return [ true, {position:[], color: []}] 137 | } 138 | } catch (error) { 139 | if (error.name === "NotFoundError") { 140 | return [false, null]; 141 | } else { 142 | console.error("Error checking if file exists:", error); 143 | return [false, null]; 144 | } 145 | } 146 | }; 147 | 148 | export { 149 | write, 150 | read, 151 | doesExist, 152 | clear, 153 | create_P_Meta_Cache, 154 | throttled_Update_Pers_Cache, 155 | }; 156 | -------------------------------------------------------------------------------- /src/octree.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | let MAX_BOUNDARY_X = 8; 3 | let MAX_BOUNDARY_Y = 8; 4 | let MAX_BOUNDARY_Z = 8; 5 | // color 6 | const white = new THREE.Color(0xffffff); 7 | const red = new THREE.Color(0xff0000); 8 | const green = new THREE.Color(0xc5e908); 9 | const blue = new THREE.Color(0x0000ff); 10 | const yellow = new THREE.Color(0xe69b00); 11 | const grey = new THREE.Color(0xe0a387); 12 | const illusion = new THREE.Color(0xf1a784); 13 | 14 | const colors = [yellow, red, illusion, blue, green, grey, illusion]; 15 | // ------------------------------------- 16 | 17 | class Point { 18 | constructor(index, x, y, z) { 19 | this.index = index; 20 | this.x = x; 21 | this.y = y; 22 | this.z = z; 23 | let mesh = new THREE.Mesh( 24 | new THREE.BoxGeometry(5, 5, 5), 25 | new THREE.MeshBasicMaterial({ color: 0xff0000 }) 26 | ); 27 | mesh.position.set(x, y, z); 28 | mesh.updateMatrix(); 29 | mesh.matrixAutoUpdate = false; 30 | this.mesh = mesh; 31 | } 32 | } 33 | 34 | class Box { 35 | constructor(label, x, y, z, width, level) { 36 | this.label = label; 37 | this.x = x; 38 | this.y = y; 39 | this.z = z; 40 | this.width = width; 41 | let mesh = new THREE.Mesh( 42 | new THREE.BoxGeometry(width, width, width), 43 | new THREE.MeshBasicMaterial({ color: colors[level % 7], wireframe: true }) 44 | ); 45 | mesh.position.set(x, y, z); 46 | mesh.updateMatrix(); 47 | mesh.matrixAutoUpdate = false; 48 | this.mesh = mesh; 49 | // scene.add(mesh); 50 | } 51 | 52 | bound(point) { 53 | if (this.x + this.width * 0.5 == 0.5 * MAX_BOUNDARY_X) { 54 | return ( 55 | point.x >= this.x - this.width * 0.5 && 56 | point.x <= this.x + this.width * 0.5 && 57 | point.y < this.y + this.width * 0.5 && 58 | point.y >= this.y - this.width * 0.5 && 59 | point.z >= this.z - this.width * 0.5 && 60 | point.z < this.z + this.width * 0.5 61 | ); 62 | } 63 | if (this.y + this.width * 0.5 == 0.5 * MAX_BOUNDARY_Y) { 64 | return ( 65 | point.x >= this.x - this.width * 0.5 && 66 | point.x < this.x + this.width * 0.5 && 67 | point.y <= this.y + this.width * 0.5 && 68 | point.y >= this.y - this.width * 0.5 && 69 | point.z >= this.z - this.width * 0.5 && 70 | point.z < this.z + this.width * 0.5 71 | ); 72 | } 73 | if (this.z + this.width * 0.5 == 0.5 * MAX_BOUNDARY_Z) { 74 | return ( 75 | point.x >= this.x - this.width * 0.5 && 76 | point.x < this.x + this.width * 0.5 && 77 | point.y < this.y + this.width * 0.5 && 78 | point.y >= this.y - this.width * 0.5 && 79 | point.z >= this.z - this.width * 0.5 && 80 | point.z <= this.z + this.width * 0.5 81 | ); 82 | } else { 83 | return ( 84 | point.x >= this.x - this.width * 0.5 && 85 | point.x < this.x + this.width * 0.5 && 86 | point.y < this.y + this.width * 0.5 && 87 | point.y >= this.y - this.width * 0.5 && 88 | point.z >= this.z - this.width * 0.5 && 89 | point.z < this.z + this.width * 0.5 90 | ); 91 | } 92 | } 93 | } 94 | 95 | class Octree { 96 | constructor(box, level = 0) { 97 | this.box = box; 98 | this.minNE = null; 99 | this.minNW = null; 100 | this.minSW = null; 101 | this.minSE = null; 102 | this.maxNE = null; 103 | this.maxNW = null; 104 | this.maxSW = null; 105 | this.maxSE = null; 106 | this.isDivided = false; 107 | // this.representativeNodes = []; 108 | this.points = []; 109 | this.buffer = []; 110 | this.level = level; 111 | this.parent = null; 112 | } 113 | 114 | // function findRepresentiveNode(){ 115 | // let children = [this.minNE, this.minNW, this.minSW, this.minSE, this.maxNE, this.maxNW, this.maxSW, this.maxSE] 116 | // children.forEach((element, index)=>{ 117 | // if(element != null && element.length>0){ 118 | // this.representativeNodes.push(element.nodes[0]) 119 | // } 120 | // }) 121 | // } 122 | 123 | partition() { 124 | let x = this.box.x; 125 | let y = this.box.y; 126 | let z = this.box.z; 127 | let newWidth = this.box.width * 0.5; 128 | let level = this.level + 1; 129 | let maxNE_Box = new Box( 130 | "maxNE", 131 | x + newWidth * 0.5, 132 | y + 0.5 * newWidth, 133 | z - 0.5 * newWidth, 134 | newWidth, 135 | level 136 | ); 137 | let maxNW_Box = new Box( 138 | "maxNW", 139 | x - newWidth * 0.5, 140 | y + 0.5 * newWidth, 141 | z - 0.5 * newWidth, 142 | newWidth, 143 | level 144 | ); 145 | let maxSW_Box = new Box( 146 | "maxSW", 147 | x - newWidth * 0.5, 148 | y - 0.5 * newWidth, 149 | z - 0.5 * newWidth, 150 | newWidth, 151 | level 152 | ); 153 | let maxSE_Box = new Box( 154 | "maxSE", 155 | x + newWidth * 0.5, 156 | y - 0.5 * newWidth, 157 | z - 0.5 * newWidth, 158 | newWidth, 159 | level 160 | ); 161 | 162 | let minNE_Box = new Box( 163 | "minNE", 164 | x + newWidth * 0.5, 165 | y + 0.5 * newWidth, 166 | z + 0.5 * newWidth, 167 | newWidth, 168 | level 169 | ); 170 | let minNW_Box = new Box( 171 | "minNW", 172 | x - newWidth * 0.5, 173 | y + 0.5 * newWidth, 174 | z + 0.5 * newWidth, 175 | newWidth, 176 | level 177 | ); 178 | let minSW_Box = new Box( 179 | "minSW", 180 | x - newWidth * 0.5, 181 | y - 0.5 * newWidth, 182 | z + 0.5 * newWidth, 183 | newWidth, 184 | level 185 | ); 186 | let minSE_Box = new Box( 187 | "minSE", 188 | x + newWidth * 0.5, 189 | y - 0.5 * newWidth, 190 | z + 0.5 * newWidth, 191 | newWidth, 192 | level 193 | ); 194 | 195 | this.minNE = new Octree(minNE_Box, level); 196 | this.minNW = new Octree(minNW_Box, level); 197 | this.minSW = new Octree(minSW_Box, level); 198 | this.minSE = new Octree(minSE_Box, level); 199 | this.maxNE = new Octree(maxNE_Box, level); 200 | this.maxNW = new Octree(maxNW_Box, level); 201 | this.maxSW = new Octree(maxSW_Box, level); 202 | this.maxSE = new Octree(maxSE_Box, level); 203 | this.isDivided = true; 204 | } 205 | 206 | insert(point) { 207 | if (!this.box.bound(point)) { 208 | // console.log( 209 | // "out of boundary", 210 | // "for node", 211 | // point.x, 212 | // point.y, 213 | // point.z, 214 | // "for box", 215 | // this.box.x, 216 | // this.box.y, 217 | // this.box.z, 218 | // this.box.width * 0.5, 219 | // this.box.label 220 | // ); 221 | return false; 222 | } 223 | if (this.points.length < tree.leafCapacity && !this.isDivided) { 224 | // this.updateRepresentativeNode(); 225 | this.points.push(point.index); 226 | // this.sortNode(); 227 | return true; 228 | } else if (this.buffer.length < tree.bufferCapacity && !this.isDivided) { 229 | this.buffer.push(point.index); 230 | return true; 231 | } else { 232 | if (!this.isDivided) { 233 | this.partition(); 234 | this.buffer.forEach((existingPoint, index) => { 235 | if ( 236 | existingPoint.x == point.x && 237 | existingPoint.y == point.y && 238 | existingPoint.z == point.z 239 | ) { 240 | console.log("repetitive node not allowed"); 241 | return false; 242 | } 243 | this.minNE.insert(existingPoint) || 244 | this.minNW.insert(existingPoint) || 245 | this.minSE.insert(existingPoint) || 246 | this.minSW.insert(existingPoint) || 247 | this.maxNE.insert(existingPoint) || 248 | this.maxNW.insert(existingPoint) || 249 | this.maxSW.insert(existingPoint) || 250 | this.maxSE.insert(existingPoint); 251 | }); 252 | this.buffer = []; 253 | } 254 | return ( 255 | this.minNE.insert(point) || 256 | this.minNW.insert(point) || 257 | this.minSE.insert(point) || 258 | this.minSW.insert(point) || 259 | this.maxNE.insert(point) || 260 | this.maxNW.insert(point) || 261 | this.maxSW.insert(point) || 262 | this.maxSE.insert(point) 263 | ); 264 | } 265 | } 266 | } 267 | 268 | export { Point, Box, Octree }; 269 | -------------------------------------------------------------------------------- /src/webgpu/renderer.js: -------------------------------------------------------------------------------- 1 | import { vs, fs } from "../shaders/renderShader.js"; 2 | import { 3 | bufferMap, 4 | retrivePoints, 5 | toDeleteMap, 6 | wait, 7 | controls, 8 | global_max_intensity, 9 | } from "../index.js"; 10 | import Stats from "three/examples/jsm/libs/stats.module"; 11 | 12 | let adapter = null; 13 | let device = null; 14 | let worldViewProj = mat4.create(); 15 | var projView = mat4.create(); 16 | let proj; 17 | let camera; 18 | let context = null; 19 | let swapChainFormat = "bgra8unorm"; 20 | let renderPipeline; 21 | let mvp_BG; 22 | let positionBuffer; 23 | let colorBuffer; 24 | let MVP_Buffer; 25 | var lasInfoBuffer; 26 | let maxIntensityBuffer; 27 | let commandEncoder; 28 | let renderPassDescriptor; 29 | let renderDepthTexture; 30 | let canvas; 31 | let numPoints; 32 | let positions, colors; 33 | let keyMap = { 34 | isDown: false, 35 | dragging: false, 36 | }; 37 | let debounceTimeOutId = null; 38 | let colorMapBuffer; 39 | let paramsBuffer; 40 | let currentAxis = 3; 41 | let param; 42 | let abortController = null; 43 | let levelBuffer; 44 | 45 | const stats = Stats(); 46 | document.body.appendChild(stats.dom); 47 | 48 | function throttle(callback, interval) { 49 | let enableCall = true; 50 | return function (...args) { 51 | if (!enableCall) return; 52 | enableCall = false; 53 | callback.apply(this, args); 54 | setTimeout(() => (enableCall = true), interval); 55 | }; 56 | } 57 | 58 | let throttleTreeTravel = throttle(retrivePoints, 2000); 59 | 60 | // ------------------------------- camera itenary 61 | 62 | const iternary = [ 63 | { x: -100, y: 1000, z: 700 }, 64 | { x: 0, y: 1000, z: 700 }, 65 | { x: 0, y: 500, z: 300 }, 66 | { x: 100, y: 200, z: 300 }, 67 | { x: 0, y: 100, z: 100 }, 68 | ]; 69 | 70 | function moveCamera() { 71 | return new Promise((resolve, reject) => { 72 | controls.object.position.set( 73 | iternary[count].x, 74 | iternary[count].y, 75 | iternary[count].z 76 | ); 77 | render(); 78 | throttleTreeTravel(projView); 79 | resolve("done"); 80 | }); 81 | } 82 | 83 | let count = 0; 84 | function itenaryStart(fn) { 85 | fn().then((response) => { 86 | count++; 87 | if (count < 5) setTimeout(() => itenaryStart(fn), 10000); 88 | }); 89 | } 90 | 91 | // ----------------------------------------------------------------- 92 | 93 | function configureSwapChain(device) { 94 | context.configure({ 95 | device: device, 96 | format: swapChainFormat, 97 | usage: GPUTextureUsage.RenderAttachment, 98 | alphaMode: "premultiplied", 99 | }); 100 | } 101 | 102 | function goToFallback() { 103 | console.error("unable to start webgpu"); 104 | return; 105 | } 106 | 107 | function recoverFromDeviceLoss(data) { 108 | console.log("device is lost"); 109 | } 110 | 111 | (() => { 112 | const selectColormap = document.getElementById("colormap-axis"); 113 | selectColormap.addEventListener("change", (event) => { 114 | const axis = parseInt(event.target.value); 115 | if (axis != currentAxis) { 116 | currentAxis = axis; 117 | updateAxis(); 118 | } 119 | }); 120 | })(); 121 | 122 | async function updateMaxIntensity() { 123 | param[param.length - 1] = global_max_intensity; 124 | const stagingBuffer = device.createBuffer({ 125 | usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, 126 | size: 32, 127 | mappedAtCreation: true, 128 | }); 129 | 130 | const stagingData = new Float32Array(stagingBuffer.getMappedRange()); 131 | stagingData.set(param); 132 | stagingBuffer.unmap(); 133 | const copyEncoder = device.createCommandEncoder(); 134 | copyEncoder.copyBufferToBuffer(stagingBuffer, 28, paramsBuffer, 28, 4); 135 | device.queue.submit([copyEncoder.finish()]); 136 | } 137 | 138 | async function updateAxis() { 139 | param[param.length - 2] = currentAxis; 140 | const stagingBuffer = device.createBuffer({ 141 | usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, 142 | size: 32, 143 | mappedAtCreation: true, 144 | }); 145 | 146 | const stagingData = new Float32Array(stagingBuffer.getMappedRange()); 147 | stagingData.set(param); 148 | stagingBuffer.unmap(); 149 | const copyEncoder = device.createCommandEncoder(); 150 | copyEncoder.copyBufferToBuffer(stagingBuffer, 24, paramsBuffer, 24, 8); 151 | device.queue.submit([copyEncoder.finish()]); 152 | } 153 | 154 | async function init() { 155 | adapter = await navigator.gpu.requestAdapter(); 156 | if (!adapter) return goToFallback(); 157 | device = await adapter.requestDevice(); 158 | if (!device) return goToFallback(); 159 | // device.lost.then(recoverFromDeviceLoss); 160 | 161 | canvas = document.getElementById("screen-canvas"); 162 | canvas.width = window.innerWidth * (window.devicePixelRatio || 1); 163 | canvas.height = window.innerHeight * (window.devicePixelRatio || 1); 164 | 165 | context = canvas.getContext("webgpu"); 166 | if (!context) { 167 | console.error("coould not get context from the canvas"); 168 | return; 169 | } 170 | 171 | swapChainFormat = navigator.gpu.getPreferredCanvasFormat(); 172 | configureSwapChain(device); 173 | canvas.addEventListener("mousedown", (e) => { 174 | if (e.buttons == 1 || e.buttons == 2) { 175 | keyMap["isDown"] = true; 176 | } 177 | }); 178 | 179 | window.addEventListener("mouseup", (e) => { 180 | keyMap["isDown"] = false; 181 | }); 182 | 183 | canvas.addEventListener("mousemove", () => { 184 | if (keyMap["isDown"] == true) { 185 | throttleTreeTravel(projView); 186 | } 187 | }); 188 | 189 | window.addEventListener("wheel", (event) => { 190 | // console.log(camera.eyePos()); 191 | if (abortController) { 192 | abortController.abort(); 193 | } 194 | abortController = new AbortController(); 195 | throttleTreeTravel(projView, abortController.signal); 196 | }); 197 | } 198 | 199 | async function intRenderPipeline() { 200 | let Vertex_Buffer_Descriptor = [{}]; 201 | let vs_module = device.createShaderModule({ 202 | label: "vertex shader", 203 | code: vs, 204 | }); 205 | 206 | let fs_module = device.createShaderModule({ 207 | label: "fragment shader", 208 | code: fs, 209 | }); 210 | 211 | let positionAttribute_Desc = { 212 | shaderLocation: 0, 213 | offset: 0, 214 | format: "float32x4", 215 | }; 216 | 217 | let colorAttribute_Desc = { 218 | shaderLocation: 1, 219 | offset: 0, 220 | format: "float32x3", 221 | }; 222 | 223 | let Vertex_Shader_Descriptor = { 224 | module: vs_module, 225 | entryPoint: "main", 226 | buffers: [ 227 | { 228 | arrayStride: 16, 229 | stepMode: "instance", 230 | attributes: [positionAttribute_Desc], 231 | }, 232 | { 233 | arrayStride: 12, 234 | stepMode: "instance", 235 | attributes: [colorAttribute_Desc], 236 | }, 237 | ], 238 | }; 239 | 240 | let Fragment_Shader_Descriptor = { 241 | module: fs_module, 242 | entryPoint: "main", 243 | targets: [{ format: swapChainFormat }], 244 | }; 245 | 246 | let Depth_Stencil_Descriptor = { 247 | format: "depth24plus-stencil8", 248 | depthWriteEnabled: true, 249 | depthCompare: "less", 250 | }; 251 | 252 | let Primitive_Descriptor = { 253 | topology: "triangle-strip", 254 | cullMode: "none", 255 | }; 256 | 257 | renderPipeline = await device.createRenderPipeline({ 258 | label: "render pipeline", 259 | layout: "auto", 260 | vertex: Vertex_Shader_Descriptor, 261 | fragment: Fragment_Shader_Descriptor, 262 | depthStencil: Depth_Stencil_Descriptor, 263 | primitive: Primitive_Descriptor, 264 | }); 265 | } 266 | 267 | async function initVertexBuffer() { 268 | let totalNumberOfPoints = numPoints; 269 | positionBuffer = device.createBuffer({ 270 | label: "vertex position buffer", 271 | size: totalNumberOfPoints * 16, 272 | usage: GPUBufferUsage.VERTEX, 273 | mappedAtCreation: true, 274 | }); 275 | 276 | let mapArrayPosition = new Float32Array(positionBuffer.getMappedRange()); 277 | mapArrayPosition.set(positions); 278 | positionBuffer.unmap(); 279 | 280 | colorBuffer = device.createBuffer({ 281 | label: "vertex color buffer", 282 | size: totalNumberOfPoints * 12, 283 | usage: GPUBufferUsage.VERTEX, 284 | mappedAtCreation: true, 285 | }); 286 | 287 | let mapArrayColor = new Float32Array(colorBuffer.getMappedRange()); 288 | mapArrayColor.set(colors); 289 | colorBuffer.unmap(); 290 | } 291 | 292 | function initUniform(cam, projMatrix, params) { 293 | camera = cam; 294 | proj = projMatrix; 295 | param = params; 296 | params.push(currentAxis); 297 | params.push(global_max_intensity); 298 | // params 299 | paramsBuffer = device.createBuffer({ 300 | size: 8 * 4, 301 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 302 | mappedAtCreation: true, 303 | }); 304 | let mapArray_params = new Float32Array(paramsBuffer.getMappedRange()); 305 | mapArray_params.set(params); 306 | paramsBuffer.unmap(); 307 | 308 | levelBuffer = device.createBuffer({ 309 | size: 4, 310 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 311 | mappedAtCreation: true, 312 | }); 313 | let mapArray_level = new Float32Array(levelBuffer.getMappedRange()); 314 | mapArray_level.set([0]); 315 | levelBuffer.unmap(); 316 | 317 | function get1DArray(arr) { 318 | return +arr.join().split(","); 319 | } 320 | // create colormap 321 | let hsv_colors = [ 322 | [0.0, 0.0, 0.5], 323 | [0.0, 0.2, 0.7], 324 | [0.0, 0.4, 0.9], 325 | [0.0, 0.6, 1.0], 326 | [0.0, 0.8, 1.0], 327 | [0.2, 0.9, 0.8], 328 | [0.4, 1.0, 0.6], 329 | [0.6, 1.0, 0.4], 330 | [0.8, 1.0, 0.2], 331 | [1.0, 1.0, 0.0], 332 | [1.0, 0.9, 0.0], 333 | [1.0, 0.8, 0.0], 334 | [1.0, 0.6, 0.0], 335 | [1.0, 0.4, 0.0], 336 | [1.0, 0.2, 0.0], 337 | [0.9, 0.0, 0.0], 338 | [0.7, 0.0, 0.0], 339 | [0.5, 0.0, 0.0], 340 | [0.3, 0.0, 0.0], 341 | [0.1, 0.5, 0.0], 342 | ]; 343 | 344 | hsv_colors = hsv_colors.flat(); 345 | colorMapBuffer = device.createBuffer({ 346 | size: hsv_colors.length * 3 * 4, 347 | usage: GPUBufferUsage.UNIFORM, 348 | mappedAtCreation: true, 349 | }); 350 | 351 | let mapArray = new Float32Array(colorMapBuffer.getMappedRange()); 352 | 353 | mapArray.set(hsv_colors); 354 | colorMapBuffer.unmap(); 355 | 356 | MVP_Buffer = device.createBuffer({ 357 | size: 16 * 4, 358 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 359 | }); 360 | 361 | let viewMatrix = camera.matrixWorldInverse.elements 362 | projView = mat4.mul(projView, viewMatrix, proj); 363 | return projView; 364 | } 365 | 366 | async function createBindGroups() { 367 | mvp_BG = device.createBindGroup({ 368 | label: "uniform bindgroup - rendering", 369 | layout: renderPipeline.getBindGroupLayout(0), 370 | entries: [ 371 | { 372 | binding: 0, 373 | resource: { 374 | buffer: MVP_Buffer, 375 | }, 376 | }, 377 | { 378 | binding: 1, 379 | resource: { 380 | buffer: colorMapBuffer, 381 | }, 382 | }, 383 | { 384 | binding: 2, 385 | resource: { 386 | buffer: paramsBuffer, 387 | }, 388 | }, 389 | ], 390 | }); 391 | } 392 | 393 | async function createDepthBuffer() { 394 | renderDepthTexture = device.createTexture({ 395 | size: [canvas.width, canvas.height, 1], 396 | format: "depth24plus-stencil8", 397 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 398 | }); 399 | } 400 | 401 | async function encodedCommand() { 402 | // create render pass descriptor 403 | // console.log(currentAxis); 404 | 405 | let colorAttachment = { 406 | view: context.getCurrentTexture().createView(), 407 | clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 408 | loadOp: "clear", 409 | storeOp: "store", 410 | }; 411 | 412 | let depthAttachment = { 413 | view: renderDepthTexture.createView(), 414 | depthLoadOp: "clear", 415 | depthClearValue: 1.0, 416 | depthStoreOp: "store", 417 | stencilLoadOp: "clear", 418 | stencilClearValue: 0, 419 | stencilStoreOp: "store", 420 | }; 421 | 422 | renderPassDescriptor = { 423 | colorAttachments: [colorAttachment], 424 | depthStencilAttachment: depthAttachment, 425 | }; 426 | } 427 | 428 | async function update(timestamp) { 429 | { 430 | // update worldViewProj 431 | let proj = mat4.create(); 432 | let view = mat4.create(); 433 | 434 | { 435 | // proj 436 | const aspect = Math.abs(canvas.width / canvas.height); 437 | mat4.perspective(proj, 90, aspect, 0.1, 1000.0); 438 | } 439 | 440 | { 441 | // view 442 | let target = vec3.fromValues(2, 5, 0); 443 | // let r = 50; 444 | // let x = r * Math.sin(timestamp / 10) + target[0]; 445 | // let y = r * Math.cos(timestamp / 10) + target[1]; 446 | // let z = 10; 447 | 448 | let position = vec3.fromValues(5, 100, 100); 449 | let up = vec3.fromValues(0, 0, 1); 450 | mat4.lookAt(view, position, target, up); 451 | } 452 | const view_matrix = camera.matrixWorldInverse; 453 | mat4.multiply(worldViewProj, view_matrix, proj); 454 | } 455 | } 456 | 457 | async function stages(camera, proj, params) { 458 | await init(); 459 | await intRenderPipeline(); 460 | let projectionViewMatrix = await initUniform(camera, proj, params); 461 | return projectionViewMatrix; 462 | } 463 | 464 | // --------------------------------------------------------------------------- 465 | // i guess i am not using this 466 | 467 | async function renderStages(position, color) { 468 | numPoints = position.length / 3; 469 | positions = position; 470 | colors = color; 471 | await initVertexBuffer(); 472 | await createBindGroups(); 473 | await createDepthBuffer(); 474 | requestAnimationFrame(render2); 475 | } 476 | // ----------------------------------------------------------------------------- 477 | async function renderWrapper() { 478 | await createBindGroups(); 479 | await createDepthBuffer(); 480 | await updateMaxIntensity(); 481 | render(); 482 | // itenaryStart(moveCamera); 483 | } 484 | 485 | function render(timestamp) { 486 | stats.update(); 487 | var startTime = performance.now(); 488 | commandEncoder = device.createCommandEncoder(); 489 | // this is not helpful for tree traversal so model matrix rotation is removed for now 490 | let viewMatrix = camera.matrixWorldInverse.elements; 491 | // console.log(controls.object.position, controls.object.rotation, controls.target ) 492 | projView = mat4.mul(projView, proj, viewMatrix); 493 | controls.update(); 494 | // update(timestamp); 495 | encodedCommand(); 496 | 497 | // device.queue.writeBuffer(MVP_Buffer, 0, worldViewProj, 16); 498 | 499 | let wvStagingBuffer = device.createBuffer({ 500 | size: 4 * 16, 501 | usage: GPUBufferUsage.COPY_SRC, 502 | mappedAtCreation: true, 503 | }); 504 | const stagingUniformData = new Float32Array(wvStagingBuffer.getMappedRange()); 505 | stagingUniformData.set(projView); 506 | wvStagingBuffer.unmap(); 507 | commandEncoder.copyBufferToBuffer(wvStagingBuffer, 0, MVP_Buffer, 0, 64); 508 | let renderPass = commandEncoder.beginRenderPass(renderPassDescriptor); 509 | renderPass.setPipeline(renderPipeline); 510 | renderPass.setViewport(0, 0, canvas.width, canvas.height, 0.0, 1.0); 511 | renderPass.setBindGroup(0, mvp_BG); 512 | for (let key in bufferMap) { 513 | // console.log(bufferMap[key].position); 514 | renderPass.setVertexBuffer(0, bufferMap[key].position); 515 | renderPass.setVertexBuffer(1, bufferMap[key].color); 516 | numPoints = +bufferMap[key].position.label / 4; 517 | renderPass.draw(4, numPoints, 0, 0); 518 | } 519 | renderPass.end(); 520 | device.queue.submit([commandEncoder.finish()]); 521 | var endTime = performance.now(); 522 | requestAnimationFrame(render); 523 | } 524 | 525 | export { renderStages, device, stages, renderWrapper, throttle }; 526 | -------------------------------------------------------------------------------- /docs/js/webgl-util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Compute the view frustum in world space from the provided 3 | // column major projection * view matrix 4 | var Frustum = function (projView) { 5 | var rows = [vec4.create(), vec4.create(), vec4.create(), vec4.create()]; 6 | for (var i = 0; i < rows.length; ++i) { 7 | rows[i] = vec4.set( 8 | rows[i], 9 | projView[i], 10 | projView[4 + i], 11 | projView[8 + i], 12 | projView[12 + i] 13 | ); 14 | } 15 | 16 | this.planes = [ 17 | // -x plane 18 | vec4.add(vec4.create(), rows[3], rows[0]), 19 | // +x plane 20 | vec4.sub(vec4.create(), rows[3], rows[0]), 21 | // -y plane 22 | vec4.add(vec4.create(), rows[3], rows[1]), 23 | // +y plane 24 | vec4.sub(vec4.create(), rows[3], rows[1]), 25 | // -z plane 26 | vec4.add(vec4.create(), rows[3], rows[2]), 27 | // +z plane 28 | vec4.sub(vec4.create(), rows[3], rows[2]), 29 | ]; 30 | 31 | // Normalize the planes 32 | for (var i = 0; i < this.planes.length; ++i) { 33 | var s = 34 | 1.0 / 35 | Math.sqrt( 36 | this.planes[i][0] * this.planes[i][0] + 37 | this.planes[i][1] * this.planes[i][1] + 38 | this.planes[i][2] * this.planes[i][2] 39 | ); 40 | this.planes[i][0] *= s; 41 | this.planes[i][1] *= s; 42 | this.planes[i][2] *= s; 43 | this.planes[i][3] *= s; 44 | } 45 | 46 | // Compute the frustum points as well 47 | var invProjView = mat4.invert(mat4.create(), projView); 48 | this.points = [ 49 | // x_l, y_l, z_l 50 | vec4.set(vec4.create(), -1, -1, -1, 1), 51 | // x_h, y_l, z_l 52 | vec4.set(vec4.create(), 1, -1, -1, 1), 53 | // x_l, y_h, z_l 54 | vec4.set(vec4.create(), -1, 1, -1, 1), 55 | // x_h, y_h, z_l 56 | vec4.set(vec4.create(), 1, 1, -1, 1), 57 | // x_l, y_l, z_h 58 | vec4.set(vec4.create(), -1, -1, 1, 1), 59 | // x_h, y_l, z_h 60 | vec4.set(vec4.create(), 1, -1, 1, 1), 61 | // x_l, y_h, z_h 62 | vec4.set(vec4.create(), -1, 1, 1, 1), 63 | // x_h, y_h, z_h 64 | vec4.set(vec4.create(), 1, 1, 1, 1), 65 | ]; 66 | for (var i = 0; i < 8; ++i) { 67 | this.points[i] = vec4.transformMat4( 68 | this.points[i], 69 | this.points[i], 70 | invProjView 71 | ); 72 | this.points[i][0] /= this.points[i][3]; 73 | this.points[i][1] /= this.points[i][3]; 74 | this.points[i][2] /= this.points[i][3]; 75 | this.points[i][3] = 1.0; 76 | } 77 | }; 78 | 79 | // Check if the box is contained in the Frustum 80 | // The box should be [x_lower, y_lower, z_lower, x_upper, y_upper, z_upper] 81 | // This is done using Inigo Quilez's approach to help with large 82 | // bounds: https://www.iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm 83 | Frustum.prototype.containsBox = function (box) { 84 | // Test the box against each plane 85 | var vec = vec4.create(); 86 | var out = 0; 87 | for (var i = 0; i < this.planes.length; ++i) { 88 | out = 0; 89 | // x_l, y_l, z_l 90 | vec4.set(vec, box[0], box[1], box[2], 1.0); 91 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 92 | // x_h, y_l, z_l 93 | vec4.set(vec, box[3], box[1], box[2], 1.0); 94 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 95 | // x_l, y_h, z_l 96 | vec4.set(vec, box[0], box[4], box[2], 1.0); 97 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 98 | // x_h, y_h, z_l 99 | vec4.set(vec, box[3], box[4], box[2], 1.0); 100 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 101 | // x_l, y_l, z_h 102 | vec4.set(vec, box[0], box[1], box[5], 1.0); 103 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 104 | // x_h, y_l, z_h 105 | vec4.set(vec, box[3], box[1], box[5], 1.0); 106 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 107 | // x_l, y_h, z_h 108 | vec4.set(vec, box[0], box[4], box[5], 1.0); 109 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 110 | // x_h, y_h, z_h 111 | vec4.set(vec, box[3], box[4], box[5], 1.0); 112 | out += vec4.dot(this.planes[i], vec) < 0.0 ? 1 : 0; 113 | 114 | if (out == 8) { 115 | return false; 116 | } 117 | } 118 | 119 | // Test the frustum against the box 120 | out = 0; 121 | for (var i = 0; i < 8; ++i) { 122 | out += this.points[i][0] > box[3] ? 1 : 0; 123 | } 124 | if (out == 8) { 125 | return false; 126 | } 127 | 128 | out = 0; 129 | for (var i = 0; i < 8; ++i) { 130 | out += this.points[i][0] < box[0] ? 1 : 0; 131 | } 132 | if (out == 8) { 133 | return false; 134 | } 135 | 136 | out = 0; 137 | for (var i = 0; i < 8; ++i) { 138 | out += this.points[i][1] > box[4] ? 1 : 0; 139 | } 140 | if (out == 8) { 141 | return false; 142 | } 143 | 144 | out = 0; 145 | for (var i = 0; i < 8; ++i) { 146 | out += this.points[i][1] < box[1] ? 1 : 0; 147 | } 148 | if (out == 8) { 149 | return false; 150 | } 151 | 152 | out = 0; 153 | for (var i = 0; i < 8; ++i) { 154 | out += this.points[i][2] > box[5] ? 1 : 0; 155 | } 156 | if (out == 8) { 157 | return false; 158 | } 159 | 160 | out = 0; 161 | for (var i = 0; i < 8; ++i) { 162 | out += this.points[i][2] < box[2] ? 1 : 0; 163 | } 164 | if (out == 8) { 165 | return false; 166 | } 167 | return true; 168 | }; 169 | 170 | var Shader = function (gl, vertexSrc, fragmentSrc) { 171 | var self = this; 172 | this.program = compileShader(gl, vertexSrc, fragmentSrc); 173 | 174 | var regexUniform = /uniform[^;]+[ ](\w+);/g; 175 | var matchUniformName = /uniform[^;]+[ ](\w+);/; 176 | 177 | this.uniforms = {}; 178 | 179 | var vertexUnifs = vertexSrc.match(regexUniform); 180 | var fragUnifs = fragmentSrc.match(regexUniform); 181 | 182 | if (vertexUnifs) { 183 | vertexUnifs.forEach(function (unif) { 184 | var m = unif.match(matchUniformName); 185 | self.uniforms[m[1]] = -1; 186 | }); 187 | } 188 | if (fragUnifs) { 189 | fragUnifs.forEach(function (unif) { 190 | var m = unif.match(matchUniformName); 191 | self.uniforms[m[1]] = -1; 192 | }); 193 | } 194 | 195 | for (var unif in this.uniforms) { 196 | this.uniforms[unif] = gl.getUniformLocation(this.program, unif); 197 | } 198 | }; 199 | 200 | Shader.prototype.use = function (gl) { 201 | gl.useProgram(this.program); 202 | }; 203 | 204 | // Compile and link the shaders vert and frag. vert and frag should contain 205 | // the shader source code for the vertex and fragment shaders respectively 206 | // Returns the compiled and linked program, or null if compilation or linking failed 207 | var compileShader = function (gl, vert, frag) { 208 | var vs = gl.createShader(gl.VERTEX_SHADER); 209 | gl.shaderSource(vs, vert); 210 | gl.compileShader(vs); 211 | if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { 212 | alert("Vertex shader failed to compile, see console for log"); 213 | console.log(gl.getShaderInfoLog(vs)); 214 | return null; 215 | } 216 | 217 | var fs = gl.createShader(gl.FRAGMENT_SHADER); 218 | gl.shaderSource(fs, frag); 219 | gl.compileShader(fs); 220 | if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { 221 | alert("Fragment shader failed to compile, see console for log"); 222 | console.log(gl.getShaderInfoLog(fs)); 223 | return null; 224 | } 225 | 226 | var program = gl.createProgram(); 227 | gl.attachShader(program, vs); 228 | gl.attachShader(program, fs); 229 | gl.linkProgram(program); 230 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 231 | alert("Shader failed to link, see console for log"); 232 | console.log(gl.getProgramInfoLog(program)); 233 | return null; 234 | } 235 | return program; 236 | }; 237 | 238 | var getGLExtension = function (gl, ext) { 239 | if (!gl.getExtension(ext)) { 240 | alert("Missing " + ext + " WebGL extension"); 241 | return false; 242 | } 243 | return true; 244 | }; 245 | 246 | /* The arcball camera will be placed at the position 'eye', rotating 247 | * around the point 'center', with the up vector 'up'. 'screenDims' 248 | * should be the dimensions of the canvas or region taking mouse input 249 | * so the mouse positions can be normalized into [-1, 1] from the pixel 250 | * coordinates. 251 | */ 252 | var ArcballCamera = function (eye, center, up, zoomSpeed, screenDims) { 253 | var veye = vec3.set(vec3.create(), eye[0], eye[1], eye[2]); 254 | var vcenter = vec3.set(vec3.create(), center[0], center[1], center[2]); 255 | var vup = vec3.set(vec3.create(), up[0], up[1], up[2]); 256 | vec3.normalize(vup, vup); 257 | 258 | var zAxis = vec3.sub(vec3.create(), vcenter, veye); 259 | var viewDist = vec3.len(zAxis); 260 | vec3.normalize(zAxis, zAxis); 261 | 262 | var xAxis = vec3.cross(vec3.create(), zAxis, vup); 263 | vec3.normalize(xAxis, xAxis); 264 | 265 | var yAxis = vec3.cross(vec3.create(), xAxis, zAxis); 266 | vec3.normalize(yAxis, yAxis); 267 | 268 | vec3.cross(xAxis, zAxis, yAxis); 269 | vec3.normalize(xAxis, xAxis); 270 | 271 | this.zoomSpeed = zoomSpeed; 272 | this.invScreen = [1.0 / screenDims[0], 1.0 / screenDims[1]]; 273 | 274 | this.centerTranslation = mat4.fromTranslation(mat4.create(), center); 275 | mat4.invert(this.centerTranslation, this.centerTranslation); 276 | 277 | var vt = vec3.set(vec3.create(), 0, 0, -1.0 * viewDist); 278 | this.translation = mat4.fromTranslation(mat4.create(), vt); 279 | 280 | var rotMat = mat3.fromValues( 281 | xAxis[0], 282 | xAxis[1], 283 | xAxis[2], 284 | yAxis[0], 285 | yAxis[1], 286 | yAxis[2], 287 | -zAxis[0], 288 | -zAxis[1], 289 | -zAxis[2] 290 | ); 291 | mat3.transpose(rotMat, rotMat); 292 | this.rotation = quat.fromMat3(quat.create(), rotMat); 293 | quat.normalize(this.rotation, this.rotation); 294 | 295 | this.camera = mat4.create(); 296 | this.invCamera = mat4.create(); 297 | this.updateCameraMatrix(); 298 | }; 299 | 300 | ArcballCamera.prototype.rotate = function (prevMouse, curMouse) { 301 | var mPrev = vec2.set( 302 | vec2.create(), 303 | clamp(prevMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0), 304 | clamp(1.0 - prevMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0) 305 | ); 306 | 307 | var mCur = vec2.set( 308 | vec2.create(), 309 | clamp(curMouse[0] * 2.0 * this.invScreen[0] - 1.0, -1.0, 1.0), 310 | clamp(1.0 - curMouse[1] * 2.0 * this.invScreen[1], -1.0, 1.0) 311 | ); 312 | 313 | var mPrevBall = screenToArcball(mPrev); 314 | var mCurBall = screenToArcball(mCur); 315 | // rotation = curBall * prevBall * rotation 316 | this.rotation = quat.mul(this.rotation, mPrevBall, this.rotation); 317 | this.rotation = quat.mul(this.rotation, mCurBall, this.rotation); 318 | 319 | this.updateCameraMatrix(); 320 | }; 321 | 322 | ArcballCamera.prototype.zoom = function (amount) { 323 | var vt = vec3.set( 324 | vec3.create(), 325 | 0.0, 326 | 0.0, 327 | amount * this.invScreen[1] * this.zoomSpeed 328 | ); 329 | var t = mat4.fromTranslation(mat4.create(), vt); 330 | this.translation = mat4.mul(this.translation, t, this.translation); 331 | if (this.translation[14] >= -0.2) { 332 | this.translation[14] = -0.2; 333 | } 334 | this.updateCameraMatrix(); 335 | }; 336 | 337 | ArcballCamera.prototype.pan = function (mouseDelta) { 338 | var delta = vec4.set( 339 | vec4.create(), 340 | mouseDelta[0] * this.invScreen[0] * Math.abs(this.translation[14]), 341 | mouseDelta[1] * this.invScreen[1] * Math.abs(this.translation[14]), 342 | 0, 343 | 0 344 | ); 345 | var worldDelta = vec4.transformMat4(vec4.create(), delta, this.invCamera); 346 | var translation = mat4.fromTranslation(mat4.create(), worldDelta); 347 | this.centerTranslation = mat4.mul( 348 | this.centerTranslation, 349 | translation, 350 | this.centerTranslation 351 | ); 352 | this.updateCameraMatrix(); 353 | }; 354 | 355 | ArcballCamera.prototype.updateCameraMatrix = function () { 356 | // camera = translation * rotation * centerTranslation 357 | var rotMat = mat4.fromQuat(mat4.create(), this.rotation); 358 | this.camera = mat4.mul(this.camera, rotMat, this.centerTranslation); 359 | this.camera = mat4.mul(this.camera, this.translation, this.camera); 360 | this.invCamera = mat4.invert(this.invCamera, this.camera); 361 | }; 362 | 363 | ArcballCamera.prototype.eyePos = function () { 364 | return [this.invCamera[12], this.invCamera[13], this.invCamera[14]]; 365 | }; 366 | 367 | ArcballCamera.prototype.eyeDir = function () { 368 | var dir = vec4.set(vec4.create(), 0.0, 0.0, -1.0, 0.0); 369 | dir = vec4.transformMat4(dir, dir, this.invCamera); 370 | dir = vec4.normalize(dir, dir); 371 | return [dir[0], dir[1], dir[2]]; 372 | }; 373 | 374 | ArcballCamera.prototype.upDir = function () { 375 | var dir = vec4.set(vec4.create(), 0.0, 1.0, 0.0, 0.0); 376 | dir = vec4.transformMat4(dir, dir, this.invCamera); 377 | dir = vec4.normalize(dir, dir); 378 | return [dir[0], dir[1], dir[2]]; 379 | }; 380 | 381 | var screenToArcball = function (p) { 382 | var dist = vec2.dot(p, p); 383 | if (dist <= 1.0) { 384 | return quat.set(quat.create(), p[0], p[1], Math.sqrt(1.0 - dist), 0); 385 | } else { 386 | var unitP = vec2.normalize(vec2.create(), p); 387 | // cgmath is w, x, y, z 388 | // glmatrix is x, y, z, w 389 | return quat.set(quat.create(), unitP[0], unitP[1], 0, 0); 390 | } 391 | }; 392 | var clamp = function (a, min, max) { 393 | return a < min ? min : a > max ? max : a; 394 | }; 395 | 396 | var pointDist = function (a, b) { 397 | var v = [b[0] - a[0], b[1] - a[1]]; 398 | return Math.sqrt(Math.pow(v[0], 2.0) + Math.pow(v[1], 2.0)); 399 | }; 400 | 401 | var Buffer = function (capacity, dtype) { 402 | this.len = 0; 403 | this.capacity = capacity; 404 | if (dtype == "uint8") { 405 | this.buffer = new Uint8Array(capacity); 406 | } else if (dtype == "int8") { 407 | this.buffer = new Int8Array(capacity); 408 | } else if (dtype == "uint16") { 409 | this.buffer = new Uint16Array(capacity); 410 | } else if (dtype == "int16") { 411 | this.buffer = new Int16Array(capacity); 412 | } else if (dtype == "uint32") { 413 | this.buffer = new Uint32Array(capacity); 414 | } else if (dtype == "int32") { 415 | this.buffer = new Int32Array(capacity); 416 | } else if (dtype == "float32") { 417 | this.buffer = new Float32Array(capacity); 418 | } else if (dtype == "float64") { 419 | this.buffer = new Float64Array(capacity); 420 | } else { 421 | console.log("ERROR: unsupported type " + dtype); 422 | } 423 | }; 424 | 425 | Buffer.prototype.append = function (buf) { 426 | if (this.len + buf.length >= this.capacity) { 427 | var newCap = Math.floor( 428 | Math.max(this.capacity * 1.5), 429 | this.len + buf.length 430 | ); 431 | var tmp = new this.buffer.constructor(newCap); 432 | tmp.set(this.buffer); 433 | 434 | this.capacity = newCap; 435 | this.buffer = tmp; 436 | } 437 | this.buffer.set(buf, this.len); 438 | this.len += buf.length; 439 | }; 440 | 441 | Buffer.prototype.clear = function () { 442 | this.len = 0; 443 | }; 444 | 445 | Buffer.prototype.stride = function () { 446 | return this.buffer.BYTES_PER_ELEMENT; 447 | }; 448 | 449 | Buffer.prototype.view = function (offset, length) { 450 | return new this.buffer.constructor(this.buffer.buffer, offset, length); 451 | }; 452 | 453 | // Various utilities that don't really fit anywhere else 454 | 455 | // Parse the hex string to RGB values in [0, 255] 456 | var hexToRGB = function (hex) { 457 | var val = parseInt(hex.substr(1), 16); 458 | var r = (val >> 16) & 255; 459 | var g = (val >> 8) & 255; 460 | var b = val & 255; 461 | return [r, g, b]; 462 | }; 463 | 464 | // Parse the hex string to RGB values in [0, 1] 465 | var hexToRGBf = function (hex) { 466 | var c = hexToRGB(hex); 467 | return [c[0] / 255.0, c[1] / 255.0, c[2] / 255.0]; 468 | }; 469 | 470 | /* The controller can register callbacks for various events on a canvas: 471 | * 472 | * mousemove: function(prevMouse, curMouse, evt) 473 | * receives both regular mouse events, and single-finger drags (sent as a left-click), 474 | * 475 | * press: function(curMouse, evt) 476 | * receives mouse click and touch start events 477 | * 478 | * wheel: function(amount) 479 | * mouse wheel scrolling 480 | * 481 | * pinch: function(amount) 482 | * two finger pinch, receives the distance change between the fingers 483 | * 484 | * twoFingerDrag: function(dragVector) 485 | * two finger drag, receives the drag movement amount 486 | */ 487 | var Controller = function () { 488 | this.mousemove = null; 489 | this.press = null; 490 | this.wheel = null; 491 | this.twoFingerDrag = null; 492 | this.pinch = null; 493 | }; 494 | 495 | Controller.prototype.registerForCanvas = function (canvas) { 496 | var prevMouse = null; 497 | var mouseState = [false, false]; 498 | var self = this; 499 | canvas.addEventListener("mousemove", function (evt) { 500 | evt.preventDefault(); 501 | var rect = canvas.getBoundingClientRect(); 502 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 503 | if (!prevMouse) { 504 | prevMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 505 | } else if (self.mousemove) { 506 | self.mousemove(prevMouse, curMouse, evt); 507 | } 508 | prevMouse = curMouse; 509 | }); 510 | 511 | canvas.addEventListener("mousedown", function (evt) { 512 | evt.preventDefault(); 513 | var rect = canvas.getBoundingClientRect(); 514 | var curMouse = [evt.clientX - rect.left, evt.clientY - rect.top]; 515 | if (self.press) { 516 | self.press(curMouse, evt); 517 | } 518 | }); 519 | 520 | canvas.addEventListener("wheel", function (evt) { 521 | evt.preventDefault(); 522 | if (self.wheel) { 523 | self.wheel(-evt.deltaY); 524 | } 525 | }); 526 | 527 | canvas.oncontextmenu = function (evt) { 528 | evt.preventDefault(); 529 | }; 530 | 531 | var touches = {}; 532 | canvas.addEventListener("touchstart", function (evt) { 533 | var rect = canvas.getBoundingClientRect(); 534 | evt.preventDefault(); 535 | for (var i = 0; i < evt.changedTouches.length; ++i) { 536 | var t = evt.changedTouches[i]; 537 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top]; 538 | if (evt.changedTouches.length == 1 && self.press) { 539 | self.press(touches[t.identifier], evt); 540 | } 541 | } 542 | }); 543 | 544 | canvas.addEventListener("touchmove", function (evt) { 545 | evt.preventDefault(); 546 | var rect = canvas.getBoundingClientRect(); 547 | var numTouches = Object.keys(touches).length; 548 | // Single finger to rotate the camera 549 | if (numTouches == 1) { 550 | if (self.mousemove) { 551 | var t = evt.changedTouches[0]; 552 | var prevTouch = touches[t.identifier]; 553 | var curTouch = [t.clientX - rect.left, t.clientY - rect.top]; 554 | evt.buttons = 1; 555 | self.mousemove(prevTouch, curTouch, evt); 556 | } 557 | } else { 558 | var curTouches = {}; 559 | for (var i = 0; i < evt.changedTouches.length; ++i) { 560 | var t = evt.changedTouches[i]; 561 | curTouches[t.identifier] = [ 562 | t.clientX - rect.left, 563 | t.clientY - rect.top, 564 | ]; 565 | } 566 | 567 | // If some touches didn't change make sure we have them in 568 | // our curTouches list to compute the pinch distance 569 | // Also get the old touch points to compute the distance here 570 | var oldTouches = []; 571 | for (t in touches) { 572 | if (!(t in curTouches)) { 573 | curTouches[t] = touches[t]; 574 | } 575 | oldTouches.push(touches[t]); 576 | } 577 | 578 | var newTouches = []; 579 | for (t in curTouches) { 580 | newTouches.push(curTouches[t]); 581 | } 582 | 583 | // Determine if the user is pinching or panning 584 | var motionVectors = [ 585 | vec2.set( 586 | vec2.create(), 587 | newTouches[0][0] - oldTouches[0][0], 588 | newTouches[0][1] - oldTouches[0][1] 589 | ), 590 | vec2.set( 591 | vec2.create(), 592 | newTouches[1][0] - oldTouches[1][0], 593 | newTouches[1][1] - oldTouches[1][1] 594 | ), 595 | ]; 596 | var motionDirs = [vec2.create(), vec2.create()]; 597 | vec2.normalize(motionDirs[0], motionVectors[0]); 598 | vec2.normalize(motionDirs[1], motionVectors[1]); 599 | 600 | var pinchAxis = vec2.set( 601 | vec2.create(), 602 | oldTouches[1][0] - oldTouches[0][0], 603 | oldTouches[1][1] - oldTouches[0][1] 604 | ); 605 | vec2.normalize(pinchAxis, pinchAxis); 606 | 607 | var panAxis = vec2.lerp( 608 | vec2.create(), 609 | motionVectors[0], 610 | motionVectors[1], 611 | 0.5 612 | ); 613 | vec2.normalize(panAxis, panAxis); 614 | 615 | var pinchMotion = [ 616 | vec2.dot(pinchAxis, motionDirs[0]), 617 | vec2.dot(pinchAxis, motionDirs[1]), 618 | ]; 619 | var panMotion = [ 620 | vec2.dot(panAxis, motionDirs[0]), 621 | vec2.dot(panAxis, motionDirs[1]), 622 | ]; 623 | 624 | // If we're primarily moving along the pinching axis and in the opposite direction with 625 | // the fingers, then the user is zooming. 626 | // Otherwise, if the fingers are moving along the same direction they're panning 627 | if ( 628 | self.pinch && 629 | Math.abs(pinchMotion[0]) > 0.5 && 630 | Math.abs(pinchMotion[1]) > 0.5 && 631 | Math.sign(pinchMotion[0]) != Math.sign(pinchMotion[1]) 632 | ) { 633 | // Pinch distance change for zooming 634 | var oldDist = pointDist(oldTouches[0], oldTouches[1]); 635 | var newDist = pointDist(newTouches[0], newTouches[1]); 636 | self.pinch(newDist - oldDist); 637 | } else if ( 638 | self.twoFingerDrag && 639 | Math.abs(panMotion[0]) > 0.5 && 640 | Math.abs(panMotion[1]) > 0.5 && 641 | Math.sign(panMotion[0]) == Math.sign(panMotion[1]) 642 | ) { 643 | // Pan by the average motion of the two fingers 644 | var panAmount = vec2.lerp( 645 | vec2.create(), 646 | motionVectors[0], 647 | motionVectors[1], 648 | 0.5 649 | ); 650 | panAmount[1] = -panAmount[1]; 651 | self.twoFingerDrag(panAmount); 652 | } 653 | } 654 | 655 | // Update the existing list of touches with the current positions 656 | for (var i = 0; i < evt.changedTouches.length; ++i) { 657 | var t = evt.changedTouches[i]; 658 | touches[t.identifier] = [t.clientX - rect.left, t.clientY - rect.top]; 659 | } 660 | }); 661 | 662 | var touchEnd = function (evt) { 663 | evt.preventDefault(); 664 | for (var i = 0; i < evt.changedTouches.length; ++i) { 665 | var t = evt.changedTouches[i]; 666 | delete touches[t.identifier]; 667 | } 668 | }; 669 | canvas.addEventListener("touchcancel", touchEnd); 670 | canvas.addEventListener("touchend", touchEnd); 671 | }; 672 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { Octree, Box, Point } from "./octree"; 3 | import { OrbitControls } from "three/addons/controls/OrbitControls.js"; 4 | 5 | import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer"; 6 | import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass"; 7 | import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass"; 8 | import { CopyShader } from "three/examples/jsm/shaders/CopyShader"; 9 | import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader"; 10 | import Stats from "three/examples/jsm/libs/stats.module"; 11 | import { Copc, Key } from "copc"; 12 | import Worker from "./worker/fetcher.worker.js"; 13 | import { renderStages, device, stages, renderWrapper } from "./webgpu/renderer"; 14 | import { traverseTreeWrapper } from "./passiveloader"; 15 | import { 16 | write, 17 | read, 18 | doesExist, 19 | clear, 20 | create_P_Meta_Cache, 21 | throttled_Update_Pers_Cache, 22 | } from "./private_origin/file_manager"; 23 | import { 24 | p_cache, 25 | get_inCache, 26 | getLRU_inCache, 27 | getMRU_inCache, 28 | sortObjectIntoMap, 29 | mapIntoJSON, 30 | put_inCache, 31 | } from "./private_origin/cache_manager"; 32 | 33 | import "./styles/main.css"; 34 | import { fillArray, fillMidNodes, updateHtmlUI } from "./helper"; 35 | import { cache } from "./lru-cache/index"; 36 | 37 | let loadedFromCache = 0; 38 | let fetchedFromsource = 0; 39 | const bytes = new Float32Array(59); 40 | const source_file_name = process.env.filename.split("/").pop(); 41 | let state_meta = {}; 42 | 43 | // ------------------- used to clear intital POFS that got created while writing code --------------- 44 | // (async () => { 45 | // const root = await navigator.storage.getDirectory(); 46 | // const fileHandle2 = await root.getFileHandle("1-1-0-0.txt"); 47 | // await fileHandle2.remove(); 48 | // const fileHandle = await root.getFileHandle("0-0-0-0.txt"); 49 | // await fileHandle.remove(); 50 | // const fileHandle1 = await root.getFileHandle("1-0-0-0.txt"); 51 | // await fileHandle1.remove(); 52 | 53 | // // console.log(await doesExist("0-1-0-0")); 54 | // })(); 55 | // --------------------------------------------------------------------------------------------------- 56 | 57 | let bufferMap = {}; 58 | let wait = false; 59 | let toDeleteMap = {}; 60 | let toDelete = false; 61 | let postMessageRes = 100; 62 | let positions = []; 63 | let colors = []; 64 | let workerCount = 0; 65 | const clock = new THREE.Clock(); 66 | const workers = new Array(1).fill(null); 67 | let TotalCount = 0; 68 | const MAX_WORKERS = navigator.hardwareConcurrency - 1; 69 | let promises = []; 70 | let nodePages, nodePagesString; 71 | let pagesString; 72 | let camera, proj; 73 | let copcString; 74 | let x_min, 75 | y_min, 76 | z_min, 77 | x_max, 78 | y_max, 79 | z_max, 80 | widthx, 81 | widthy, 82 | widthz, 83 | center_x, 84 | center_y, 85 | center_z, 86 | scaleFactor, 87 | params, 88 | controls; 89 | let pers_cache; 90 | let global_max_intensity = 0; 91 | let prefetch_keyCountMap; 92 | 93 | const canvas = document.getElementById("screen-canvas"); 94 | 95 | canvas.width = window.innerWidth * (window.devicePixelRatio || 1); 96 | canvas.height = window.innerHeight * (window.devicePixelRatio || 1); 97 | 98 | function isTerminated(worker) { 99 | try { 100 | worker.postMessage(() => {}); 101 | return true; 102 | } catch (e) { 103 | return false; 104 | } 105 | } 106 | 107 | function createWorker(data1, data2) { 108 | let myNode = data1.split("-").map(Number); 109 | let myLevel = myNode[0]; 110 | return new Promise((resolve) => { 111 | let worker = new Worker(); 112 | worker.onmessage = (event) => { 113 | let postMessageRes = event.data; 114 | if (postMessageRes == 200) { 115 | worker.postMessage([ 116 | nodePagesString, 117 | pagesString, 118 | copcString, 119 | data1, 120 | data2, 121 | [ 122 | x_min, 123 | y_min, 124 | z_min, 125 | widthx, 126 | widthy, 127 | widthz, 128 | scaleFactor[0], 129 | scaleFactor[1], 130 | scaleFactor[2], 131 | myLevel, 132 | ], 133 | ]); 134 | } else { 135 | workerCount += 1; 136 | let position = postMessageRes[0]; 137 | let color = postMessageRes[1]; 138 | let [minZ, maxZ, maxIntensity, dataLevel] = postMessageRes[2]; 139 | if (maxIntensity > global_max_intensity) { 140 | global_max_intensity = maxIntensity; 141 | } 142 | let localPosition = []; 143 | let localColor = []; 144 | for (let i = 0; i < position.length; i++) { 145 | positions.push(position[i]); 146 | if (i > 0 && i % 3 == 0) { 147 | localPosition.push(dataLevel); 148 | } 149 | localPosition.push(position[i]); 150 | colors.push(color[i]); 151 | localColor.push(color[i]); 152 | } 153 | localPosition.push(dataLevel); 154 | 155 | if (workerCount == MAX_WORKERS) { 156 | workerCount = 0; 157 | promises = []; 158 | } 159 | worker.terminate(); 160 | resolve([localPosition, localColor, data1, maxIntensity]); 161 | } 162 | }; 163 | }); 164 | } 165 | 166 | let boxGroup = new THREE.Group(); 167 | let scene_width = 1000; 168 | let scene_height = 1000; 169 | let scene_depth = 1000; 170 | let scale = 1; 171 | let qt; 172 | let mapCamera, 173 | mapSizeX = 128, 174 | mapSizeY = 64; 175 | let _width = window.innerWidth; 176 | let _height = window.innerHeight; 177 | let right = 1024, 178 | left = -1024, 179 | top = 1024, 180 | bottom = -1024; 181 | 182 | let isIntensityPresent; 183 | 184 | function findLevel(qt) { 185 | // traverse octre 186 | let threshold = 100; 187 | let cameraPosition = controls.object.position; 188 | // remove all bounding box 3d object after disposing before every check 189 | for (let i = 0, _length = boxGroup.children.length; i < _length; i++) { 190 | boxGroup.children[i].material.dispose(); 191 | boxGroup.children[i].geometry.dispose(); 192 | } 193 | 194 | boxGroup.remove(...boxGroup.children); 195 | 196 | function traverseTree(node = qt) { 197 | if (node == null) { 198 | return null; 199 | } 200 | boxGroup.add(node.box.mesh); 201 | if (!node.isDivided) { 202 | return [...node.points, ...node.buffer]; 203 | } 204 | let myDistanceFromCamera = cameraPosition.distanceTo( 205 | new THREE.Vector3(node.box.x, node.box.y, node.box.z) 206 | ); 207 | if (myDistanceFromCamera > threshold) { 208 | return [...node.points, ...node.buffer]; 209 | } 210 | let children = [ 211 | node.minNE, 212 | node.minNW, 213 | node.minSW, 214 | node.minSE, 215 | node.maxNE, 216 | node.maxNW, 217 | node.maxSW, 218 | node.maxSE, 219 | ]; 220 | let results = [...node.points, ...node.buffer]; 221 | for (let i = 0, _length = children.length; i < _length; i++) { 222 | let points = traverseTree(children[i]); 223 | results.push(...points); 224 | } 225 | return results; 226 | } 227 | return traverseTree(); 228 | } 229 | function onWindowResize() { 230 | camera.aspect = window.innerWidth / window.innerHeight; 231 | camera.updateProjectionMatrix(); 232 | renderer.setSize(window.innerWidth, window.innerHeight); 233 | } 234 | 235 | function sleep(ms) { 236 | return new Promise((resolve) => setTimeout(resolve, ms)); 237 | } 238 | 239 | let keyCountMap; 240 | 241 | const createBuffer = (positions, colors) => { 242 | let size = positions.length; 243 | // console.log("size is ", size); 244 | let positionBuffer = device.createBuffer({ 245 | label: `${size}`, 246 | size: size * 4, 247 | usage: GPUBufferUsage.VERTEX, 248 | mappedAtCreation: true, 249 | }); 250 | 251 | let positionMappedArray = new Float32Array(positionBuffer.getMappedRange()); 252 | positionMappedArray.set(positions); 253 | positionBuffer.unmap(); 254 | 255 | let colorBuffer = device.createBuffer({ 256 | label: `${size}`, 257 | size: size * 4, 258 | usage: GPUBufferUsage.VERTEX, 259 | mappedAtCreation: true, 260 | }); 261 | 262 | let colorMappedArray = new Float32Array(colorBuffer.getMappedRange()); 263 | colorMappedArray.set(colors); 264 | colorBuffer.unmap(); 265 | return [positionBuffer, colorBuffer]; 266 | }; 267 | 268 | const syncThread = async () => { 269 | await Promise.all(promises).then(async (response) => { 270 | for (let i = 0, _length = response.length; i < _length; i++) { 271 | let data = response[i]; 272 | let fileName = data[2]; 273 | 274 | let data_json = { 275 | position: data[0], 276 | color: data[1], 277 | maxIntensity: data[3] 278 | }; 279 | 280 | let data_json_stringify = JSON.stringify(data_json); 281 | await write(`${source_file_name}-${fileName}`, data_json_stringify); 282 | state_meta[fileName] = { 283 | count: 1, 284 | date: new Date(), 285 | }; 286 | // console.log("data sent from worker is", data[0].length); 287 | let [positionBuffer, colorBuffer] = createBuffer(data[0], data[1]); 288 | 289 | bufferMap[data[2]] = { 290 | position: positionBuffer, 291 | color: colorBuffer, 292 | maxIntensity: data[3] 293 | }; 294 | } 295 | // console.log(bufferMap); 296 | // console.log("one chunk finish"); 297 | }); 298 | }; 299 | 300 | const syncThread_Prefetch = async () => { 301 | await Promise.all(promises).then(async (response) => { 302 | for (let i = 0, _length = response.length; i < _length; i++) { 303 | let data = response[i]; 304 | let fileName = data[2]; 305 | 306 | let data_json = { 307 | position: data[0], 308 | color: data[1], 309 | maxIntensity: data[3] 310 | }; 311 | let data_json_stringify = JSON.stringify(data_json); 312 | await write(`${source_file_name}-${fileName}`, data_json_stringify); 313 | state_meta[fileName] = { 314 | count: 1, 315 | date: new Date(), 316 | }; 317 | cache.set(fileName, data_json_stringify); 318 | } 319 | }); 320 | }; 321 | 322 | 323 | 324 | async function filterkeyCountMap_Prefetch(keyMap) { 325 | 326 | 327 | let afterCheckingCache = []; 328 | 329 | for (let i = 0; i < keyMap.length; i += 2) { 330 | let cachedResult = cache.get(keyMap[i]); 331 | if (!cachedResult) { 332 | afterCheckingCache.push(keyMap[i], keyMap[i + 1]); 333 | } 334 | } 335 | 336 | let filteredElements = []; 337 | for (let i = 0; i < afterCheckingCache.length; i += 2) { 338 | let [Exist, data] = await doesExist( 339 | `${source_file_name}-${afterCheckingCache[i]}` 340 | ); 341 | if (Exist) { 342 | cache.set(afterCheckingCache[i], JSON.stringify(data)); 343 | pers_cache = get_inCache(pers_cache, afterCheckingCache[i]); 344 | } else { 345 | filteredElements.push(afterCheckingCache[i], afterCheckingCache[i + 1]); 346 | pers_cache = put_inCache(pers_cache, afterCheckingCache[i], { 347 | count: 1, 348 | date: Date.now(), 349 | }); 350 | } 351 | } 352 | await throttled_Update_Pers_Cache(mapIntoJSON(cache)); 353 | return filteredElements; 354 | } 355 | 356 | async function filterkeyCountMap(keyMap) { 357 | 358 | // computing node fetch counts 359 | let nodeNotFoundInBuffer = 0; 360 | let nodeFoundInBuffer = 0; 361 | let nodeFoundInLRU = 0; 362 | let nodeFoundInPersistent = 0; 363 | let nodeToFetch = 0; 364 | 365 | let newKeyMap = []; 366 | let newBufferMap = {}; 367 | for (const key in toDeleteMap) { 368 | toDeleteMap[key].position.destroy(); 369 | toDeleteMap[key].color.destroy(); 370 | delete toDeleteMap[key]; 371 | } 372 | 373 | let existingBuffers = Object.keys(bufferMap); 374 | let toDeleteArray = existingBuffers.reduce((acc, val) => { 375 | acc[val] = true; 376 | return acc; 377 | }, {}); 378 | 379 | const startTime1 = performance.now(); 380 | for (let i = 0; i < keyMap.length; i += 2) { 381 | if (!(keyMap[i] in bufferMap)) { 382 | newKeyMap.push(keyMap[i], keyMap[i + 1]); 383 | nodeNotFoundInBuffer++ 384 | } else { 385 | // console.log(`found ${keyMap[i]} in existing buffer`); 386 | nodeFoundInBuffer++; 387 | let maxIntensity = bufferMap[keyMap[i]].maxIntensity; 388 | newBufferMap[keyMap[i]] = { 389 | position: bufferMap[keyMap[i]].position, 390 | color: bufferMap[keyMap[i]].color, 391 | maxIntensity: maxIntensity 392 | }; 393 | if (maxIntensity > global_max_intensity) { 394 | global_max_intensity = maxIntensity; 395 | } 396 | pers_cache = get_inCache(pers_cache, keyMap[i]); 397 | delete toDeleteArray[keyMap[i]]; 398 | } 399 | } 400 | const endTime1 = performance.now(); 401 | // console.log(`Time taken: ${endTime1 - startTime1}ms`); 402 | 403 | // --------------------- these are new ones 404 | // are they in non-persistence cache? 405 | const startTime2 = performance.now(); 406 | 407 | let afterCheckingCache = []; 408 | for (let i = 0; i < newKeyMap.length; i += 2) { 409 | let cachedResult = cache.get(newKeyMap[i]); 410 | if (cachedResult) { 411 | nodeFoundInLRU++ 412 | // console.log(`found ${newKeyMap[i]} in cache`); 413 | cachedResult = JSON.parse(cachedResult); 414 | pers_cache = get_inCache(pers_cache, newKeyMap[i]); 415 | let [positionBuffer, colorBuffer] = createBuffer( 416 | cachedResult.position, 417 | cachedResult.color 418 | ); 419 | const maxIntensity = cachedResult.maxIntensity 420 | newBufferMap[newKeyMap[i]] = { 421 | position: positionBuffer, 422 | color: colorBuffer, 423 | maxIntensity: maxIntensity 424 | }; 425 | if(maxIntensity > global_max_intensity){ 426 | global_max_intensity = maxIntensity; 427 | } 428 | } else { 429 | afterCheckingCache.push(newKeyMap[i], newKeyMap[i + 1]); 430 | } 431 | } 432 | const endTime2 = performance.now(); 433 | // console.log(`Time taken: ${endTime2 - startTime2}ms`); 434 | 435 | const startTime3 = performance.now(); 436 | let filteredElements = []; 437 | for (let i = 0; i < afterCheckingCache.length; i += 2) { 438 | let [Exist, data] = await doesExist( 439 | `${source_file_name}-${afterCheckingCache[i]}` 440 | ); 441 | if (Exist) { 442 | console.log("found in POFS") 443 | // console.log(`found ${afterCheckingCache[i]} in POFS`); 444 | // let data = await read(afterCheckingCache[i]); 445 | nodeFoundInPersistent++; 446 | let [positionBuffer, colorBuffer] = createBuffer( 447 | data.position, 448 | data.color 449 | ); 450 | newBufferMap[afterCheckingCache[i]] = { 451 | position: positionBuffer, 452 | color: colorBuffer, 453 | maxIntensity: data.maxIntensity 454 | }; 455 | if(data.maxIntensity > global_max_intensity){ 456 | global_max_intensity = data.maxIntensity; 457 | } 458 | cache.set(afterCheckingCache[i], JSON.stringify(data)); 459 | pers_cache = get_inCache(pers_cache, afterCheckingCache[i]); 460 | } else { 461 | filteredElements.push(afterCheckingCache[i], afterCheckingCache[i + 1]); 462 | pers_cache = put_inCache(pers_cache, afterCheckingCache[i], { 463 | count: 1, 464 | date: Date.now(), 465 | }); 466 | nodeToFetch++ 467 | } 468 | } 469 | const endTime3 = performance.now(); 470 | // console.log(`Time taken: ${endTime3 - startTime3}ms`); 471 | 472 | await throttled_Update_Pers_Cache(mapIntoJSON(cache)); 473 | //------------------------------------------------------------------------- 474 | 475 | for (let key in toDeleteArray) { 476 | toDeleteMap[key] = { 477 | position: bufferMap[key].position, 478 | color: bufferMap[key].position, 479 | }; 480 | } 481 | bufferMap = newBufferMap; 482 | // console.log(`to fetch this ${filteredElements}`); 483 | 484 | updateHtmlUI(nodeNotFoundInBuffer, nodeFoundInBuffer, nodeFoundInLRU, nodeFoundInPersistent, nodeToFetch) 485 | return filteredElements; 486 | //filter and delete unwanted bufferMap 487 | } 488 | 489 | let direction = [ 490 | [0, 0, 0], 491 | [0, 0, 1], 492 | [0, 1, 0], 493 | [0, 1, 1], 494 | [1, 0, 0], 495 | [1, 0, 1], 496 | [1, 1, 0], 497 | [1, 1, 1], 498 | ]; 499 | 500 | function findSiblings(keyCountMap) { 501 | let siblings = {}; 502 | let nodeKeyCount = []; 503 | for (let i = 0; i < keyCountMap.length; i += 2) { 504 | let myNode = keyCountMap[i]; 505 | myNode = myNode.split("-").map(Number); 506 | if (myNode.every((c) => c == 0)) { 507 | continue; 508 | } 509 | let myParent = []; 510 | myParent[0] = myNode[0] - 1; 511 | for (let i = 1; i <= 3; i++) { 512 | myParent[i] = Math.floor(myNode[i] / 2); 513 | } 514 | 515 | // for (let k = 0; k < 8; k++) { 516 | // let sibling = [ 517 | // myNode[0], 518 | // 2 * myParent[1] + direction[k][0], 519 | // 2 * myParent[2] + direction[k][1], 520 | // 2 * myParent[3] + direction[k][2], 521 | // ]; 522 | // sibling = sibling.join("-"); 523 | // if (sibling in siblings || sibling in bufferMap) { 524 | // continue; 525 | // } else if (sibling in nodePages && nodePages[sibling].pointCount > 0) { 526 | // nodeKeyCount.push(sibling, nodePages[sibling].pointCount); 527 | // siblings[sibling] = true; 528 | // } 529 | // } 530 | } 531 | return nodeKeyCount; 532 | } 533 | 534 | async function retrivePoints(projectionViewMatrix, controllerSignal = null) { 535 | const startTime4 = performance.now(); 536 | let [keyCountMap, nodeToPrefetch] = traverseTreeWrapper( 537 | nodePages, 538 | [0, 0, 0, 0], 539 | center_x, 540 | center_y, 541 | center_z, 542 | [0.5 * widthx, 0.5 * widthy, 0.5 * widthz], 543 | scaleFactor, 544 | controls, 545 | projectionViewMatrix 546 | ); 547 | const endTime4 = performance.now(); 548 | // console.log(`Time taken to traverse tree: ${endTime4 - startTime4}ms`); 549 | 550 | keyCountMap = await filterkeyCountMap(keyCountMap); 551 | prefetch_keyCountMap = await filterkeyCountMap_Prefetch(nodeToPrefetch); 552 | 553 | clock.getDelta(); 554 | let totalNodes = keyCountMap.length / 2; 555 | let doneCount = 0; 556 | 557 | for (let m = 0; m < keyCountMap.length; ) { 558 | let remaining = totalNodes - doneCount; 559 | let numbWorker = Math.min(MAX_WORKERS, remaining); 560 | for (let i = 0; i < numbWorker; i++) { 561 | // console.log("i am entering first time"); 562 | promises.push(createWorker(keyCountMap[m], keyCountMap[m + 1])); 563 | doneCount++; 564 | m += 2; 565 | if (doneCount % MAX_WORKERS == 0 || doneCount == totalNodes) { 566 | await syncThread(); 567 | // console.log(controllerSignal) 568 | if (controllerSignal && controllerSignal.aborted) { 569 | // console.log("i am aborted now from fetching thread"); 570 | return; 571 | } 572 | } 573 | } 574 | } 575 | 576 | // ------------------------------------------------------------------ 577 | totalNodes = prefetch_keyCountMap.length / 2; 578 | doneCount = 0; 579 | promises = []; 580 | 581 | // for (let m = 0; m < prefetch_keyCountMap.length; ) { 582 | // let remaining = totalNodes - doneCount; 583 | // let numbWorker = Math.min(MAX_WORKERS, remaining); 584 | // for (let i = 0; i < numbWorker; i++) { 585 | // // console.log("i am entering first time"); 586 | // promises.push( 587 | // createWorker(prefetch_keyCountMap[m], prefetch_keyCountMap[m + 1]) 588 | // ); 589 | // doneCount++; 590 | // m += 2; 591 | // if (doneCount % MAX_WORKERS == 0 || doneCount == totalNodes) { 592 | // await syncThread_Prefetch(); 593 | // if (controllerSignal && controllerSignal.aborted) { 594 | // console.log("i am aborted now from prefetcher"); 595 | // return; 596 | // } 597 | // } 598 | // } 599 | // } 600 | 601 | // console.log("it finished at", clock.getDelta()); 602 | 603 | // find sibling 604 | // let siblings = findSiblings(keyCountMap); 605 | // console.log(siblings); 606 | } 607 | 608 | async function createCameraProj() { 609 | camera = new THREE.PerspectiveCamera( 610 | 50, 611 | canvas.width / canvas.height, 612 | 0.1, 613 | 4000 614 | ); 615 | camera.up.set(0, 0, 1 ); 616 | camera.position.set(0, 1000, 1000) 617 | // camera.position.set( 1446.6944661802368, -1721.55051389683 , 444.2788); 618 | // camera.rotation.set(0.6403667747403355, 0.34, 0.42); 619 | camera.updateProjectionMatrix(); 620 | 621 | controls = new OrbitControls(camera, canvas); 622 | // controls.target.set(900, -665, -710); 623 | 624 | controls.enableDamping = true; 625 | controls.dampingFactor = 0.5; 626 | // controls.minAzimuthAngle = 0; 627 | // controls.maxAzimuthAngle = 0.25 * Math.PI; 628 | // controls.minPolarAngle = 0; 629 | // controls.maxPolarAngle = Math.PI; 630 | // controls.rotateSpeed = 2; 631 | controls.zoomSpeed = 1; 632 | controls.panSpeed = 2; 633 | // controls.autoRotate = true; 634 | // controls.autoRotateSpeed = 0.5; 635 | controls.update(); 636 | 637 | // camera = new ArcballCamera([100, -100, 100], [0, 0, 0], [0, 1, 0], 300, [ 638 | // window.innerWidth, 639 | // window.innerHeight, 640 | // ]); 641 | 642 | proj = mat4.perspective( 643 | mat4.create(), 644 | (50 * Math.PI) / 180.0, 645 | canvas.width / canvas.height, 646 | 0.1, 647 | 8000 648 | ); 649 | } 650 | 651 | async function loadCOPC() { 652 | clock.getDelta(); 653 | // https://viewer.copc.io/?copc=https://s3.amazonaws.com/data.entwine.io/millsite.copc.laz 654 | // https://github.com/PDAL/data/blob/master/autzen/autzen-classified.copc.laz 655 | // let filename = "https://s3.amazonaws.com/data.entwine.io/millsite.copc.laz"; 656 | const filename = process.env.filename; 657 | const copc = await Copc.create(filename); 658 | console.log("file is", copc) 659 | scaleFactor = copc.header.scale; 660 | scaleFactor = [1.0, 1.0, 1.0] 661 | copcString = JSON.stringify(copc); 662 | // scale = copc.header.scale[0]; 663 | [x_min, y_min, z_min, x_max, y_max, z_max] = [...copc.header.min, ...copc.header.max]; 664 | x_min *= scaleFactor[0]; 665 | x_max *= scaleFactor[0]; 666 | y_min *= scaleFactor[1]; 667 | y_max *= scaleFactor[1]; 668 | z_min *= scaleFactor[2]; 669 | z_max *= scaleFactor[2]; 670 | widthx = Math.abs(x_max - x_min); 671 | widthy = Math.abs(y_max - y_min); 672 | widthz = Math.abs(z_max - z_min); 673 | // console.log(widthx, widthy, widthz); 674 | // console.log(z_max, z_min, widthz); 675 | // for new COPC file widthz is 50, z_min is fine but width is wrong 676 | 677 | // console.log("minimum z is", z_min, "z-width is", widthz, copc); 678 | 679 | params = [widthx, widthy, widthz, x_min, y_min, z_min]; 680 | center_x = ((x_min + x_max) / 2 - x_min - 0.5 * widthx) ; 681 | center_y = ((y_min + y_max) / 2 - y_min - 0.5 * widthy); 682 | center_z = ((z_min + z_max) / 2 - z_min - 0.5 * widthz); 683 | // center is moved to origin and we dont need to do this but for sake for checking the cordinate system, i am doing this 684 | const { nodes: nodePages1, pages: pages } = await Copc.loadHierarchyPage( 685 | filename, 686 | copc.info.rootHierarchyPage 687 | ); 688 | nodePages = nodePages1; 689 | nodePagesString = JSON.stringify(nodePages); 690 | pagesString = JSON.stringify(pages); 691 | } 692 | 693 | (async () => { 694 | // await clear(); 695 | // const start6 = performance.now(); 696 | await create_P_Meta_Cache(); 697 | // const end6 = performance.now(); 698 | // console.log(`Time taken to create meta cache: ${end6 - start6}ms`); 699 | 700 | // const start7 = performance.now(); 701 | pers_cache = await p_cache(); 702 | // const end7 = performance.now(); 703 | // console.log(`Time taken to make LRU meta cache: ${end7 - start7}ms`); 704 | // console.log("cache created successfully"); 705 | 706 | // const start8 = performance.now(); 707 | await createCameraProj(); 708 | // const end8 = performance.now(); 709 | // console.log(`Time taken to create camera: ${end8 - start8}ms`); 710 | // console.log("file reading start"); 711 | 712 | // const start9 = performance.now(); 713 | await loadCOPC(); 714 | // const end9 = performance.now(); 715 | // console.log(`Time taken to load COPC file: ${end9 - start9}ms`); 716 | // console.log("initialize the uniform buffers"); 717 | 718 | // const start10 = performance.now(); 719 | let projViewMatrix = await stages(camera, proj, params); 720 | // const end10 = performance.now(); 721 | // console.log(`Time taken to do initial rendering setup: ${end10 - start10}ms`); 722 | // console.log("data loading start"); 723 | 724 | // const start11 = performance.now(); 725 | await retrivePoints(projViewMatrix); 726 | // const end11 = performance.now(); 727 | // console.log( 728 | // `Time taken to retrive needed nodes from tree: ${end11 - start11}ms` 729 | // ); 730 | // console.log("data loaded"); 731 | 732 | // const start12 = performance.now(); 733 | await renderWrapper(); 734 | // const end12 = performance.now(); 735 | // console.log(`Time taken to render: ${end12 - start12}ms`); 736 | // console.log("render done"); 737 | })(); 738 | 739 | // ------------------------------------------------------------------------------------ 740 | // -------------make iternary of camera ---------------------------------------------- 741 | 742 | // render by WebGPU 743 | // console.log(colors); 744 | // renderStages(positions, colors); 745 | // ---------------------------------------------------------------------------- 746 | // geometry.setAttribute( 747 | // "position", 748 | // new THREE.Float32BufferAttribute(positions, 3) 749 | // ); 750 | // geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); 751 | // var material = new THREE.PointsMaterial({ size: 2, vertexColors: true }); 752 | // let p = new THREE.Points(geometry, material); 753 | // scene.add(p); 754 | // ----------------------------------------------------------------------------- 755 | 756 | export { 757 | scene_width, 758 | scene_height, 759 | scene_depth, 760 | loadCOPC, 761 | bufferMap, 762 | retrivePoints, 763 | toDeleteMap, 764 | wait, 765 | controls, 766 | global_max_intensity, 767 | }; 768 | --------------------------------------------------------------------------------