├── src ├── index.css ├── vite-env.d.ts ├── assets │ └── fonts │ │ ├── Rubik-VariableFont_wght.ttf │ │ └── OFL.txt ├── lib │ ├── interfaces │ │ └── IAnimatedElement.ts │ ├── utils │ │ └── Pointer.ts │ ├── elements │ │ ├── Palettes.ts │ │ ├── IsolinesMaterial.ts │ │ └── IsolinesMeshing.ts │ ├── Root.ts │ └── nodes │ │ └── DistanceFunctions.ts ├── main.tsx ├── App.css ├── components │ ├── Slider.tsx │ ├── Collapsable.tsx │ ├── LargeCollapsable.tsx │ ├── threeroot.tsx │ ├── IntroModal.tsx │ ├── InfoModal.tsx │ └── GFX.tsx └── App.tsx ├── .gitattributes ├── postcss.config.js ├── readme └── isolines-github-header.jpg ├── public ├── assets │ ├── ultrahdr │ │ └── table_mountain_2_puresky_2k.jpg │ ├── googlefonts.svg │ ├── three.svg │ ├── tailwind.svg │ ├── typescript.svg │ ├── vite.svg │ ├── bootstrap.svg │ ├── react.svg │ └── gsap.svg └── icon.svg ├── tsconfig.node.json ├── .gitignore ├── .eslintrc.cjs ├── tsconfig.json ├── vite.config.ts ├── index.html ├── LICENSE ├── package.json ├── tailwind.config.js └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /readme/isolines-github-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ULuIQ12/webgpu-isoline-geometry/HEAD/readme/isolines-github-header.jpg -------------------------------------------------------------------------------- /src/assets/fonts/Rubik-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ULuIQ12/webgpu-isoline-geometry/HEAD/src/assets/fonts/Rubik-VariableFont_wght.ttf -------------------------------------------------------------------------------- /public/assets/ultrahdr/table_mountain_2_puresky_2k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ULuIQ12/webgpu-isoline-geometry/HEAD/public/assets/ultrahdr/table_mountain_2_puresky_2k.jpg -------------------------------------------------------------------------------- /src/lib/interfaces/IAnimatedElement.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Small interface for animated elements 3 | */ 4 | export interface IAnimatedElement { 5 | update(dt: number, elapsed: number): void; 6 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | import ThreeRoot from './components/threeroot.tsx' 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /public/assets/googlefonts.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Rubik"; 7 | src: url("./assets/fonts/Rubik-VariableFont_wght.ttf"); 8 | } 9 | 10 | #root { 11 | @apply w-full min-h-screen 12 | } 13 | 14 | #threecanvas { 15 | @apply absolute top-0 left-0 w-full h-full z-0 16 | } 17 | 18 | #app { 19 | @apply flex flex-col top-0 left-0 p-4 w-fit font-rubik z-10 20 | } 21 | 22 | .uiButton { 23 | @apply flex w-full rounded bg-blue-500 text-white text-sm px-2 py-1 uppercase justify-center 24 | } 25 | 26 | -------------------------------------------------------------------------------- /public/assets/three.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/assets/tailwind.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noImplicitAny": false, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": false, 23 | "noFallthroughCasesInSwitch": false, 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, PluginOption } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | const fullReloadAlways: PluginOption = { 5 | name: 'full-reload-always', 6 | handleHotUpdate({ server }) { 7 | server.ws.send({ type: "full-reload" }) 8 | return [] 9 | }, 10 | } as PluginOption 11 | 12 | // https://vitejs.dev/config/ 13 | export default defineConfig({ 14 | base: 'https://ulucode.com/random/webgputests/isolines/', 15 | plugins: [ 16 | react(), 17 | fullReloadAlways 18 | ], 19 | build: { 20 | target: 'esnext' //browsers can handle the latest ES features 21 | }, 22 | esbuild: { 23 | supported: { 24 | 'top-level-await': true //browsers can handle top-level-await features 25 | }, 26 | }, 27 | optimizeDeps: { 28 | exclude: ['three'], 29 | esbuildOptions: { 30 | target: 'esnext' 31 | } 32 | }, 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Isolines compute geometry 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/assets/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Slider.tsx: -------------------------------------------------------------------------------- 1 | interface ISliderProps { 2 | label:string, 3 | tooltip?:string, 4 | min:number, 5 | max:number, 6 | step:number, 7 | value:number, 8 | isInt?:boolean, 9 | paramName:string, 10 | onChange:(event, paramName:string)=>void, 11 | } 12 | 13 | export default function Slider({ 14 | label, 15 | tooltip, 16 | min, 17 | max, 18 | step, 19 | value, 20 | isInt, 21 | paramName, 22 | onChange, 23 | }: ISliderProps) { 24 | 25 | return ( 26 |
27 |
28 |
{label}
29 | onChange(event, paramName)} 32 | /> 33 |
{isInt?value:value.toFixed(2)}
34 |
35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Christophe Choffel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgpu-tests-3", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@webvoxel/fast-simplex-noise": "^0.0.1-a2", 14 | "gsap": "^3.12.5", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "three": "^0.167.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.14.9", 21 | "@types/react": "^18.2.66", 22 | "@types/react-dom": "^18.2.22", 23 | "@types/three": "^0.167.0", 24 | "@typescript-eslint/eslint-plugin": "^7.2.0", 25 | "@typescript-eslint/parser": "^7.2.0", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "autoprefixer": "^10.4.19", 28 | "daisyui": "^4.12.10", 29 | "eslint": "^8.57.0", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.4.6", 32 | "postcss": "^8.4.38", 33 | "tailwindcss": "^3.4.4", 34 | "typescript": "^5.2.2", 35 | "vite": "^5.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Collapsable.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | 3 | interface ICollapsableProps { 4 | title:string, 5 | children:ReactNode, 6 | onOpenCloseChange?:()=>void, 7 | } 8 | 9 | export default function Collapsable({ 10 | title, 11 | children, 12 | onOpenCloseChange, 13 | }: ICollapsableProps) { 14 | 15 | const [isOpen, setIsOpen] = useState(true); 16 | const handleOpenCloseChange = () => { 17 | setIsOpen(!isOpen); 18 | if (onOpenCloseChange) { 19 | onOpenCloseChange(); 20 | } 21 | } 22 | 23 | return ( 24 |
25 | 26 |
{title}
27 |
28 |
29 | {children} 30 |
31 |
32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /src/components/LargeCollapsable.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | 3 | interface ILargeCollapsableProps { 4 | title:string, 5 | children:ReactNode, 6 | accordionName?:string, 7 | onOpenCloseChange?:()=>void, 8 | } 9 | 10 | export default function LargeCollapsable({ 11 | title, 12 | children, 13 | accordionName, 14 | onOpenCloseChange, 15 | }: ILargeCollapsableProps) { 16 | 17 | const [isOpen, setIsOpen] = useState(false); 18 | const handleOpenCloseChange = () => { 19 | //setIsOpen(!isOpen); 20 | if (onOpenCloseChange) { 21 | onOpenCloseChange(); 22 | } 23 | } 24 | 25 | return ( 26 |
27 | 28 |
{title}
29 |
30 |
31 | {children} 32 |
33 |
34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /src/components/threeroot.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { Root } from "../lib/Root"; 3 | 4 | export default function ThreeRoot() { 5 | const canvasRef = useRef(null); 6 | 7 | useEffect(() => { 8 | const canvas = canvasRef.current; 9 | if (canvas == null) { 10 | throw new Error('Canvas not found'); 11 | } 12 | const scene: Root = new Root(canvas); 13 | 14 | (async () => { 15 | await scene.init(); 16 | })(); 17 | 18 | }, []); 19 | 20 | useEffect(() => { 21 | const canvas: HTMLCanvasElement | null = canvasRef.current; 22 | 23 | const resizeCanvas = () => { 24 | if (canvas == null) throw new Error('Canvas not found'); 25 | 26 | canvas.width = window.innerWidth; 27 | canvas.height = window.innerHeight; 28 | canvas.style.width = window.innerWidth + 'px'; 29 | canvas.style.height = window.innerHeight + 'px'; 30 | } 31 | window.addEventListener('resize', resizeCanvas); 32 | 33 | resizeCanvas(); 34 | 35 | return () => { 36 | window.removeEventListener('resize', resizeCanvas); 37 | } 38 | }, []); 39 | 40 | return ( 41 | 42 | ); 43 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import daisyui from 'daisyui' 3 | 4 | export default { 5 | content: [ 6 | "./index.html", 7 | "./src/**/*.{js,ts,jsx,tsx}", 8 | "!./lib/**/*" 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily :{ 13 | rubik: ["Rubik","sans-serif"], 14 | }, 15 | dropShadow : { 16 | 'outline': ['2px 0 rgba(0,0,0,.5)', '0 2px rgba(0,0,0,.5)', '-2px 0 rgba(0,0,0,.5)', '0 -2px rgba(0,0,0,.5)'], 17 | } 18 | }, 19 | }, 20 | plugins: [ 21 | daisyui, 22 | ], 23 | daisyui: { 24 | themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] 25 | darkTheme: "dark", // name of one of the included themes for dark mode 26 | base: true, // applies background color and foreground color for root element by default 27 | styled: true, // include daisyUI colors and design decisions for all components 28 | utils: true, // adds responsive and modifier utility classes 29 | prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors) 30 | logs: false, // Shows info about daisyUI version and used config in the console when building your CSS 31 | themeRoot: ":root", // The element that receives theme color CSS variables 32 | }, 33 | } 34 | 35 | -------------------------------------------------------------------------------- /public/assets/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/bootstrap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/utils/Pointer.ts: -------------------------------------------------------------------------------- 1 | import { Camera, Plane, Raycaster, Vector2, Vector3, WebGPURenderer, uniform } from "three/webgpu"; 2 | 3 | export class Pointer { 4 | 5 | camera:Camera; 6 | renderer:WebGPURenderer; 7 | rayCaster: Raycaster = new Raycaster(); 8 | iPlane: Plane = new Plane(new Vector3(0, 0, 1)); 9 | pointer: Vector2 = new Vector2(); 10 | scenePointer: Vector3 = new Vector3(); 11 | pointerDown: boolean = false; 12 | uPointerDown = uniform(0); 13 | uPointer = uniform(new Vector3()); 14 | 15 | constructor(renderer:WebGPURenderer, camera: Camera) { 16 | 17 | this.camera = camera; 18 | this.renderer = renderer; 19 | 20 | renderer.domElement.addEventListener("pointerdown", this.onPointerDown.bind(this)); 21 | renderer.domElement.addEventListener("pointerup", this.onPointerUp.bind(this)); 22 | window.addEventListener("pointermove", this.onPointerMove.bind(this)); 23 | } 24 | 25 | onPointerDown(e: PointerEvent): void { 26 | if (e.pointerType !== 'mouse' || e.button === 0) { 27 | this.pointerDown = true; 28 | this.uPointerDown.value = 1; 29 | } 30 | this.updateScreenPointer(e); 31 | } 32 | onPointerUp(e: PointerEvent): void { 33 | this.updateScreenPointer(e); 34 | this.pointerDown = false; 35 | this.uPointerDown.value = 0; 36 | 37 | } 38 | onPointerMove(e: PointerEvent): void { 39 | this.updateScreenPointer(e); 40 | } 41 | 42 | updateScreenPointer(e: PointerEvent): void { 43 | this.pointer.set( 44 | (e.clientX / window.innerWidth) * 2 - 1, 45 | - (e.clientY / window.innerHeight) * 2 + 1 46 | ); 47 | this.rayCaster.setFromCamera(this.pointer, this.camera); 48 | this.rayCaster.ray.intersectPlane(this.iPlane, this.scenePointer); 49 | this.uPointer.value.x = this.scenePointer.x; 50 | this.uPointer.value.y = this.scenePointer.y; 51 | this.uPointer.value.z = this.scenePointer.z; 52 | //console.log( this.scenePointer ); 53 | } 54 | } -------------------------------------------------------------------------------- /src/lib/elements/Palettes.ts: -------------------------------------------------------------------------------- 1 | import { Color, uniforms } from "three/webgpu"; 2 | 3 | export class Palettes { 4 | 5 | static defaultColors = uniforms([ 6 | new Color(0xff1f70), 7 | new Color(0x3286ff), 8 | new Color(0xffba03), 9 | new Color(0xff6202), 10 | new Color(0x874af3), 11 | new Color(0x14b2a1), 12 | new Color(0x3a5098), 13 | new Color(0xf53325), 14 | ]); 15 | 16 | static earthyColors = uniforms([ 17 | new Color(0xf2cb7c), 18 | new Color(0xc5de42), 19 | new Color(0xa3c83c), 20 | new Color(0xce9639), 21 | new Color(0xfdce62), 22 | new Color(0xdbab3f), 23 | new Color(0xe57627), 24 | new Color(0xdb924d), 25 | ]); 26 | 27 | static rainbowColors = uniforms([ 28 | new Color(0x448aff), 29 | new Color(0x1565c0), 30 | new Color(0x009688), 31 | new Color(0x8bc34a), 32 | new Color(0xffc107), 33 | new Color(0xff9800), 34 | new Color(0xf44336), 35 | new Color(0xad1457) 36 | ]); 37 | 38 | static shibuyaColor = uniforms([ 39 | new Color(0x1d1d1b), 40 | new Color(0xffd200), 41 | new Color(0xd2b0a3), 42 | new Color(0xe51f23), 43 | new Color(0xe6007b), 44 | new Color(0x005aa7), 45 | new Color(0x5ec5ee), 46 | new Color(0xf9f0de), 47 | ]); 48 | 49 | static tuttiColors = uniforms([ 50 | new Color(0xd7312e), 51 | new Color(0xf9f0de), 52 | new Color(0xf0ac00), 53 | new Color(0x0c7e45), 54 | new Color(0x2c52a0), 55 | new Color(0xf7bab6), 56 | new Color(0x5ec5ee), 57 | new Color(0xece3d0), 58 | ]); 59 | 60 | static blackWhite = uniforms([ 61 | new Color(0x000000), 62 | new Color(0xffffff), 63 | ]); 64 | 65 | static mondrian = uniforms([ 66 | new Color(0x000000), 67 | new Color(0xff0000), 68 | new Color(0x0000ff), 69 | new Color(0xffd800), 70 | new Color(0xffffff), 71 | ]); 72 | 73 | 74 | static get pinkBlueMirror() { 75 | const colors = []; 76 | const nb: number = 16; 77 | const pink = new Color(0xff0066); 78 | const blue = new Color(0x00b7fb); 79 | const temp = new Color(); 80 | for (let i: number = 0; i < nb; i++) { 81 | const r: number = i / (nb - 1); 82 | if (r < 0.5) { 83 | temp.lerpColors(pink, blue, r * 2); 84 | } else { 85 | temp.lerpColors(blue, pink, (r - 0.5) * 2); 86 | } 87 | colors.push(temp.clone()); 88 | } 89 | return uniforms(colors); 90 | } 91 | 92 | static getGrayGradient(samples: number) { 93 | const colors = []; 94 | const s: number = samples + 3; 95 | for (let i: number = 0; i < s; i++) { 96 | //colors.push( new Color().setHSL(0, 0, Math.pow( i/samples, 0.75)*2) ); 97 | colors.push(new Color().setHSL(0, 0, i / (s - 1))); 98 | } 99 | return uniforms(colors); 100 | 101 | } 102 | } -------------------------------------------------------------------------------- /src/components/IntroModal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import WebGPU from "three/examples/jsm/capabilities/WebGPU.js"; 3 | import { questionIcon, tools } from "./GFX"; 4 | 5 | interface IIntroModalProps { 6 | onClose?: () => void; 7 | } 8 | 9 | export default function IntroModal({ 10 | onClose, 11 | }: IIntroModalProps) { 12 | 13 | const [isOpen, setIsOpen] = useState(true); 14 | const handleOpenCloseChange = () => { 15 | setIsOpen(!isOpen); 16 | if (onClose) { 17 | onClose(); 18 | } 19 | } 20 | 21 | const [hasWebGPU, setHasWebGPU] = useState(false); 22 | useEffect(() => { 23 | setHasWebGPU(WebGPU.isAvailable()) 24 | }, []); 25 | 26 | return ( 27 | <> 28 | 29 |
30 |
Hello!
31 | { hasWebGPU && 32 |
33 |
Please wait while the buffers are buffering.
34 |
This is a toy designed while experimenting with the Three Shading Language.
35 |
Access more infos by clicking the ? button, also in the top left.
36 |
37 |
Controls
38 |
  • Left click to rotate the camera
  • 39 |
  • Right click to pan the viewport
  • 40 |
  • Wheel to zoom
  • 41 |
  • Press SPACE to toggle UI visibility
  • 42 |
    43 |
    Play with the sliders, click the buttons, and Have fun!
    44 |
    45 | } 46 | { !hasWebGPU && 47 |
    48 |
    WebGPU is not available on your browser. The simulation will not work.
    49 |
    Please try with a WebGPU compatible browser, like Chrome or Edge.
    50 |
    51 | } 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 | 61 | ) 62 | } 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSL Isolines Geometry 2 | 3 | ![TSL Isolines Geometry](./readme/isolines-github-header.jpg "TSL Isolines Geometry") 4 | 5 | Welcome to "TSL Isolines Geometry" 6 | This repo is adressed to three.js enthousiasts curious about TSL, but also to creative coders who may not know about this particular algorithm. 7 | 8 | ## Website 9 | 10 | Visit https://ulucode.com/random/webgputests/isolines/ to play! 11 | Requires a browser with WebGPU support. 12 | 13 | ## TSL 14 | Most of the important code, regarding TSL and the implementation of the algorithm is in [/src/lib/elements/IsolinesMeshing.ts](https://github.com/ULuIQ12/webgpu-isoline-geometry/blob/main/src/lib/elements/IsolinesMeshing.ts) 15 | The file is commented and uses descriptive variable names. 16 | It is partially typed, but don't worry if you know nothing about Typescript : you can safely ignore it (although I would encourage you to look into it). 17 | 18 | ## Disclaimer 19 | This is very experimental : I haven't looked under the hood at how TSL works, I'm just going from the examples provided by three.js and their documentation. 20 | I can't guarantee that I'm following good TSL practices is such a thing exists. My goal was to produce a fun toy, with an artistic flavor. 21 | 22 | ## Features 23 | 24 | - **TSL and WebGPU**: Takes advantage of Three Shading Language (TSL) and WebGPU, with vertex, fragment and compute shaders all in Javascript, no WGSL involved for the end user. 25 | - **Interactive Simulation**: Plenty of buttons and sliders to play with, as well cursor interactions. 26 | - **Capture**: Capture still frames of your creation. 27 | 28 | 29 | ## Getting Started 30 | 31 | To start the development environment for this project, follow these steps: 32 | 33 | 1. Clone the repository to your local machine: 34 | 35 | ```bash 36 | git clone https://github.com/ULuIQ12/webgpu-isoline-geometry.git 37 | ``` 38 | 39 | 2. Navigate to the project directory: 40 | 41 | ```bash 42 | cd webgpu-isoline-geometry 43 | ``` 44 | 45 | 3. Install the dependencies: 46 | 47 | ```bash 48 | npm install 49 | ``` 50 | 51 | 4. Start the development server: 52 | 53 | ```bash 54 | npm run dev 55 | ``` 56 | 57 | This will start the development server and open the project in your default browser. 58 | 59 | ## Building the Project 60 | 61 | 1. Edit the base path in vite.config.ts 62 | 63 | 2. To build the project for production, run the following command: 64 | 65 | ```bash 66 | npm run build 67 | ``` 68 | 69 | This will create an optimized build of the project in the `dist` directory. 70 | 71 | 72 | ## Acknowledgements 73 | - Uses Three.js https://threejs.org/ 74 | - Built with Vite https://vitejs.dev/ 75 | - UI Management uses LilGui and a bit of React https://react.dev/ 76 | - UI components use TailwindCSS https://tailwindcss.com/ 77 | - SDF functions and other utilities from Inigo Quilez https://iquilezles.org/ 78 | - Skybox is https://polyhaven.com/a/table_mountain_2_puresky by Greg Zaal and Jarod Guest 79 | 80 | ## Resources 81 | - Three.js WebGPU examples : https://threejs.org/examples/?q=webgpu 82 | - Three.js TSL documentation : https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language 83 | - HDR/EXR tp UltraHDR converter : https://gainmap-creator.monogrid.com/ 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/lib/elements/IsolinesMaterial.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // cutting the Typescript linting for this file, as it seems a bit too strict for the TSL code 3 | import { abs, dot, float, If, MeshStandardNodeMaterial, mx_fractal_noise_vec3, normalLocal, normalView, oscSquare, positionWorld, ShaderNodeObject, tslFn, UniformNode, VarNode, vec3 } from "three/webgpu"; 4 | 5 | export class IsolinesMaterial extends MeshStandardNodeMaterial { 6 | uWireFrame:ShaderNodeObject>; 7 | uUseBands:ShaderNodeObject>; 8 | uLayerHeight:ShaderNodeObject>; 9 | uRoughness:ShaderNodeObject>; 10 | uMetalness:ShaderNodeObject>; 11 | heightNode:ShaderNodeObject; 12 | gridWidth = 0; 13 | cellSize = 0; 14 | constructor( 15 | gridWidth:number, 16 | cellSize:number, 17 | uWireFrame:ShaderNodeObject>, 18 | uLayerHeight:ShaderNodeObject>, 19 | uUseBands:ShaderNodeObject>, 20 | heightNode:ShaderNodeObject, 21 | uRoughness:ShaderNodeObject>, 22 | uMetalness:ShaderNodeObject>, 23 | ) { 24 | 25 | super(); 26 | this.gridWidth = gridWidth; 27 | this.cellSize = cellSize; 28 | this.heightNode = heightNode; 29 | this.uWireFrame = uWireFrame; 30 | this.uUseBands = uUseBands; 31 | this.uLayerHeight = uLayerHeight; 32 | this.uRoughness = uRoughness; 33 | this.uMetalness = uMetalness; 34 | 35 | this.vertexColors = true; 36 | this.colorNode = this.customColorNode(); 37 | this.normalNode = this.customNormalNode(); 38 | this.roughnessNode = this.uRoughness; 39 | this.metalnessNode = this.uMetalness; 40 | } 41 | 42 | customColorNode = tslFn(() => { 43 | const ocol = vec3(1.0).toVar(); 44 | 45 | If( this.uWireFrame.equal(0), () => { 46 | const margin = float(0.1); 47 | const hgw = float(this.gridWidth*.5*this.cellSize).sub(this.cellSize*.5); // remove jagged edge due to tiling 48 | positionWorld.x.lessThan(hgw.negate().add(margin)).discard(); 49 | positionWorld.x.greaterThan(hgw.sub(margin)).discard(); 50 | // not necessary on Z, but I'm keeping it square 51 | positionWorld.z.lessThan(hgw.negate().add(margin)).discard(); 52 | positionWorld.z.greaterThan(hgw.sub(margin)).discard(); 53 | 54 | const d = abs( dot(normalLocal, vec3(0, 1, 0)) ); 55 | If( d.greaterThan(0.5).and( this.uUseBands.equal(1)), () => { // banding effect only horizontal faces 56 | const smallNoise = mx_fractal_noise_vec3(positionWorld.xz.mul(10.0), 1, 2, 0.5, .1 ); 57 | const band = oscSquare( this.heightNode(positionWorld.xyz.add(smallNoise)).mul(2).div(this.uLayerHeight) ).add(1.0).mul(0.25).oneMinus(); 58 | ocol.mulAssign(band); 59 | }); 60 | 61 | }); 62 | 63 | return ocol; 64 | }); 65 | 66 | customNormalNode = tslFn(() => { 67 | // adding graininess to the normal for a bit of texture 68 | const norm = normalView.xyz.toVar(); 69 | If( this.uWireFrame.equal(0), () => { 70 | const st = positionWorld.mul(100.0); 71 | const n1 = mx_fractal_noise_vec3(st, 1, 2, 0.5, this.uRoughness.mul(0.5)); 72 | norm.addAssign(n1.mul(0.5)).normalizeAssign(); 73 | }); 74 | 75 | return norm; 76 | }); 77 | } -------------------------------------------------------------------------------- /public/assets/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import LargeCollapsable from "./LargeCollapsable"; 3 | import { cameraIcon, closeCross, crosshairIcon, saveIcon, tools, uploadIcon } from "./GFX"; 4 | 5 | interface IInfoModalProps { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | } 9 | 10 | export default function InfoModal({ 11 | isOpen, 12 | onClose, 13 | }: IInfoModalProps) { 14 | 15 | const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark'); 16 | 17 | useEffect(() => { 18 | setTheme(localStorage.getItem('theme') || 'dark'); 19 | }, [isOpen]); 20 | 21 | const handleOpenCloseChange = () => { 22 | onClose(); 23 | } 24 | 25 | return ( 26 | <> 27 | 28 |
    29 |
    30 |
    Info / Help
    31 | 32 |
    33 |
    34 | 35 |
    36 | TODO 37 |
    38 |
    39 | 40 |
    41 | TODO 42 |
    43 |
    44 | 45 |
    46 | TODO 47 |
    48 |
    49 | {/* 50 | 51 |
    52 |
  • Go easy on the sliders, sometimes very small changes lead to big effects as all the factors are often in a tight balance.
  • 53 |
  • Use the "Randomize sliders" and "Randomize weights" button. You never know what result will appear. Find something you like that way 54 | and then fine tune the other sliders.
  • 55 |
  • To get a better understanding of the system, set the number of types to two, click "Set weights to zero", 56 | and then slowly change the values in the matrix while observing the result.
  • 57 |
  • The "Additive blending" and "After image" options mostly only work with dark backgrounds.
  • 58 |
  • Send me your favorite configuration! I might add it to the presets :)
  • 59 |
    60 |
    61 | */} 62 | 63 |
    64 |
  • Three.js, 3D for the web.
  • 65 |
  • Three.js webgpu examples, many of them using nodes and or TSL.
  • 66 |
  • Three.js TSL documentation.
  • 67 |
    68 |
    69 | 70 |
    71 |
  • Built with Vite
  • 72 |
  • Keeping it clean with TypeScript
  • 73 |
  • Some UI management with React
  • 74 |
  • Main UI is LilGUI
  • 75 |
  • 3D canvas with Three.js
  • 76 | {/*
  • Font is Rubik
  • */} 77 |
  • Icons picked from Bootstrap icons
  • 78 |
    79 |
    80 |
    81 |
    82 |
    83 | 84 | 85 | ) 86 | } -------------------------------------------------------------------------------- /src/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Josefin Sans Project Authors (https://github.com/ThomasJockin/JosefinSansFont-master), with Reserved Font Name "Josefin Sans". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import gsap from 'gsap'; 3 | import './App.css' 4 | import IntroModal from './components/IntroModal'; 5 | import { brightness, cameraIcon, closeCross, crosshairIcon, fxIcon, githubIcon, globeIcon, moonIcon, questionIcon, saveIcon, sunIcon, tools, twitterIcon, uluLogo, uploadIcon } from './components/GFX'; 6 | import { Root } from './lib/Root'; 7 | import InfoModal from './components/InfoModal'; 8 | 9 | 10 | 11 | function App() { 12 | 13 | const [isDrawerOpen, setIsDrawerOpen] = useState(false); 14 | const [isUIHidden, setIsUIHidden] = useState(false); 15 | 16 | const closedButtons = useRef(null); 17 | const sideButtonsRef = useRef(null); 18 | 19 | useEffect(() => { 20 | (document.getElementById('intro-modal') as unknown as any).showModal(); 21 | 22 | }, []); 23 | 24 | useEffect(() => { 25 | const handleKeyDown = (event: KeyboardEvent) => { 26 | if (event.code === 'Space') { 27 | console.log( "toggle UI") 28 | setIsUIHidden(!isUIHidden); 29 | } 30 | }; 31 | window.addEventListener('keydown', handleKeyDown); 32 | return () => { 33 | window.removeEventListener('keydown', handleKeyDown); 34 | }; 35 | }, [isUIHidden]); 36 | 37 | useEffect(() => { 38 | const tl = gsap.timeline(); 39 | if (isDrawerOpen) { 40 | if (sideButtonsRef.current !== null) tl.to(sideButtonsRef.current, { left: 450, duration: 0.25, ease: "power1.out" }, 0); 41 | if (closedButtons.current !== null) tl.to(closedButtons.current, { left: -50, duration: 0.25, ease: "power2.in" }, 0); 42 | 43 | } 44 | else { 45 | if (sideButtonsRef.current !== null) tl.to(sideButtonsRef.current, { left: -50, duration: 0.2, ease: "power1.in" }, 0); 46 | if (closedButtons.current !== null) tl.to(closedButtons.current, { left: 16, duration: 0.2, ease: "power2.out" }, 0.2); 47 | } 48 | }, [isDrawerOpen]); 49 | 50 | const handleOpen = () => { 51 | setIsDrawerOpen(true); 52 | }; 53 | 54 | const handleClose = () => { 55 | setIsDrawerOpen(false); 56 | }; 57 | 58 | 59 | 60 | 61 | const handleResetCamera = () => { 62 | //ParticlesLife.resetCamera(); 63 | } 64 | 65 | 66 | 67 | const [promoLinksMenuOpen, setPromoLinksMenuOpen] = useState(false); 68 | const promoMenuRef = useRef(null); 69 | const logoRef = useRef(null); 70 | 71 | const handlePromoToggle = () => { 72 | setPromoLinksMenuOpen(!promoLinksMenuOpen); 73 | const tl = gsap.timeline(); 74 | if (promoLinksMenuOpen) { 75 | tl.to(promoMenuRef.current, { opacity: 0, bottom: 96, duration: 0.2, ease: "power2.in" }, 0); 76 | tl.to(logoRef.current, { rotate: 10, duration: 0.1, ease: "power2.in" }, 0); 77 | tl.to(logoRef.current, { rotate: 0, duration: 0.5, ease: "elastic.out" }, 0.1); 78 | 79 | 80 | } 81 | else { 82 | tl.to(promoMenuRef.current, { opacity: 1, bottom: 80, duration: 0.2, ease: "power2.out" }, 0); 83 | tl.to(logoRef.current, { rotate: -10, duration: 0.1, ease: "power2.in" }, 0); 84 | tl.to(logoRef.current, { rotate: 0, duration: 0.5, ease: "elastic.out" }, 0.1); 85 | } 86 | } 87 | 88 | const handleCaptureRequest = () => { 89 | Root.StartCapture(); 90 | } 91 | 92 | const [infoModalOpen, setInfoModalOpen] = useState(false); 93 | const handleShowInfosClick = () => { 94 | setInfoModalOpen(true); 95 | (document.getElementById('info-modal') as unknown as any).showModal(); 96 | } 97 | 98 | const handleInfoClose = () => { 99 | setInfoModalOpen(false); 100 | (document.getElementById('info-modal') as unknown as any).close(); 101 | } 102 | 103 | return ( 104 |
    105 |
    106 |
    107 |
    108 |
    109 | 110 |
    111 |
    112 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 | { }} /> 119 | 120 |
    121 |
    122 |
    123 |
    124 | 125 |
    126 |
    127 |
    128 |
    129 |
    130 |
    131 | 132 | 133 |
    134 |
    135 |
    136 | 137 |
    138 | 144 |
    145 | 146 | 147 | 148 |
    149 | ) 150 | } 151 | 152 | export default App 153 | -------------------------------------------------------------------------------- /src/lib/Root.ts: -------------------------------------------------------------------------------- 1 | import { WebGPURenderer, PostProcessing, ACESFilmicToneMapping, Clock, PerspectiveCamera, Scene, Vector2, Vector3, pass, uniform, viewportTopLeft } from "three/webgpu"; 2 | import { OrbitControls, TrackballControls } from "three/examples/jsm/Addons.js"; 3 | import WebGPU from "three/examples/jsm/capabilities/WebGPU.js"; 4 | import { IAnimatedElement } from "./interfaces/IAnimatedElement"; 5 | import { IsolinesMeshing } from "./elements/IsolinesMeshing"; 6 | 7 | 8 | export class Root { 9 | 10 | static instance: Root; 11 | animatedElements: IAnimatedElement[] = []; 12 | static registerAnimatedElement(element: IAnimatedElement) { 13 | if (Root.instance == null) { 14 | throw new Error("Root instance not found"); 15 | } 16 | if (Root.instance.animatedElements.indexOf(element) == -1) { 17 | Root.instance.animatedElements.push(element); 18 | } 19 | } 20 | 21 | canvas: HTMLCanvasElement; 22 | 23 | constructor(canvas: HTMLCanvasElement) { 24 | 25 | this.canvas = canvas; 26 | 27 | if (Root.instance != null) { 28 | console.warn("Root instance already exists"); 29 | return; 30 | } 31 | Root.instance = this; 32 | } 33 | 34 | async init() { 35 | this.initRenderer(); 36 | this.initCamera(); 37 | await this.initScene(); 38 | this.initPost(); 39 | 40 | this.clock.start(); 41 | this.renderer!.setAnimationLoop(this.animate.bind(this)); 42 | 43 | return new Promise((resolve) => { 44 | resolve(); 45 | }); 46 | } 47 | 48 | renderer?: WebGPURenderer; 49 | clock: Clock = new Clock(false); 50 | post?: PostProcessing; 51 | initRenderer() { 52 | 53 | if (WebGPU.isAvailable() === false) { // doesn't work with WebGL2 54 | throw new Error('No WebGPU support'); 55 | } 56 | 57 | this.renderer = new WebGPURenderer({ canvas: this.canvas, antialias: true }); 58 | this.renderer.setPixelRatio(1); 59 | this.renderer.setSize(window.innerWidth, window.innerHeight); 60 | this.renderer.toneMapping = ACESFilmicToneMapping; 61 | this.renderer.toneMappingExposure = 1.1; 62 | console.log("Renderer :", this.renderer); 63 | window.addEventListener('resize', this.onResize.bind(this)); 64 | } 65 | 66 | camera: PerspectiveCamera = new PerspectiveCamera(70, 1, 1, 1000); 67 | controls?: OrbitControls | TrackballControls; 68 | initCamera() { 69 | const aspect: number = window.innerWidth / window.innerHeight; 70 | this.camera.aspect = aspect; 71 | this.camera.position.z = 10; 72 | this.camera.updateProjectionMatrix(); 73 | this.controls = new OrbitControls(this.camera, this.canvas); 74 | this.controls.target.set(0, 0, 0); 75 | } 76 | 77 | scene: Scene = new Scene(); 78 | fx:IsolinesMeshing; 79 | async initScene() { 80 | this.fx = new IsolinesMeshing(this.scene, this.camera, this.controls as OrbitControls, this.renderer!); 81 | await this.fx.init(); 82 | 83 | } 84 | 85 | postProcessing?: PostProcessing; 86 | afterImagePass ; 87 | scenePass ; 88 | uUseAfterImage = uniform(0); 89 | static setUseAfterImage( value:boolean ) { 90 | if( Root.instance == null ) { 91 | throw new Error("Root instance not found"); 92 | } 93 | Root.instance.uUseAfterImage.value = value ? 1 : 0; 94 | } 95 | initPost() { 96 | 97 | this.scenePass = pass(this.scene, this.camera); 98 | const vignette = viewportTopLeft.distance( .5 ).mul( 0.75 ).clamp().oneMinus(); 99 | this.postProcessing = new PostProcessing(this.renderer!); 100 | this.postProcessing.outputNode = this.scenePass.mul( vignette ); 101 | 102 | } 103 | 104 | onResize( event, toSize?:Vector2 ) { 105 | const size:Vector2 = new Vector2(window.innerWidth, window.innerHeight); 106 | if(toSize) size.copy(toSize); 107 | 108 | this.camera.aspect = size.x / size.y; 109 | this.camera.updateProjectionMatrix(); 110 | //this.renderer!.setPixelRatio(window.devicePixelRatio); 111 | this.renderer!.setPixelRatio(1); 112 | this.renderer!.setSize(size.x, size.y); 113 | this.renderer!.domElement.style.width = `${size.x}px`; 114 | this.renderer!.domElement.style.height = `${size.y}px`; 115 | } 116 | 117 | elapsedFrames = 0; 118 | animate() { 119 | if (!this.capturing) { 120 | const dt: number = this.clock.getDelta(); 121 | const elapsed: number = this.clock.getElapsedTime(); 122 | this.controls!.update(); 123 | this.animatedElements.forEach((element) => { 124 | element.update(dt, elapsed); 125 | }); 126 | this.postProcessing!.render(); 127 | 128 | this.elapsedFrames++; 129 | } 130 | 131 | if( this.elapsedFrames == 2) { 132 | 133 | this.fx.setPalette("default"); 134 | } 135 | } 136 | 137 | static StartCapture(): void { 138 | if (Root.instance == null) { 139 | throw new Error("Root instance not found"); 140 | } 141 | if( Root.instance.capturing ) { 142 | console.log( "Already capturing") 143 | return; 144 | } 145 | 146 | (async () => { 147 | await Root.instance.capture(); 148 | console.log( "Capture done"); 149 | })(); 150 | 151 | } 152 | 153 | capturing: boolean = false; 154 | savedPosition:Vector3 = new Vector3(); 155 | async capture() { 156 | try { 157 | this.capturing = true; 158 | //const resolution:Vector2 = new Vector2(4096,4096); 159 | const resolution:Vector2 = new Vector2(window.innerWidth,window.innerHeight); 160 | this.onResize(null, resolution); 161 | 162 | 163 | await new Promise(resolve => setTimeout(resolve, 20)); 164 | await this.postProcessing!.renderAsync(); 165 | 166 | const strMime = "image/jpeg"; 167 | const imgData = this.renderer.domElement.toDataURL(strMime, 1.0); 168 | const strDownloadMime: string = "image/octet-stream"; 169 | const filename: string = `particles_${(Date.now())}.jpg` 170 | 171 | await this.saveFile(imgData.replace(strMime, strDownloadMime), filename); 172 | 173 | } catch (e) { 174 | console.log(e); 175 | return; 176 | } 177 | 178 | } 179 | 180 | async saveFile(strData, filename) { 181 | const link = document.createElement('a'); 182 | if (typeof link.download === 'string') { 183 | this.renderer.domElement.appendChild(link); 184 | link.download = filename; 185 | link.href = strData; 186 | link.click(); 187 | this.renderer.domElement.removeChild(link); 188 | } else { 189 | // 190 | } 191 | await new Promise(resolve => setTimeout(resolve, 10)); 192 | 193 | this.onResize(null); 194 | this.capturing = false; 195 | } 196 | } -------------------------------------------------------------------------------- /src/lib/nodes/DistanceFunctions.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { abs, clamp, cond, dot, float, If, int, length, max, min, mul, sign, sqrt, tslFn, vec2, vec3, vec4 } from "three/webgpu"; 3 | 4 | // Inigo Quilez functions put through the transpiler 5 | // https://iquilezles.org/articles/distfunctions/ 6 | // https://iquilezles.org/articles/distfunctions2d/ 7 | // https://threejs.org/examples/?q=webgpu#webgpu_tsl_transpiler 8 | 9 | // 3D functions //////////////////////////////////////////////////////////////////////////// 10 | const sdSphere = tslFn(([p_immutable, s_immutable]) => { 11 | 12 | const s = float(s_immutable).toVar(); 13 | const p = vec3(p_immutable).toVar(); 14 | return length(p).sub(s); 15 | }).setLayout({ 16 | name: 'sdSphere', 17 | type: 'float', 18 | inputs: [ 19 | { name: 'p', type: 'vec3' }, 20 | { name: 's', type: 'float' } 21 | ] 22 | }); 23 | 24 | const sdHollowSphere = tslFn(([p_immutable, s_immutable, t_immutable]) => { 25 | 26 | const s = float(s_immutable).toVar(); 27 | const t = float(t_immutable).toVar(); 28 | const p = vec3(p_immutable).toVar(); 29 | return abs(length(p).sub(s)).sub(t); 30 | }).setLayout({ 31 | name: 'sdHollowSphere', 32 | type: 'float', 33 | inputs: [ 34 | { name: 'p', type: 'vec3' }, 35 | { name: 's', type: 'float' }, 36 | { name: 't', type: 'float' } 37 | ] 38 | }); 39 | 40 | 41 | const sdTorus = tslFn(([p_immutable, t_immutable]) => { 42 | 43 | const t = vec2(t_immutable).toVar(); 44 | const p = vec3(p_immutable).toVar(); 45 | const q = vec2(length(p.xz).sub(t.x), p.y).toVar(); 46 | return length(q).sub(t.y); 47 | }).setLayout({ 48 | name: 'sdTorus', 49 | type: 'float', 50 | inputs: [ 51 | { name: 'p', type: 'vec3' }, 52 | { name: 't', type: 'vec2' } 53 | ] 54 | }); 55 | 56 | // a slight change to the scaling from the original 57 | const sdPyramid = tslFn(([p_immutable, h_immutable, sc_immutable]) => { 58 | 59 | const h = float(h_immutable).toVar(); 60 | const p = vec3(p_immutable).toVar(); 61 | const sc = float(sc_immutable).toVar(); 62 | p.mulAssign(sc); 63 | const m2 = float(h.mul(h).add(0.25)).toVar(); 64 | p.xz.assign(abs(p.xz)); 65 | p.xz.assign(cond(p.z.greaterThan(p.x), p.zx, p.xz)); 66 | p.xz.subAssign(0.5); 67 | const q = vec3(p.z, h.mul(p.y).sub(mul(0.5, p.x)), h.mul(p.x).add(mul(0.5, p.y))).toVar(); 68 | const s = float(max(q.x.negate(), 0.0)).toVar(); 69 | const t = float(clamp(q.y.sub(mul(0.5, p.z)).div(m2.add(0.25)), 0.0, 1.0)).toVar(); 70 | const a = float(m2.mul(q.x.add(s).mul(q.x.add(s))).add(q.y.mul(q.y))).toVar(); 71 | const b = float(m2.mul(q.x.add(mul(0.5, t)).mul(q.x.add(mul(0.5, t)))).add(q.y.sub(m2.mul(t)).mul(q.y.sub(m2.mul(t))))).toVar(); 72 | const d2 = float(cond(min(q.y, q.x.negate().mul(m2).sub(q.y.mul(0.5))).greaterThan(0.0), 0.0, min(a, b))).toVar(); 73 | 74 | return sqrt(d2.add(q.z.mul(q.z)).div(m2)).mul(sign(max(q.z, p.y.negate()))); 75 | 76 | }).setLayout({ 77 | name: 'sdPyramid', 78 | type: 'float', 79 | inputs: [ 80 | { name: 'p', type: 'vec3' }, 81 | { name: 'h', type: 'float' }, 82 | { name: 'sc', type: 'float' } 83 | ] 84 | }); 85 | 86 | const sdBoxFrame = tslFn(([p_immutable, b_immutable, e_immutable]) => { 87 | 88 | const e = float(e_immutable).toVar(); 89 | const b = vec3(b_immutable).toVar(); 90 | const p = vec3(p_immutable).toVar(); 91 | p.assign(abs(p).sub(b)); 92 | const q = vec3(abs(p.add(e)).sub(e)).toVar(); 93 | 94 | return min(min(length(max(vec3(p.x, q.y, q.z), 0.0)).add(min(max(p.x, max(q.y, q.z)), 0.0)), length(max(vec3(q.x, p.y, q.z), 0.0)).add(min(max(q.x, max(p.y, q.z)), 0.0))), length(max(vec3(q.x, q.y, p.z), 0.0)).add(min(max(q.x, max(q.y, p.z)), 0.0))); 95 | 96 | }).setLayout({ 97 | name: 'sdBoxFrame', 98 | type: 'float', 99 | inputs: [ 100 | { name: 'p', type: 'vec3' }, 101 | { name: 'b', type: 'vec3' }, 102 | { name: 'e', type: 'float' } 103 | ] 104 | }); 105 | 106 | /////// 2D functions //////////////////////////////////////////////////////////////////////////// 107 | const sdBox = tslFn(([p_immutable, b_immutable]) => { 108 | 109 | const b = vec2(b_immutable).toVar(); 110 | const p = vec2(p_immutable).toVar(); 111 | const d = vec2(abs(p).sub(b)).toVar(); 112 | 113 | return length(max(d, 0.0)).add(min(max(d.x, d.y), 0.0)); 114 | 115 | }).setLayout({ 116 | name: 'sdBox', 117 | type: 'float', 118 | inputs: [ 119 | { name: 'p', type: 'vec2', qualifier: 'in' }, 120 | { name: 'b', type: 'vec2', qualifier: 'in' } 121 | ] 122 | }); 123 | 124 | export const sdRoundedBox = /*#__PURE__*/ tslFn(([p_immutable, b_immutable, r_immutable]) => { 125 | 126 | const r = vec4(r_immutable).toVar(); 127 | const b = vec2(b_immutable).toVar(); 128 | const p = vec2(p_immutable).toVar(); 129 | r.xy.assign(cond(p.x.greaterThan(0.0), r.xy, r.zw)); 130 | r.x.assign(cond(p.y.greaterThan(0.0), r.x, r.y)); 131 | const q = vec2(abs(p).sub(b).add(r.x)).toVar(); 132 | 133 | return min(max(q.x, q.y), 0.0).add(length(max(q, 0.0)).sub(r.x)); 134 | 135 | }).setLayout({ 136 | name: 'sdRoundedBox', 137 | type: 'float', 138 | inputs: [ 139 | { name: 'p', type: 'vec2', qualifier: 'in' }, 140 | { name: 'b', type: 'vec2', qualifier: 'in' }, 141 | { name: 'r', type: 'vec4', qualifier: 'in' } 142 | ] 143 | }); 144 | 145 | const sdCircle = tslFn(([p_immutable, r_immutable]) => { 146 | 147 | const r = float(r_immutable).toVar(); 148 | const p = vec2(p_immutable).toVar(); 149 | return length(p).sub(r); 150 | 151 | }).setLayout({ 152 | name: 'sdCircle', 153 | type: 'float', 154 | inputs: [ 155 | { name: 'p', type: 'vec2' }, 156 | { name: 'r', type: 'float' } 157 | ] 158 | }); 159 | 160 | const sdRoundedX = tslFn(([p_immutable, w_immutable, r_immutable]) => { 161 | 162 | const r = float(r_immutable).toVar(); 163 | const w = float(w_immutable).toVar(); 164 | const p = vec2(p_immutable).toVar(); 165 | p.assign(abs(p)); 166 | 167 | return length(p.sub(min(p.x.add(p.y), w).mul(0.5))).sub(r); 168 | 169 | }).setLayout({ 170 | name: 'sdRoundedX', 171 | type: 'float', 172 | inputs: [ 173 | { name: 'p', type: 'vec2', qualifier: 'in' }, 174 | { name: 'w', type: 'float', qualifier: 'in' }, 175 | { name: 'r', type: 'float', qualifier: 'in' } 176 | ] 177 | }); 178 | 179 | const sdHexagon = tslFn(([p_immutable, r_immutable]) => { 180 | 181 | const r = float(r_immutable).toVar(); 182 | const p = vec2(p_immutable).toVar(); 183 | const k = vec3(- 0.866025404, 0.5, 0.577350269); 184 | p.assign(abs(p)); 185 | p.subAssign(mul(2.0, min(dot(k.xy, p), 0.0).mul(k.xy))); 186 | p.subAssign(vec2(clamp(p.x, k.z.negate().mul(r), k.z.mul(r)), r)); 187 | 188 | return length(p).mul(sign(p.y)); 189 | 190 | }).setLayout({ 191 | name: 'sdHexagon', 192 | type: 'float', 193 | inputs: [ 194 | { name: 'p', type: 'vec2', qualifier: 'in' }, 195 | { name: 'r', type: 'float', qualifier: 'in' } 196 | ] 197 | }); 198 | 199 | const sdCross = /*#__PURE__*/ tslFn(([p_immutable, b_immutable, r_immutable]) => { 200 | 201 | const r = float(r_immutable).toVar(); 202 | const b = vec2(b_immutable).toVar(); 203 | const p = vec2(p_immutable).toVar(); 204 | p.assign(abs(p)); 205 | p.assign(cond(p.y.greaterThan(p.x), p.yx, p.xy)); 206 | const q = vec2(p.sub(b)).toVar(); 207 | const k = float(max(q.y, q.x)).toVar(); 208 | const w = vec2(cond(k.greaterThan(0.0), q, vec2(b.y.sub(p.x), k.negate()))).toVar(); 209 | 210 | return sign(k).mul(length(max(w, 0.0))).add(r); 211 | 212 | }).setLayout({ 213 | name: 'sdCross', 214 | type: 'float', 215 | inputs: [ 216 | { name: 'p', type: 'vec2', qualifier: 'in' }, 217 | { name: 'b', type: 'vec2', qualifier: 'in' }, 218 | { name: 'r', type: 'float' } 219 | ] 220 | }); 221 | 222 | const sdMoon = /*#__PURE__*/ tslFn(([p_immutable, d_immutable, ra_immutable, rb_immutable]) => { 223 | 224 | const rb = float(rb_immutable).toVar(); 225 | const ra = float(ra_immutable).toVar(); 226 | const d = float(d_immutable).toVar(); 227 | const p = vec2(p_immutable).toVar(); 228 | p.y.assign(abs(p.y)); 229 | const a = float(ra.mul(ra).sub(rb.mul(rb)).add(d.mul(d)).div(mul(2.0, d))).toVar(); 230 | const b = float(sqrt(max(ra.mul(ra).sub(a.mul(a)), 0.0))).toVar(); 231 | 232 | If(d.mul(p.x.mul(b).sub(p.y.mul(a))).greaterThan(d.mul(d).mul(max(b.sub(p.y), 0.0))), () => { 233 | 234 | return length(p.sub(vec2(a, b))); 235 | 236 | }); 237 | 238 | return max(length(p).sub(ra), length(p.sub(vec2(d, int(0)))).sub(rb).negate()); 239 | 240 | }).setLayout({ 241 | name: 'sdMoon', 242 | type: 'float', 243 | inputs: [ 244 | { name: 'p', type: 'vec2' }, 245 | { name: 'd', type: 'float' }, 246 | { name: 'ra', type: 'float' }, 247 | { name: 'rb', type: 'float' } 248 | ] 249 | }); 250 | 251 | export { sdTorus, sdSphere, sdPyramid, sdBoxFrame, sdHollowSphere }; 252 | export { sdCircle, sdBox, sdRoundedX, sdHexagon, sdCross, sdMoon } -------------------------------------------------------------------------------- /src/components/GFX.tsx: -------------------------------------------------------------------------------- 1 | export const closeCross = () => ( 2 | 3 | 4 | 5 | ); 6 | 7 | 8 | export const tools = () => ( 9 | 10 | 11 | 12 | ); 13 | 14 | export const brightness = () => ( 15 | 16 | 17 | 18 | ); 19 | 20 | export const uploadIcon = () => ( 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export const saveIcon = () => ( 28 | 29 | 30 | 31 | ); 32 | 33 | export const crosshairIcon = () => ( 34 | 35 | 36 | 37 | ) 38 | 39 | export const questionIcon = () => ( 40 | 41 | 42 | 43 | ) 44 | 45 | // #FF1F70 white 46 | export const uluLogo = () => ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ) 57 | 58 | export const globeIcon = () => ( 59 | 60 | 61 | 62 | ) 63 | 64 | // I prefer the bird! 65 | export const twitterIcon = () => ( 66 | 67 | 68 | 69 | ) 70 | 71 | export const githubIcon = () => ( 72 | 73 | 74 | 75 | ) 76 | 77 | export const fxIcon = () => ( 78 | 79 | 80 | 81 | 82 | ) 83 | 84 | export const cameraIcon = () => ( 85 | 86 | 87 | 88 | 89 | ) 90 | 91 | export const sunIcon = () => ( 92 | 97 | 99 | 100 | ) 101 | 102 | export const moonIcon = () => ( 103 | 108 | 110 | 111 | ) 112 | -------------------------------------------------------------------------------- /public/assets/gsap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/elements/IsolinesMeshing.ts: -------------------------------------------------------------------------------- 1 | 2 | // @ts-nocheck 3 | // cutting the Typescript linting for this file, as it seems a bit too strict for the TSL code 4 | import { IAnimatedElement } from "../interfaces/IAnimatedElement"; 5 | import { WebGPURenderer, BufferGeometry, DirectionalLight, DirectionalLightShadow, EquirectangularReflectionMapping,Group, Mesh, PerspectiveCamera, Plane, Scene, Vector3, Vector4, StorageBufferAttribute } from "three/webgpu"; 6 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 7 | import { Root } from "../Root"; 8 | import { loop, color, float, If, instanceIndex, max, MeshStandardNodeMaterial, min,mx_fractal_noise_float, pow, storage, sub, tslFn, uniform, uniforms, vec3, vec4, cond, int, mix, timerGlobal, positionWorld, mul, oscSine} from "three/webgpu"; 9 | import { OrbitControls, UltraHDRLoader } from "three/examples/jsm/Addons.js"; 10 | import { Pointer } from "../utils/Pointer"; 11 | import { IsolinesMaterial } from "./IsolinesMaterial"; 12 | import { Palettes } from "./Palettes"; 13 | 14 | export class IsolinesMeshing implements IAnimatedElement { 15 | scene: Scene; 16 | camera: PerspectiveCamera; 17 | renderer: WebGPURenderer; 18 | controls: OrbitControls; 19 | gui: GUI; 20 | pointerHandler: Pointer; 21 | 22 | 23 | constructor(scene: Scene, camera: PerspectiveCamera, controls: OrbitControls, renderer: WebGPURenderer) { 24 | this.scene = scene; 25 | this.camera = camera; 26 | this.controls = controls; 27 | this.controls.enableDamping = true; 28 | this.controls.dampingFactor = 0.1; 29 | this.camera.position.set(0, 30, -100); 30 | this.camera.updateMatrixWorld(); 31 | this.renderer = renderer; 32 | this.renderer.shadowMap.enabled = true; 33 | this.pointerHandler = new Pointer(this.renderer, this.camera); 34 | this.pointerHandler.iPlane = new Plane(new Vector3(0, 1, 0), 0.0); 35 | this.gui = new GUI(); 36 | } 37 | 38 | async init() { 39 | 40 | this.createLights(); 41 | // this uniform buffer needs to be initialized at the maximum size i'm going to use at first for the rest of palettes to work correctly 42 | this.layerColors.array = Palettes.getGrayGradient(128).array; 43 | this.uNbColors.value = this.layerColors.array.length; 44 | 45 | await this.initMesh(); 46 | 47 | // load the bg / envmap // https://polyhaven.com/a/table_mountain_2_puresky 48 | // converted to Adobe Gain Map with https://gainmap-creator.monogrid.com/ 49 | const texture = await new UltraHDRLoader().setPath('./assets/ultrahdr/').loadAsync('table_mountain_2_puresky_2k.jpg', (progress) => { 50 | console.log("Skybox load progress", Math.round(progress.loaded / progress.total * 100) + "%"); 51 | }); 52 | texture.mapping = EquirectangularReflectionMapping; 53 | this.scene.background = texture; 54 | this.scene.environment = texture; 55 | 56 | // plug the main animation loop 57 | Root.registerAnimatedElement(this); 58 | 59 | this.initGUI(); 60 | 61 | // palette is set back to default after a few frames , in Root.ts / update 62 | } 63 | 64 | uNbColors = uniform(8); // number of colors in the current palette 65 | layerColors = uniforms([]); // the colors of the palette 66 | 67 | uScrollTimeScale = uniform(1.0); // 68 | uScrollSpeedX = uniform(0.0); 69 | uScrollSpeedY = uniform(-0.01); 70 | uScrollSpeedZ = uniform(0.01); 71 | uRotationSpeed = uniform(0.0); 72 | uNoiseScaleX = uniform(1.0); 73 | uNoiseScaleZ = uniform(1.0); 74 | uScrollOffset = uniform(new Vector4(0.0, 0.0, 0.0, 0.0)); 75 | 76 | uFrequency = uniform(0.01); 77 | uOctaves = uniform(4.0); 78 | 79 | useCursor: boolean = false; 80 | uUseCursor = uniform(0); 81 | uCursorSize = uniform(20); 82 | 83 | 84 | tilings = [ 85 | "Triangles", 86 | "Quads", 87 | ] 88 | tiling: string = this.tilings[0]; 89 | uTiling = uniform(0.0); 90 | hideWalls: boolean = false; 91 | rotatePalette: boolean = false; 92 | uRotatePalette = uniform(0); 93 | uPaletteRotSpeed = uniform(1.0); 94 | uWireFrame = uniform(0); 95 | useBands: boolean = true; 96 | uUseBands = uniform(1); 97 | 98 | uRoughness = uniform(0.8); 99 | uMetalness = uniform(0.1); 100 | 101 | 102 | palettes = [ 103 | "default", 104 | "rainbow", 105 | "shibuya", 106 | "tutti", 107 | "earthy", 108 | "gray", 109 | "blackWhite", 110 | "mondrian", 111 | "pinkBlueMirror" 112 | ]; 113 | palette: string = this.palettes[0]; 114 | 115 | infos: string = "Field marked with * are GPU intensive, adjust with that in mind"; 116 | initGUI() { 117 | const infoFolder = this.gui.addFolder("Info"); 118 | infoFolder.domElement.children[1].append("Field marked with * have an influence on performances, adjust with that in mind"); 119 | 120 | const noiseFolder = this.gui.addFolder("Noise"); 121 | noiseFolder.add(this.uScrollTimeScale, 'value', 0.0, 5.0).name("Scroll Time Scale"); 122 | noiseFolder.add(this.uScrollSpeedX, 'value', -0.3, 0.3).name("Scroll Speed X"); 123 | noiseFolder.add(this.uScrollSpeedY, 'value', -0.3, 0.3).name("Scroll Speed Y"); 124 | noiseFolder.add(this.uScrollSpeedZ, 'value', -0.3, 0.3).name("Scroll Speed Z"); 125 | noiseFolder.add(this.uRotationSpeed, 'value', -0.3, 0.3).name("Rotation Speed"); 126 | noiseFolder.add(this.uNoiseScaleX, 'value', 0.1, 5.0).name("Noise scale X"); 127 | noiseFolder.add(this.uNoiseScaleZ, 'value', 0.1, 5.0).name("Noise scale Z"); 128 | noiseFolder.add(this.uFrequency, 'value', 0.001, 0.02).name("Noise frequency"); 129 | noiseFolder.add(this.uOctaves, 'value', 1.0, 9.0).name("Noise octaves *"); 130 | 131 | const generationFolder = this.gui.addFolder("Generation"); 132 | generationFolder.add(this.uNbLayers, 'value', 4, 128).name("Nb layers *").onChange((v) => { 133 | if (this.palette === "gray") { 134 | this.layerColors.array = Palettes.getGrayGradient(v).array; 135 | this.uNbColors.value = this.layerColors.array.length; 136 | } 137 | }); 138 | generationFolder.add(this.uLayerHeight, 'value', 0.01, 5).name("Layer height"); 139 | generationFolder.add(this, 'tiling', this.tilings).name("Tiling").onChange((v) => { 140 | this.uTiling.value = this.tilings.indexOf(v); 141 | }); 142 | 143 | const aspectFolder = this.gui.addFolder("Aspect"); 144 | aspectFolder.add(this.mainMaterial, 'wireframe').name('Wireframe').onChange((v) => { 145 | this.sideMaterial.wireframe = v; 146 | this.uWireFrame.value = v ? 1 : 0; 147 | }); 148 | 149 | aspectFolder.add(this, 'palette', this.palettes).name("Palette").onChange((v) => { 150 | this.setPalette(v); 151 | }); 152 | 153 | aspectFolder.add(this, 'hideWalls').name("Hide walls *").onChange((v) => { 154 | this.sideMesh.visible = !v; 155 | }); 156 | aspectFolder.add(this, 'useBands').name("Dark bands *").onChange((v) => { 157 | this.uUseBands.value = v ? 1 : 0; 158 | }); 159 | 160 | aspectFolder.add(this.uRoughness, 'value', 0.0, 1.0).name("Roughness"); 161 | aspectFolder.add(this.uMetalness, 'value', 0.0, 1.0).name("Metalness"); 162 | 163 | aspectFolder.add(this, 'rotatePalette').name("Rotate palette").onChange((v) => { 164 | this.uRotatePalette.value = v ? 1 : 0; 165 | }); 166 | aspectFolder.add(this.uPaletteRotSpeed, 'value', 0.0, 20.0).name("Pal. rotation speed"); 167 | 168 | 169 | const cursorFolder = this.gui.addFolder("Cursor"); 170 | cursorFolder.add(this, 'useCursor').name("Use cursor").onChange((v) => { 171 | this.uUseCursor.value = v ? 1 : 0; 172 | }) 173 | cursorFolder.add(this.uCursorSize, 'value', 1, 100).name("Cursor size"); 174 | 175 | 176 | document.addEventListener('keydown', (e) => { 177 | if (e.code === 'Space') { 178 | if (this.gui._hidden) 179 | this.gui.show(); 180 | else 181 | this.gui.hide(); 182 | } 183 | }); 184 | } 185 | 186 | setPalette(paletteName: string = "default") { 187 | switch (paletteName) { 188 | case "default": 189 | this.layerColors.array = Palettes.defaultColors.array; 190 | break; 191 | case "rainbow": 192 | this.layerColors.array = Palettes.rainbowColors.array; 193 | break; 194 | case "shibuya": 195 | this.layerColors.array = Palettes.shibuyaColor.array; 196 | break; 197 | case "tutti": 198 | this.layerColors.array = Palettes.tuttiColors.array; 199 | break; 200 | case "earthy": 201 | this.layerColors.array = Palettes.earthyColors.array; 202 | break; 203 | case "gray": 204 | this.layerColors.array = Palettes.getGrayGradient(this.uNbLayers.value).array; 205 | break; 206 | case "blackWhite": 207 | this.layerColors.array = Palettes.blackWhite.array; 208 | break; 209 | case "mondrian": 210 | this.layerColors.array = Palettes.mondrian.array; 211 | break; 212 | case "pinkBlueMirror": 213 | this.layerColors.array = Palettes.pinkBlueMirror.array; 214 | break; 215 | } 216 | this.uNbColors.value = this.layerColors.array.length; 217 | } 218 | 219 | lightGroup: Group = new Group(); 220 | dirLight: DirectionalLight; 221 | createLights() { 222 | 223 | this.dirLight = new DirectionalLight(0xffffff, 3); 224 | this.dirLight.position.set(this.gridWidth * .5, this.gridWidth * .5, this.gridWidth * .5).multiplyScalar(this.cellSize); 225 | this.dirLight.castShadow = true; 226 | const s: DirectionalLightShadow = this.dirLight.shadow; 227 | const sCamSize: number = 175; 228 | s.bias = -0.001; 229 | s.mapSize.set(4096, 4096); 230 | s.camera.near = 0; 231 | s.camera.far = this.gridWidth * this.cellSize * 2; 232 | s.camera.left = -sCamSize; 233 | s.camera.right = sCamSize; 234 | s.camera.top = sCamSize; 235 | s.camera.bottom = -sCamSize; 236 | 237 | this.lightGroup.add(this.dirLight); 238 | this.scene.add(this.lightGroup); 239 | 240 | 241 | } 242 | 243 | 244 | uNbLayers = uniform(32); 245 | uLayerHeight = uniform(1); 246 | gridWidth: number = 256; 247 | cellSize: number = 1; 248 | 249 | nbTris: number = this.gridWidth * this.gridWidth * 2; 250 | maxSubdiv: number = 60; // the max number of vertices generated per triangle. 60 is fine in most cases, but you can see holes appearing if the sliders are pushed 251 | nbBaseVertices: number = this.nbTris * 3; 252 | nbBaseNormals: number = this.nbTris * 3; 253 | nbBigVertices: number = this.nbTris * this.maxSubdiv; 254 | nbBigNormals: number = this.nbTris * this.maxSubdiv; 255 | nbBigColors: number = this.nbTris * this.maxSubdiv; 256 | 257 | nbSideQuads: number = (this.gridWidth - 1) * 4; // a one tile margin to hide the jagged edge of the triangle tiling 258 | nbSideVertices: number = this.nbSideQuads * 6; 259 | 260 | sbaVertices: StorageBufferAttribute; 261 | sbaNormals: StorageBufferAttribute; 262 | 263 | sbaBigVertices: StorageBufferAttribute; 264 | sbaBigNormals: StorageBufferAttribute; 265 | sbaBigColors: StorageBufferAttribute; 266 | 267 | sbaSideVertices: StorageBufferAttribute; 268 | sbaSideNormals: StorageBufferAttribute; 269 | sbaSideColors: StorageBufferAttribute; 270 | 271 | mainMaterial: MeshStandardNodeMaterial; 272 | sideMaterial: MeshStandardNodeMaterial; 273 | mainMesh: Mesh; 274 | sideMesh: Mesh; 275 | async initMesh() { 276 | // main mesh 277 | this.sbaBigVertices = new StorageBufferAttribute(this.nbBigVertices, 4); 278 | this.sbaBigNormals = new StorageBufferAttribute(this.nbBigNormals, 4); 279 | this.sbaBigColors = new StorageBufferAttribute(this.nbBigColors, 4); 280 | 281 | const bigGeom: BufferGeometry = new BufferGeometry(); 282 | bigGeom.setAttribute("position", this.sbaBigVertices); 283 | bigGeom.setAttribute("normal", this.sbaBigNormals); 284 | bigGeom.setAttribute("color", this.sbaBigColors); 285 | 286 | const isoMat: IsolinesMaterial = new IsolinesMaterial( 287 | this.gridWidth, 288 | this.cellSize, 289 | this.uWireFrame, 290 | this.uLayerHeight, 291 | this.uUseBands, 292 | this.getHeight.bind(this), 293 | this.uRoughness, 294 | this.uMetalness 295 | ); 296 | this.mainMaterial = isoMat; 297 | 298 | const bigMesh: Mesh = new Mesh(bigGeom, isoMat); 299 | bigMesh.castShadow = true; 300 | bigMesh.receiveShadow = true; 301 | bigMesh.frustumCulled = false; 302 | this.scene.add(bigMesh); 303 | this.mainMesh = bigMesh; 304 | 305 | ////// sides 306 | // the sides should probably be done in the main compute, with the same treatment 307 | // but for now, this is a good enough approximation 308 | this.sbaSideVertices = new StorageBufferAttribute(this.nbSideVertices, 4); 309 | this.sbaSideNormals = new StorageBufferAttribute(this.nbSideVertices, 4); 310 | 311 | const sideGeom: BufferGeometry = new BufferGeometry(); 312 | sideGeom.setAttribute("position", this.sbaSideVertices); 313 | sideGeom.setAttribute("normal", this.sbaSideNormals); 314 | 315 | const sideMat: MeshStandardNodeMaterial = new MeshStandardNodeMaterial(); 316 | sideMat.roughness = 0.8; 317 | sideMat.metalness = 0.1; 318 | sideMat.colorNode = this.sideColorNode(); 319 | sideMat.roughnessNode = this.uRoughness; 320 | sideMat.metalnessNode = this.uMetalness; 321 | this.sideMaterial = sideMat; 322 | const sideMesh: Mesh = new Mesh(sideGeom, sideMat); 323 | sideMesh.castShadow = true; 324 | sideMesh.receiveShadow = true; 325 | sideMesh.frustumCulled = false; 326 | this.scene.add(sideMesh); 327 | this.sideMesh = sideMesh; 328 | 329 | /* 330 | // base grid mesh 331 | this.sbaVertices = new StorageBufferAttribute(this.nbBaseVertices, 4); 332 | this.sbaNormals = new StorageBufferAttribute(this.nbBaseNormals, 4); 333 | const testGeom: BufferGeometry = new BufferGeometry(); 334 | testGeom.setAttribute("position", this.sbaVertices); 335 | testGeom.setAttribute("normal", this.sbaNormals); 336 | const testMat: MeshStandardNodeMaterial = new MeshStandardNodeMaterial(); 337 | testMat.opacity = 0.1; 338 | testMat.wireframe = true; 339 | testMat.transparent = true; 340 | const testMesh: Mesh = new Mesh(testGeom, testMat); 341 | testMesh.frustumCulled = false; 342 | this.scene.add(testMesh); 343 | */ 344 | 345 | 346 | await this.renderer.computeAsync(this.computeTriangles); 347 | await this.renderer.computeAsync(this.computeSides); 348 | } 349 | 350 | sideColorNode = tslFn(() => { 351 | const h = positionWorld.y.div(this.uLayerHeight).floor().add(1); 352 | return this.getLayerColor(h); 353 | }); 354 | 355 | 356 | // runs for all triangles of the grid 357 | computeTriangles = tslFn(() => { 358 | 359 | const cellIndex = instanceIndex.div(2); // I'm treating them as slanted quads 360 | // world offset 361 | const px = cellIndex.remainder(this.gridWidth).toFloat().mul(this.cellSize).sub(float(this.gridWidth).mul(this.cellSize).mul(0.5)); 362 | const pz = cellIndex.div(this.gridWidth).toFloat().mul(this.cellSize).sub(float(this.gridWidth).mul(this.cellSize).mul(0.5)); 363 | const tri = instanceIndex.remainder(2).equal(0); // which side of the quad 364 | const p0 = vec3(0.0).toVar(); 365 | const p1 = vec3(0.0).toVar(); 366 | const p2 = vec3(0.0).toVar(); 367 | const p3 = vec3(0.0).toVar(); 368 | 369 | If(this.uTiling.equal(0), () => { 370 | // triangle tiling 371 | const offset = cond(cellIndex.div(this.gridWidth).remainder(2).equal(0), -this.cellSize * .5, 0); 372 | p0.assign(vec3(px.add(offset), 0, pz)); 373 | p1.assign(vec3(px.add(offset).add(this.cellSize), 0, pz)); 374 | p2.assign(vec3(px.add(offset).add(this.cellSize * .5), 0, pz.add(this.cellSize))); 375 | p3.assign(vec3(px.add(offset).add(this.cellSize).add(this.cellSize * .5), 0, pz.add(this.cellSize))); 376 | }).else(() => { 377 | // normal quad tiling 378 | p0.assign(vec3(px, 0, pz)); 379 | p1.assign(vec3(px.add(this.cellSize), 0, pz)); 380 | p2.assign(vec3(px, 0, pz.add(this.cellSize))); 381 | p3.assign(vec3(px.add(this.cellSize), 0, pz.add(this.cellSize))); 382 | }); 383 | 384 | const v0Pos = vec3(0.0).toVar(); 385 | const v1Pos = vec3(0.0).toVar(); 386 | const v2Pos = vec3(0.0).toVar(); 387 | 388 | // assign the vertices of the triangle 389 | If(tri, () => { 390 | v0Pos.assign(p0); 391 | v1Pos.assign(p2); 392 | v2Pos.assign(p1); 393 | }).else(() => { 394 | v0Pos.assign(p1); 395 | v1Pos.assign(p2); 396 | v2Pos.assign(p3); 397 | }) 398 | 399 | // get height of the vertices 400 | const h1 = this.getHeight(v0Pos).toVar(); 401 | const h2 = this.getHeight(v1Pos).toVar(); 402 | const h3 = this.getHeight(v2Pos).toVar(); 403 | v0Pos.y.assign(h1); 404 | v1Pos.y.assign(h2); 405 | v2Pos.y.assign(h3); 406 | 407 | // get the min and max height of the triangle 408 | const h_min = min(h1, min(h2, h3)).div(this.uLayerHeight); 409 | const h_max = max(h1, max(h2, h3)).div(this.uLayerHeight); 410 | const temp = vec3(0.0).toVar(); 411 | const v1 = vec3(0.0).toVar().assign(v0Pos.xyz); 412 | const v2 = vec3(0.0).toVar().assign(v1Pos.xyz); 413 | const v3 = vec3(0.0).toVar().assign(v2Pos.xyz); 414 | 415 | // set where in the buffers are we going to store the vertices for the triangle decomposition 416 | const startIndex = int(instanceIndex).mul(this.maxSubdiv); 417 | 418 | // our buffers 419 | const positions = storage(this.sbaBigVertices, 'vec4', this.sbaBigVertices.count); 420 | const normals = storage(this.sbaBigNormals, 'vec4', this.sbaBigNormals.count); 421 | const colors = storage(this.sbaBigColors, 'vec4', this.sbaBigColors.count); 422 | 423 | const nn = vec3(0.0).toVar(); 424 | const giMix = float(0.8); // for GI effect, could be a uniform 425 | const aoMul = float(0.3); // for AO effect, could be a uniform 426 | 427 | const vIndex = int(startIndex).toVar(); 428 | 429 | loop({ type: 'uint', start: h_min, end: h_max, condition: '<=' }, ({ i }) => { // for each layer 430 | const points_above = int(0).toVar(); 431 | const h = float(i).mul(this.uLayerHeight); 432 | const col = this.getLayerColor(int(i)); // color of the layer 433 | const dark = mix(col, this.getLayerColor(int(i.sub(1))), giMix).mul(aoMul); // to color the bottom vertices of vertical quads 434 | // calculate the number of points above the current layer among the three vertices, and reorder them if needed to keep consistant 435 | If(h1.lessThan(h), () => { 436 | If(h2.lessThan(h), () => { 437 | points_above.assign(cond(h3.lessThan(h), 0, 1)); 438 | 439 | }).else(() => { 440 | If(h3.lessThan(h), () => { 441 | points_above.assign(1); 442 | temp.xyz = v1.xyz; 443 | v1.xyz = v3.xyz; 444 | v3.xyz = v2.xyz; 445 | v2.xyz = temp.xyz; 446 | }).else(() => { 447 | points_above.assign(2); 448 | temp.xyz = v1.xyz; 449 | v1.xyz = v2.xyz; 450 | v2.xyz = v3.xyz; 451 | v3.xyz = temp.xyz; 452 | }); 453 | }); 454 | }).else(() => { 455 | If(h2.lessThan(h), () => { 456 | If(h3.lessThan(h), () => { 457 | points_above.assign(1); 458 | temp.xyz = v1.xyz; 459 | v1.xyz = v2.xyz; 460 | v2.xyz = v3.xyz; 461 | v3.xyz = temp.xyz; 462 | }).else(() => { 463 | points_above.assign(2); 464 | temp.xyz = v1.xyz; 465 | v1.xyz = v3.xyz; 466 | v3.xyz = v2.xyz; 467 | v2.xyz = temp.xyz; 468 | }); 469 | 470 | }).else(() => { 471 | If(h3.lessThan(h), () => { 472 | points_above.assign(2); 473 | }).else(() => { 474 | points_above.assign(3); 475 | }); 476 | }); 477 | }) 478 | 479 | // update height in case of reorder 480 | h1.assign(v1.y); 481 | h2.assign(v2.y); 482 | h3.assign(v3.y); 483 | 484 | // define cap points 485 | const v1_c = vec3(v1.x, h, v1.z); 486 | const v2_c = vec3(v2.x, h, v2.z); 487 | const v3_c = vec3(v3.x, h, v3.z); 488 | 489 | // define bottom points 490 | const v1_b = vec3(v1.x, h.sub(this.uLayerHeight), v1.z); 491 | const v2_b = vec3(v2.x, h.sub(this.uLayerHeight), v2.z); 492 | const v3_b = vec3(v3.x, h.sub(this.uLayerHeight), v3.z); 493 | 494 | // treat each configuration 495 | If(points_above.equal(3), () => { 496 | // just a flat triangle 497 | positions.element(vIndex).assign(v1_c); 498 | positions.element(vIndex.add(1)).assign(v2_c); 499 | positions.element(vIndex.add(2)).assign(v3_c); 500 | 501 | nn.assign(this.calcNormal([v1_c, v2_c, v3_c])); 502 | normals.element(vIndex).assign(nn); 503 | normals.element(vIndex.add(1)).assign(nn); 504 | normals.element(vIndex.add(2)).assign(nn); 505 | 506 | colors.element(vIndex).xyz.assign(col.xyz); 507 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 508 | colors.element(vIndex.add(2)).xyz.assign(col.xyz); 509 | 510 | vIndex.addAssign(3); 511 | 512 | }).else(() => { 513 | // interpolate the points to get projections at threshold height 514 | const t1 = h1.sub(h).div(h1.sub(h3)); 515 | const v1_c_n = mix(v1_c, v3_c, t1); 516 | const v1_b_n = mix(v1_b, v3_b, t1); 517 | const t2 = h2.sub(h).div(h2.sub(h3)); 518 | const v2_c_n = mix(v2_c, v3_c, t2); 519 | const v2_b_n = mix(v2_b, v3_b, t2); 520 | 521 | If(points_above.equal(2), () => { 522 | 523 | // 2 triangles cap 524 | positions.element(vIndex).assign(v1_c); 525 | positions.element(vIndex.add(1)).assign(v2_c); 526 | positions.element(vIndex.add(2)).assign(v2_c_n); 527 | nn.assign(this.calcNormal([v1_c, v2_c, v2_c_n])); 528 | normals.element(vIndex).assign(nn); 529 | normals.element(vIndex.add(1)).assign(nn); 530 | normals.element(vIndex.add(2)).assign(nn); 531 | colors.element(vIndex).xyz.assign(col.xyz); 532 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 533 | colors.element(vIndex.add(2)).xyz.assign(col.xyz); 534 | vIndex.addAssign(3); 535 | ///////////////////////////////////////// 536 | positions.element(vIndex).assign(v2_c_n); 537 | positions.element(vIndex.add(1)).assign(v1_c_n); 538 | positions.element(vIndex.add(2)).assign(v1_c); 539 | nn.assign(this.calcNormal([v2_c_n, v1_c_n, v1_c])); 540 | normals.element(vIndex).assign(nn); 541 | normals.element(vIndex.add(1)).assign(nn); 542 | normals.element(vIndex.add(2)).assign(nn); 543 | colors.element(vIndex).xyz.assign(col.xyz); 544 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 545 | colors.element(vIndex.add(2)).xyz.assign(col.xyz); 546 | vIndex.addAssign(3); 547 | ///////////////////////////////////////// 548 | // 2 triangles vertical wall 549 | positions.element(vIndex).assign(v1_c_n); 550 | positions.element(vIndex.add(1)).assign(v2_c_n); 551 | positions.element(vIndex.add(2)).assign(v2_b_n); 552 | nn.assign(this.calcNormal([v1_c_n, v2_c_n, v2_b_n])); 553 | normals.element(vIndex).assign(nn); 554 | normals.element(vIndex.add(1)).assign(nn); 555 | normals.element(vIndex.add(2)).assign(nn); 556 | colors.element(vIndex).xyz.assign(col.xyz); 557 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 558 | colors.element(vIndex.add(2)).xyz.assign(dark.xyz); // fake AO at the bottom of the wall 559 | vIndex.addAssign(3); 560 | ///////////////////////////////////////// 561 | positions.element(vIndex).assign(v1_c_n); 562 | positions.element(vIndex.add(1)).assign(v2_b_n); 563 | positions.element(vIndex.add(2)).assign(v1_b_n); 564 | nn.assign(this.calcNormal([v1_c_n, v2_b_n, v1_b_n])); 565 | normals.element(vIndex).assign(nn); 566 | normals.element(vIndex.add(1)).assign(nn); 567 | normals.element(vIndex.add(2)).assign(nn); 568 | colors.element(vIndex).xyz.assign(col.xyz); 569 | colors.element(vIndex.add(1)).xyz.assign(dark.xyz); 570 | colors.element(vIndex.add(2)).xyz.assign(dark.xyz); 571 | vIndex.addAssign(3); 572 | ///////////////////////////////////////// 573 | 574 | }).elseif(points_above.equal(1), () => { 575 | 576 | // triangle cap 577 | positions.element(vIndex).assign(v3_c); 578 | positions.element(vIndex.add(1)).assign(v1_c_n); 579 | positions.element(vIndex.add(2)).assign(v2_c_n); 580 | nn.assign(this.calcNormal([v3_c, v1_c_n, v2_c_n])); 581 | normals.element(vIndex).assign(nn); 582 | normals.element(vIndex.add(1)).assign(nn); 583 | normals.element(vIndex.add(2)).assign(nn); 584 | colors.element(vIndex).xyz.assign(col.xyz); 585 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 586 | colors.element(vIndex.add(2)).xyz.assign(col.xyz); 587 | vIndex.addAssign(3); 588 | ///////////////////////////////////////// 589 | // two triangles vertical wall 590 | positions.element(vIndex).assign(v2_c_n); 591 | positions.element(vIndex.add(1)).assign(v1_c_n); 592 | positions.element(vIndex.add(2)).assign(v2_b_n); 593 | nn.assign(this.calcNormal([v2_c_n, v1_c_n, v2_b_n])); 594 | normals.element(vIndex).assign(nn); 595 | normals.element(vIndex.add(1)).assign(nn); 596 | normals.element(vIndex.add(2)).assign(nn); 597 | colors.element(vIndex).xyz.assign(col.xyz); 598 | colors.element(vIndex.add(1)).xyz.assign(col.xyz); 599 | colors.element(vIndex.add(2)).xyz.assign(dark.xyz); 600 | vIndex.addAssign(3); 601 | ///////////////////////////////////////// 602 | positions.element(vIndex).assign(v1_c_n); 603 | positions.element(vIndex.add(1)).assign(v1_b_n); 604 | positions.element(vIndex.add(2)).assign(v2_b_n); 605 | nn.assign(this.calcNormal([v1_c_n, v1_b_n, v2_b_n])); 606 | normals.element(vIndex).assign(nn); 607 | normals.element(vIndex.add(1)).assign(nn); 608 | normals.element(vIndex.add(2)).assign(nn); 609 | colors.element(vIndex).xyz.assign(col.xyz); 610 | colors.element(vIndex.add(1)).xyz.assign(dark.xyz); 611 | colors.element(vIndex.add(2)).xyz.assign(dark.xyz); 612 | vIndex.addAssign(3); 613 | ///////////////////////////////////////// 614 | }); 615 | }); 616 | }); 617 | 618 | // cleanup unused vertices 619 | loop({ type: 'int', start: vIndex, end: startIndex.add(this.maxSubdiv), condition: '<' }, ({ i }) => { 620 | positions.element(i).assign(vec4(0.0)); 621 | }); 622 | 623 | // base triangulation, must also remove comments in initMesh 624 | /* 625 | const positionStorageAttribute = storage(this.sbaVertices, 'vec4', this.sbaVertices.count); 626 | const normalStorageAttribute = storage(this.sbaNormals, 'vec4', this.sbaNormals.count); 627 | positionStorageAttribute.element(v0i).assign(v0Pos); 628 | positionStorageAttribute.element(v1i).assign(v1Pos); 629 | positionStorageAttribute.element(v2i).assign(v2Pos); 630 | 631 | nn.assign(v1Pos.sub(v0Pos).cross(v2Pos.sub(v0Pos)).normalize()); 632 | normalStorageAttribute.element(v0i).assign(nn); 633 | normalStorageAttribute.element(v1i).assign(nn); 634 | normalStorageAttribute.element(v2i).assign(nn); 635 | */ 636 | 637 | })().compute(this.nbTris); 638 | 639 | 640 | // for all side quads 641 | computeSides = tslFn(() => { 642 | 643 | const cs = float(this.cellSize); 644 | const gw = float(this.gridWidth - 1); // margin 645 | 646 | const side = instanceIndex.div(gw); 647 | const hgw = gw.mul(.5); 648 | 649 | const halfG = hgw.mul(cs); 650 | const d = instanceIndex.remainder(gw).toFloat().mul(cs); 651 | const norm = vec3(0.0, 0.0, 0.0).toVar(); 652 | const px = float(0.0).toVar(); 653 | const pz = float(0.0).toVar(); 654 | const dir = vec3(0.0).toVar(); 655 | 656 | If(side.equal(0), () => { 657 | px.assign(halfG.sub(d)); 658 | pz.assign(halfG); 659 | norm.assign(vec3(0.0, 0.0, 1.0)); 660 | dir.assign(vec3(cs.negate(), 0.0, 0.0)); 661 | }).elseif(side.equal(1), () => { 662 | px.assign(halfG); 663 | pz.assign(d.sub(halfG)); 664 | norm.assign(vec3(1.0, 0.0, 0.0)); 665 | dir.assign(vec3(0.0, 0.0, cs)); 666 | }).elseif(side.equal(2), () => { 667 | px.assign(d.sub(halfG)); 668 | pz.assign(halfG.negate()); 669 | norm.assign(vec3(0.0, 0.0, -1.0)); 670 | dir.assign(vec3(cs, 0.0, 0.0)); 671 | }).else(() => { 672 | px.assign(halfG.negate()); 673 | pz.assign(halfG.sub(d)); 674 | norm.assign(vec3(-1.0, 0.0, 0.0)); 675 | dir.assign(vec3(0.0, 0.0, cs.negate())); 676 | }); 677 | 678 | 679 | //const h1 = this.getHeight(vec3(px, 0.0, pz)).div(this.uLayerHeight).floor().mul(this.uLayerHeight).toVar(); 680 | //const h2 = this.getHeight(vec3(px, 0.0, pz).add(dir)).div(this.uLayerHeight).floor().mul(this.uLayerHeight).toVar(); 681 | // not rounding the heights looks better 682 | const h1 = this.getHeight(vec3(px, 0.0, pz)).toVar(); 683 | const h2 = this.getHeight(vec3(px, 0.0, pz).add(dir)).toVar(); 684 | const minh = min(h1, h2); 685 | const p1 = vec3(px, minh, pz); 686 | const p2 = vec3(px, minh, pz).add(dir); 687 | const p3 = vec3(px, 0.0, pz).add(dir); 688 | const p4 = vec3(px, 0.0, pz); 689 | 690 | const vIndex = instanceIndex.mul(6).toVar(); 691 | const positions = storage(this.sbaSideVertices, 'vec4', this.sbaSideVertices.count); 692 | const normals = storage(this.sbaSideNormals, 'vec4', this.sbaSideNormals.count); 693 | 694 | positions.element(vIndex).assign(p1); 695 | positions.element(vIndex.add(1)).assign(p2); 696 | positions.element(vIndex.add(2)).assign(p3); 697 | positions.element(vIndex.add(3)).assign(p1); 698 | positions.element(vIndex.add(4)).assign(p3); 699 | positions.element(vIndex.add(5)).assign(p4); 700 | 701 | loop({ type: 'int', start: vIndex, end: vIndex.add(float(6)), condition: '<' }, ({ i }) => { 702 | normals.element(i).assign(norm); 703 | }); 704 | 705 | })().compute(this.nbSideQuads); 706 | 707 | calcNormal = tslFn(([v0, v1, v2]) => { 708 | return v1.sub(v0).cross(v2.sub(v0)).normalize(); 709 | }); 710 | 711 | getLayerColor = tslFn(([layer]) => { 712 | 713 | const col = color(0.0).toVar(); 714 | If(this.uRotatePalette.equal(0), () => { 715 | col.assign(this.layerColors.element((layer.remainder(this.uNbColors)))); 716 | }).else(() => { 717 | const timer = timerGlobal(1).mul(this.uPaletteRotSpeed); 718 | const t1 = timer.floor(); 719 | const t2 = this.gain(timer.fract(), 4.0); 720 | const c1 = this.layerColors.element((layer.add(t1).remainder(this.uNbColors))); 721 | const c2 = this.layerColors.element((layer.add(t1.add(1)).remainder(this.uNbColors))); 722 | col.assign(mix(c1, c2, t2)); 723 | }); 724 | return col; 725 | }); 726 | 727 | getHeight = tslFn(([p]) => { 728 | const pointerMaxDistance = this.uCursorSize; 729 | const pointerPos = this.pointerHandler.uPointer; 730 | const dir = pointerPos.xz.sub(p.xz); 731 | const dist = min(pointerMaxDistance, dir.length()).div(pointerMaxDistance); 732 | //const dist2 = sdRoundedX(dir.rotateUV(timerGlobal(.5), vec2(0.0)), pointerMaxDistance, pointerMaxDistance.mul(0.5)).div(pointerMaxDistance).remapClamp(-1, 0, 0, 1); 733 | const timeFac = oscSine(timerGlobal(.1)).add(1.0).mul(0.5).mul(1.0).add(1.0); // some variation over time for fun 734 | const pInfluence = cond(this.uUseCursor.equal(0), 0, dist.oneMinus().mul(pointerMaxDistance.mul(.5).mul(timeFac))); 735 | 736 | const st = vec3(p.x, 0.0, p.z).mul(this.uFrequency).toVar(); 737 | 738 | st.x.mulAssign(this.uNoiseScaleX); 739 | st.z.mulAssign(this.uNoiseScaleZ); 740 | cond(this.uRotationSpeed.greaterThan(0), st.xz.rotateUVAssign(this.uScrollOffset.w, this.uScrollOffset.xz.negate()), 0); 741 | st.addAssign(this.uScrollOffset.xyz); 742 | 743 | return max(0.0, mx_fractal_noise_float(st, int(this.uOctaves), 2.0, 0.75, 0.5).add(0.5).mul(this.uNbLayers).mul(this.uLayerHeight).sub(pInfluence)); 744 | }); 745 | 746 | pcurve = tslFn(([x, a, b]) => { 747 | const k = float(pow(a.add(b), a.add(b)).div(pow(a, a).mul(pow(b, b)))); 748 | return k.mul(pow(x, a).mul(pow(sub(1.0, x), b))); 749 | }); 750 | 751 | gain = tslFn(([x, k]) => { 752 | const a = float(mul(0.5, pow(mul(2.0, cond(x.lessThan(0.5), x, sub(1.0, x))), k))).toVar(); 753 | return cond(x.lessThan(0.5), a, sub(1.0, a)); 754 | }); 755 | ////////////////////////////////////////////////////////////// 756 | 757 | update(dt: number, elapsed: number): void { 758 | 759 | this.renderer.computeAsync(this.computeTriangles); 760 | if (!this.hideWalls) this.renderer.computeAsync(this.computeSides); 761 | 762 | this.uScrollOffset.value.x += dt * this.uScrollTimeScale.value * this.uScrollSpeedX.value; 763 | this.uScrollOffset.value.y += dt * this.uScrollTimeScale.value * this.uScrollSpeedY.value; 764 | this.uScrollOffset.value.z += dt * this.uScrollTimeScale.value * this.uScrollSpeedZ.value; 765 | this.uScrollOffset.value.w += dt * this.uScrollTimeScale.value * this.uRotationSpeed.value; 766 | } 767 | 768 | } --------------------------------------------------------------------------------