# Project export: EcoDrone

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: We use drones to construct 3D Gaussian Splat models, automating the ecological risk assessment process.
- Devpost: https://devpost.com/software/ecodrone
- GitHub: https://github.com/balbit/TreeHacks2024
- Video: https://www.youtube.com/embed/d6nTWWohsdg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Alex Hu (9 commits), James Liu (2 commits), balbit (1 commits), hdeep03 (1 commits)

## Devpost submission (written by the team)

### Inspiration

Human infrastructure like power lines or oil rigs are immensely important for all of our daily lives, and if anything fails the consequences are catastrophic. For example, in 2010, the infamous Deepwater Horizon oil spill (caused by a poorly maintained oil rig) sent 210 million gallons of oil into the Gulf of Mexico, causing staggering images of oil-stained beaches in several States and devastating local wildlife even to this day. However, these most critical and high-risk pieces of infrastructure are often also most difficult to reach. Located in remote places like submersible oil platforms or vast in scope like power grids, these challenges make automating the inspection process even more useful. Given the growth in drone and AI technology, we saw an opportunity to innovate and solve this problem.

### What it does

First, we fly the drones (thanks Parrot!) near the target location and capture footage. By applying the COLMAP algorithm to consecutive images to derive their camera poses, and then training a Gaussian splat representation of the to-be-inspected object, we are able to render novel views from every direction. This saves valuable air time in that drones no longer have to spend as much time inspecting each building as our reconstructions capture the outward appearance by extrapolating information. It also allows human reviewers to quickly inspect potentially faulty areas without having to review the entire drone footage. Next, we apply multi-modal Large Language models to analyze shots sampled from all directions. To our knowledge, we are the first to leverage Intel's XPU technology to accelerate multi-modal LLM inference. These multi-modal LLMs can quickly sift through large amounts of data to identify the problematic regions, which we are able to summarize into a condensed report. Finally, to enhance the quality of inspection, we experiment with agentic multi-modal LLMs that explore our Gaussian splat simulations to search for vulnerabilities.

### How we built it

We simulated the real environment through model creation in Blender. We hacked together cutting-edge research repositories for our project. The Intel developer cloud was useful in developing our methods.

### Challenges we ran into

Since the Intel platform for AI is not as established, we learned a lot by trying to port existing research repositories with CUDA kernels over. We got nerd-sniped by "Data Parallel C++: the oneAPI Implementation of SYCL" documentation.

### Accomplishments we're proud of

We are proud of building such high-quality Gaussian splats based upon real data from our simulations! Make sure to come to our booth to check out our fully interactive splats!

### What we learned

We learned how to create Gaussian splats, use Blender, do web development, and utilize Intel's AI GPU offerings.

### What's next

We're optimistic that introducing more specialized CV algorithms will yield enormous alpha for this important industry of ensuring our infrastructure remains working and eco-friendly. Our Demo Video (should be linked) A quick demo for our website (which shows the gaussian-splatted rendering of an oil rig, trained from our simulation generation!) We can talk to a Together.ai-powered Mistral-based fast inference chatbot about various key frames extracted from the drone footage to search for potential hazards! The Vision LLM used for frame captioning (LLaVA) is powered by Intel!

## README (from the GitHub repository)

# TreeHacks2024

We're making a fully autonomous, AI-powered drone system to detect power line anomalies, plan inspection trajectories, and craft intricate reports with full 3D modeling.

frontend run:

```
cd website
npm run dev
```


## Detected evidence (automated analysis)

Indexed codebase: 11 recognized source files, 62 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (19 of 19)

```
.DS_Store
convert.py
docs/.eslintrc.cjs
docs/.gitignore
docs/index.html
docs/package.json
docs/public/maings.js
docs/public/oil_rig.splat
docs/README.md
docs/src/App.css
docs/src/App.tsx
docs/src/index.css
docs/src/main.tsx
docs/src/vite-env.d.ts
docs/tsconfig.json
docs/tsconfig.node.json
docs/vite.config.ts
LICENSE
README.md
```

### Dependencies

