├── 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 |
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 |
--------------------------------------------------------------------------------
/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 |
59 |
60 | >
61 | )
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TSL Isolines Geometry
2 |
3 | 
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
5 | );
6 |
7 |
8 | export const tools = () => (
9 |
12 | );
13 |
14 | export const brightness = () => (
15 |
18 | );
19 |
20 | export const uploadIcon = () => (
21 |
25 | );
26 |
27 | export const saveIcon = () => (
28 |
31 | );
32 |
33 | export const crosshairIcon = () => (
34 |
37 | )
38 |
39 | export const questionIcon = () => (
40 |
43 | )
44 |
45 | // #FF1F70 white
46 | export const uluLogo = () => (
47 |
56 | )
57 |
58 | export const globeIcon = () => (
59 |
62 | )
63 |
64 | // I prefer the bird!
65 | export const twitterIcon = () => (
66 |
69 | )
70 |
71 | export const githubIcon = () => (
72 |
75 | )
76 |
77 | export const fxIcon = () => (
78 |
82 | )
83 |
84 | export const cameraIcon = () => (
85 |
89 | )
90 |
91 | export const sunIcon = () => (
92 |
100 | )
101 |
102 | export const moonIcon = () => (
103 |
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 | }
--------------------------------------------------------------------------------