- docs/package.json: @microsoft/fetch-event-source@^2.0.1, @types/react@^18.2.55, @types/react-dom@^18.2.19, @typescript-eslint/eslint-plugin@^6.21.0, @typescript-eslint/parser@^6.21.0, @vitejs/plugin-react@^4.2.1, eslint@^8.56.0, eslint-plugin-react-hooks@^4.6.0, eslint-plugin-react-refresh@^0.4.5, react@^18.2.0, react-dom@^18.2.0, typescript@^5.2.2, vite@^5.1.0

### Recent commits (newest first)

- .
- Removed glare from splat, rotated splat initial view, toggled rot direction
- add oil_rig
- manual merge :D
- Merge branch 'main' of https://github.com/balbit/TreeHacks2024
- beautiful chat
- FIXED
- Revert "merge"
- Revert "added title"
- merge
- added title
- Merge pull request #1 from balbit/streaming
- Merge branch 'main' into streaming
- adsf
- Added streaming
- beautify chat
- asdf
- chat bugix
- chat
- changed website

## Key source files (fetched from GitHub, selected and truncated for size)

### docs/package.json

```
{
  "name": "drone-3d",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@microsoft/fetch-event-source": "^2.0.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.55",
    "@types/react-dom": "^18.2.19",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.56.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.1.0"
  }
}

```

### docs/src/main.tsx

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

```

### docs/src/App.tsx

```typescript
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

```

### convert.py

```python
# You can use this to convert a .ply file to a .splat file programmatically in python
# Alternatively you can drag and drop a .ply file into the viewer at https://antimatter15.com/splat

from plyfile import PlyData
import numpy as np
import argparse
from io import BytesIO


def process_ply_to_splat(ply_file_path):
    plydata = PlyData.read(ply_file_path)
    vert = plydata["vertex"]
    sorted_indices = np.argsort(
        -np.exp(vert["scale_0"] + vert["scale_1"] + vert["scale_2"])
        / (1 + np.exp(-vert["opacity"]))
    )
    buffer = BytesIO()
    for idx in sorted_indices:
        v = plydata["vertex"][idx]
        position = np.array([v["x"], v["y"], v["z"]], dtype=np.float32)
        scales = np.exp(
            np.array(
                [v["scale_0"], v["scale_1"], v["scale_2"]],
                dtype=np.float32,
            )
        )
        rot = np.array(
            [v["rot_0"], v["rot_1"], v["rot_2"], v["rot_3"]],
            dtype=np.float32,
        )
        SH_C0 = 0.28209479177387814
        color = np.array(
            [
                0.5 + SH_C0 * v["f_dc_0"],
                0.5 + SH_C0 * v["f_dc_1"],
                0.5 + SH_C0 * v["f_dc_2"],
                1 / (1 + np.exp(-v["opacity"])),
            ]
        )
        buffer.write(position.tobytes())
        buffer.write(scales.tobytes())
        buffer.write((color * 255).clip(0, 255).astype(np.uint8).tobytes())
        buffer.write(
            ((rot / np.linalg.norm(rot)) * 128 + 128)
            .clip(0, 255)
            .astype(np.uint8)
            .tobytes()
        )

    return buffer.getvalue()


def save_splat_file(splat_data, output_path):
    with open(output_path, "wb") as f:
        f.write(splat_data)


def main():
    parser = argparse.ArgumentParser(description="Convert PLY files to SPLAT format.")
    parser.add_argument(
        "input_files", nargs="+", help="The input PLY files to process."
    )
    parser.add_argument(
        "--output", "-o", default="output.splat", help="The output SPLAT file."
    )
    args = parser.parse_args()
    for input_file in args.input_files:
        print(f"Processing {input_file}...")
        splat_data = process_ply_to_splat(input_file)
        output_file = (
            args.output if len(args.input_files) == 1 else input_file + ".splat"
        )
        save_splat_file(splat_data, output_file)
        print(f"Saved {output_file}")


if __name__ == "__main__":
    main()

```

### docs/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### docs/index.html

```html
<!DOCTYPE html>
<html lang="en" dir="ltr">
	<head>
		<title>EcoDrone Home Page</title>
		<meta charset="utf-8" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"
		/>
		<meta name="apple-mobile-web-app-capable" content="yes" />
		<meta
			name="apple-mobile-web-app-status-bar-style"
			content="black-translucent"
		/>

		<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">

		<style>
			body {
				overflow: hidden;
				margin: 0;
				height: 100vh;
				width: 100vw;
				font-family: sans-serif;
				background: black;
    			text-shadow: 0 0 3px black;
			}
			a, body {
				color: white;
			}
			#info {
				z-index: 100;
				position: absolute;
				top: 10px;
				left: 15px;
			}
			h3 {
				margin: 5px 0;
			}
			p {
				margin: 5px 0;
				font-size: small;
			}

			.cube-wrapper {
				transform-style: preserve-3d;
			}

			.cube {
				transform-style: preserve-3d;
				transform: rotateX(45deg) rotateZ(45deg);
				animation: rotation 2s infinite;
			}

			.cube-faces {
				transform-style: preserve-3d;
				height: 80px;
				width: 80px;
				position: relative;
				transform-origin: 0 0;
				transform: translateX(0) translateY(0) translateZ(-40px);
			}

			.cube-face {
				position: absolute;
				inset: 0;
				background: #0017ff;
				border: solid 1px #ffffff;
			}
			.cube-face.top {
				transform: translateZ(80px);
			}
			.cube-face.front {
				transform-origin: 0 50%;
				transform: rotateY(-90deg);
			}
			.cube-face.back {
				transform-origin: 0 50%;
				transform: rotateY(-90deg) translateZ(-80px);
			}
			.cube-face.right {
				transform-origin: 50% 0;
				transform: rotateX(-90deg) translateY(-80px);
			}
			.cube-face.left {
				transform-origin: 50% 0;
				transform: rotateX(-90deg) translateY(-80px) translateZ(80px);
			}

			@keyframes rotation {
				0% {
					transform: rotateX(45deg) rotateY(0) rotateZ(45deg);
					animation-timing-function: cubic-bezier(
						0.17,
						0.84,
						0.44,
						1
					);
				}
				50% {
					transform: rotateX(45deg) rotateY(0) rotateZ(225deg);
					animation-timing-function: cubic-bezier(
						0.76,
						0.05,
						0.86,
						0.06
					);
				}
				100% {
					transform: rotateX(45deg) rotateY(0) rotateZ(405deg);
					animation-timing-function: cubic-bezier(
						0.17,
						0.84,
						0.44,
						1
					);
				}
			}

			.scene,
			#message {
				position: absolute;
				display: flex;
				top: 0;
				right: 0;
				left: 0;
				bottom: 0;
				z-index: 2;
				height: 100%;
				width: 100%;
				align-items: center;
				justify-content: center;
			}
			#message {
				font-weight: bold;
				font-size: large;
				color: red;
				pointer-events: none;
			}

			details {
				font-size: small;

			}

			#progress {
				position: absolute;
				top: 0;
				height: 5px;
				background: blue;
				z-index: 99;
				transition: width 0.1s ease-in-out;
			}

			#quality {
				position: absolute;
				bottom: 10px;
				z-index: 999;
				right: 10px;
			}

			#caminfo {
				position: absolute;
				top: 10px;
				z-index: 999;
				right: 10px;
			}
			#canvas {
				display: block;
				position: absolute;
				top: 0;
				left: 0;
				width: 100%;
				height: 100%;
				touch-action: none;
			}

			#instructions {
				background: rgba(0,0,0,0.6);
				white-space: pre-wrap;
				padding: 10px;
				border-radius: 10px;
				font-size: x-small;
			}
			body.nohf .nohf {
				display: none;
			}
			body.nohf #progress, body.nohf .cube-face {
				background: #ff9d0d;
			}
      

	#title-box {
		font-family: 'Poppins', sans-serif;
	}

	.bold-text {
	font-weight: 700; /* Bold */
	}

	.extra-bold-text {
	font-weight: 800; /* ExtraBold */
	}	

	#fancy-title {
		position: absolute;
		top: 15%; /* Adjust the position as needed */
		left: 50%;
		transform: translateX(-50%);
		font-size: 180px;
		font-weight: bold;
		color: #FFF; 
		text-shadow: 0 0 20px rgba(0,0,0,1);
		z-index: 10;
	}


	#fancy-title span {
	background: linear-gradient(to right, rgb(52, 180, 222), green, white, rgb(52, 180, 222));
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	display: inline; /* or 'block' depending on layout needs */
	text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.0); /* Example shadow */

	}

	#subtitle {
		position: absolute;
		top: 40%; /* Adjust the position as needed */
		left: 50%;
		transform: translateX(-50%);
		opacity: 0;
		transition: opacity 1s;
		font-size: 25px;
		font-weight: 300; 
		text-shadow: 0 0 20px rgba(0,0,0,1);
		z-index: 10;
	}

      #chatBox {
     position: fixed; /* Or absolute, depending on your requirement */
     bottom: 20px;
     right: 20px;
     width: 300px;
     height: 400px;
     background-color: rgba(0, 0, 0, 0.6);
     color: white;
     border-radius: 8px;
     display: flex;
     flex-direction: column;
     padding: 10px;
     box-sizing: border-box;
   }


   #chatHeader {
     font-weight: bold;
     margin-bottom: 10px;
   }


   #chatContent {
     flex: 1;
     overflow-y: auto;
   }


   #chatInput {
     margin-top: 10px;
     padding: 5px;
     width: calc(100% - 10px); /* Adjust based on padding */
     background-color: rgba(0, 0, 0, 0.6); /* Match the chat box's opacity and color */
     color: white; /* Match the text color of the chat box */
     border: none; /* Remove default border */
     border-radius: 4px; /* Optional: match the chat box's border-radius for consistency */
     box-sizing: border-box;
   }


   #chatInput::placeholder {
     color: rgba(255, 255, 255, 0.7); /* Lighter placeholder text for better readability */
   }


   #chatInput:focus {
     outline: none; /* Removes the outline to keep the design clean */
     border: 1px solid rgba(255, 255, 255, 0.8); /* Optional: adds a subtle border on focus for better visibility */
   }
		</style>
	<
[truncated — 5456 more characters]
```

### docs/src/vite-env.d.ts

```typescript
/// <reference types="vite/client" />

```

### docs/src/App.css

```css
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

```

### docs/src/index.css

```css
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

```

### docs/public/maings.js

```javascript
function isChatInputFocused() {
  return document.activeElement.id === "chatInput";
}

let cameras = [
  {
    id: 0,
    img_name: "00001",
    width: 1959,
    height: 1090,
    position: [-3.0089893469241797, -0.11086489695181866, -3.7527640949141428],
    rotation: [
      [0.876134201218856, 0.06925962026449776, 0.47706599800804744],
      [-0.04747421839895102, 0.9972110940209488, -0.057586739349882114],
      [-0.4797239414934443, 0.027805376500959853, 0.8769787916452908],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 1,
    img_name: "00009",
    width: 1959,
    height: 1090,
    position: [-2.5199776022057296, -0.09704735754873686, -3.6247725540304545],
    rotation: [
      [0.9982731285632193, -0.011928707708098955, -0.05751927260507243],
      [0.0065061360949636325, 0.9955928229282383, -0.09355533724430458],
      [0.058381769258182864, 0.09301955098900708, 0.9939511719154457],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 2,
    img_name: "00017",
    width: 1959,
    height: 1090,
    position: [-0.7737533667465242, -0.3364271945329695, -2.9358969417573753],
    rotation: [
      [0.9998813418672372, 0.013742375651625236, -0.0069605529394208224],
      [-0.014268370388586709, 0.996512943252834, -0.08220929105659476],
      [0.00580653013657589, 0.08229885200307129, 0.9965907801935302],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 3,
    img_name: "00025",
    width: 1959,
    height: 1090,
    position: [1.2198221749590001, -0.2196687861401182, -2.3183162007028453],
    rotation: [
      [0.9208648867765482, 0.0012010625395201253, 0.389880004297208],
      [-0.06298204172269357, 0.987319521752825, 0.14571693239364383],
      [-0.3847611242348369, -0.1587410451475895, 0.9092635249821667],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 4,
    img_name: "00033",
    width: 1959,
    height: 1090,
    position: [1.742387858893817, -0.13848225198886954, -2.0566370113193146],
    rotation: [
      [0.24669889292141334, -0.08370189346592856, -0.9654706879349405],
      [0.11343747891376445, 0.9919082664242816, -0.05700815184573074],
      [0.9624300466054861, -0.09545671285663988, 0.2541976029815521],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 5,
    img_name: "00041",
    width: 1959,
    height: 1090,
    position: [3.6567309419223935, -0.16470990600750707, -1.3458085590422042],
    rotation: [
      [0.2341293058324528, -0.02968330457755884, -0.9717522161434825],
      [0.10270823606832301, 0.99469554638321, -0.005638106875665722],
      [0.9667649592295676, -0.09848690996657204, 0.2359360976431732],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 6,
    img_name: "00049",
    width: 1959,
    height: 1090,
    position: [3.9013554243203497, -0.2597500978038105, -0.8106154188297828],
    rotation: [
      [0.6717235545638952, -0.015718162115524837, -0.7406351366386528],
      [0.055627354673906296, 0.9980224478387622, 0.029270992841185218],
      [0.7387104058127439, -0.060861588786650656, 0.6712695459756353],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 7,
    img_name: "00057",
    width: 1959,
    height: 1090,
    position: [4.742994605467533, -0.05591660945412069, 0.9500365976084458],
    rotation: [
      [-0.17042655709210375, 0.01207080756938, -0.9852964448542146],
      [0.1165090336695526, 0.9931575292530063, -0.00798543433078162],
      [0.9784581921120181, -0.1161568667478904, -0.1706667764862097],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 8,
    img_name: "00065",
    width: 1959,
    height: 1090,
    position: [4.34676307626522, 0.08168160516967145, 1.0876221470355405],
    rotation: [
      [-0.003575447631888379, -0.044792503246552894, -0.9989899137764799],
      [0.10770152645126597, 0.9931680875192705, -0.04491693593046672],
      [0.9941768441149182, -0.10775333677534978, 0.0012732004866391048],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
  {
    id: 9,
    img_name: "00073",
    width: 1959,
    height: 1090,
    position: [3.264984351114202, 0.078974937336732, 1.0117200284114904],
    rotation: [
      [-0.026919994628162257, -0.1565891128261527, -0.9872968974090509],
      [0.08444552208239385, 0.983768234577625, -0.1583319754069128],
      [0.9960643893290491, -0.0876350978794554, -0.013259786205163005],
    ],
    fy: 1164.6601287484507,
    fx: 1159.5880733038064,
  },
];

let camera = cameras[0];

function getProjectionMatrix(fx, fy, width, height) {
  const znear = 0.2;
  const zfar = 200;
  return [
    [(2 * fx) / width, 0, 0, 0],
    [0, -(2 * fy) / height, 0, 0],
    [0, 0, zfar / (zfar - znear), 1],
    [0, 0, -(zfar * znear) / (zfar - znear), 0],
  ].flat();
}

function getViewMatrix(camera) {
  const R = camera.rotation.flat();
  const t = camera.position;
  const camToWorld = [
    [R[0], R[1], R[2], 0],
    [R[3], R[4], R[5], 0],
    [R[6], R[7], R[8], 0],
    [
      -t[0] * R[0] - t[1] * R[3] - t[2] * R[6],
      -t[0] * R[1] - t[1] * R[4] - t[2] * R[7],
      -t[0] * R[2] - t[1] * R[5] - t[2] * R[8],
      1,
    ],
  ].flat();
  return camToWorld;
}
// function translate4(a, x, y, z) {
//     return [
//         ...a.slice(0, 12),
//         a[0] * x + a[4] * y + a[8] * z + a[12],
//         a[1] * x + a[5] * y + a[9] * z + a[13],
//         a[2] * x + a[6] * y + a[10] * z + a[14],
//         a[3] * x + a[7] * y + a[11] * z + a[15],
//     ];
// }

function multiply4(a, b) {
  return [
    b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12],
    b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13],
    b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14],
    b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15],
    b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12],
    b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13]
[truncated — 39402 more characters]
```