├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── eslint.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __screenshots__ └── box.png ├── eslint.config.js ├── examples ├── 3d-core │ ├── brush.mjs │ ├── lines.mjs │ ├── mesh.mjs │ ├── points.mjs │ ├── rect.mjs │ ├── snapshot.mjs │ ├── surface.mjs │ └── tris.mjs ├── boids │ ├── README.md │ ├── boids-cl.ts │ ├── boids-gl.ts │ ├── cl │ │ ├── bird-fs.glsl │ │ ├── bird-geometry-cl.ts │ │ ├── bird-mesh-cl.ts │ │ ├── bird-vs.glsl │ │ └── boids.cl │ ├── gl │ │ ├── bird-fs.glsl │ │ ├── bird-geometry.ts │ │ ├── bird-mesh.ts │ │ ├── bird-vs.glsl │ │ ├── position-fs.glsl │ │ └── velocity-fs.glsl │ ├── package-lock.json │ ├── package.json │ ├── screenshot.png │ └── utils │ │ ├── bird-geometry-cl.ts │ │ ├── bird-mesh-cl.ts │ │ ├── fill-data.ts │ │ ├── init-common.ts │ │ └── loop-common.ts ├── crate-lean.mjs ├── knot.mjs ├── palette │ ├── models │ │ └── LittlestTokyo.glb │ ├── palette.mjs │ ├── post.glsl │ ├── textures │ │ ├── help.png │ │ ├── help.xcf │ │ └── icon.png │ └── utils │ │ ├── DRACOLoader.mjs │ │ ├── create-color-quads.js │ │ ├── create-post-material.js │ │ ├── create-render-target.js │ │ ├── debug-shaders.js │ │ ├── palette.js │ │ └── populate-scene.js ├── pixi │ ├── index.mjs │ ├── package-lock.json │ └── package.json ├── screenshot.png ├── screenshot.xcf ├── srgb │ ├── .gitignore │ ├── main.ts │ ├── package-lock.json │ ├── package.json │ ├── simple_texture_processor.ts │ └── tsconfig.json ├── three │ ├── crate.mjs │ ├── models │ │ └── LeePerrySmith.glb │ ├── points-buffer.mjs │ ├── post.mjs │ └── textures │ │ ├── Infinite-Level_02_Tangent_SmoothUV.jpg │ │ ├── Map-COL.jpg │ │ ├── crate.gif │ │ ├── pz.jpg │ │ └── three.png ├── tsconfig.json └── utils │ ├── debug-shaders.ts │ └── perf.ts ├── index.d.ts ├── index.js ├── js ├── core │ ├── location.js │ ├── navigator.js │ ├── threejs-helpers.js │ └── vr-manager.js ├── index.js ├── math │ ├── color.js │ ├── index.js │ ├── vec2.js │ ├── vec3.js │ └── vec4.js └── objects │ ├── brush.js │ ├── cloud.js │ ├── drawable.js │ ├── index.js │ ├── lines.js │ ├── points.js │ ├── rect.js │ ├── screen.js │ ├── surface.js │ └── tris.js ├── package-lock.json ├── package.json └── test ├── compare.test.js ├── freeimage.jpg ├── index.test.js ├── init.js └── screenshot.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] ___ doesn't work" 5 | labels: bug 6 | assignees: raub 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ___ 16 | 2. ___ 17 | 3. ___ 18 | 19 | **Expected behavior** 20 | Description of what you expected to happen. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT] ____" 5 | labels: new 6 | assignees: raub 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | Description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | Description of alternative solutions or features you've considered. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Test Plan 6 | 7 | 1. 8 | 2. 9 | 3. 10 | 11 | 12 | ## Checklist 13 | 14 | - [ ] I've followed the code style. 15 | - [ ] I've tried running the code with my changes. 16 | - [ ] The docs and TS declarations are in sync with code changes. 17 | - [ ] (optional) I've added unit tests for my changes. 18 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | Eslint: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | 21 | - name: Fetch Repository 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 22.14.0 30 | cache: 'npm' 31 | 32 | - name: Install Modules 33 | run: npm ci 34 | 35 | - name: Run Lint 36 | run: npm run eslint 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | defaults: 3 | run: 4 | shell: bash 5 | 6 | on: 7 | workflow_dispatch 8 | 9 | jobs: 10 | Publish: 11 | if: contains('["raub"]', github.actor) 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | 16 | - name: Fetch Repository 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22.14.0 25 | cache: 'npm' 26 | 27 | - name: Get Package Version 28 | id: package-version 29 | run: node -p "'version='+require('./package').version" >> $GITHUB_OUTPUT 30 | 31 | - name: Publish 32 | run: | 33 | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} 34 | npm publish --ignore-scripts 35 | env: 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | 38 | - name: Create Release 39 | id: create_release 40 | uses: softprops/action-gh-release@v2 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ steps.package-version.outputs.version }} 45 | name: Release ${{ steps.package-version.outputs.version }} 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | Test: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-14] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | 21 | - name: Fetch Repository 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 22.14.0 30 | cache: 'npm' 31 | 32 | - name: Install Modules 33 | run: npm ci 34 | 35 | - name: Tests - Linux 36 | if: matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-22.04-arm' 37 | run: xvfb-run --auto-servernum npm run test-ci 38 | 39 | - uses: ssciwr/setup-mesa-dist-win@v2 40 | if: matrix.os == 'windows-2022' 41 | 42 | - name: Test - Windows 43 | shell: bash 44 | if: matrix.os == 'windows-2022' 45 | run: npm run test-ci 46 | 47 | # - name: Test - MacOS 48 | # if: matrix.os == 'macos-14' 49 | # run: npm run test-ci 50 | 51 | - uses: actions/upload-artifact@v4 52 | if: matrix.os != 'macos-14' 53 | with: 54 | name: __diff__ 55 | path: test/__diff__ 56 | retention-days: 1 57 | compression-level: 0 58 | overwrite: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cproject 2 | .idea 3 | .lock-wscript 4 | .DS_Store 5 | .project 6 | .vscode 7 | node_modules/ 8 | examples/3d-core/*.jpg 9 | examples/3d-core/*.png 10 | *.log 11 | test/__diff__ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We pledge to act and interact in ways that contribute to an open and healthy community. 4 | 5 | ## Our Standards 6 | 7 | Examples of unacceptable behavior: 8 | 9 | * The use of sexualized language or imagery 10 | * Trolling, insulting or derogatory comments 11 | * Public or private harassment 12 | * Publishing others' private information 13 | * Other unprofessional conduct 14 | 15 | ## Enforcement 16 | 17 | Community leaders will remove, edit, or reject 18 | contributions that are not aligned to this Code of Conduct. 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Bugs and enhancements are tracked as GitHub issues. 4 | 5 | ## Issues 6 | 7 | * Use a clear and descriptive title. 8 | * Describe the desired enhancement / problem. 9 | * Provide examples to demonstrate the issue. 10 | * If the problem involves a crash, provide its trace log. 11 | 12 | ## Pull Requests 13 | 14 | * Do not include issue numbers in the PR title. 15 | * Commits use the present tense (`"Add feature"` not `"Added feature"`). 16 | * Commits use the imperative mood (`"Move cursor to..."` not `"Moves cursor to..."`). 17 | * File System 18 | * Prefer kebab-lowercase (`my-dir/example-file-name.js`). 19 | * Place an empty `.keep` file to keep an empty directory. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Luis Blanco 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js 3D Core 2 | 3 | This is a part of [Node3D](https://github.com/node-3d) project. 4 | 5 | [![NPM](https://badge.fury.io/js/3d-core-raub.svg)](https://badge.fury.io/js/3d-core-raub) 6 | [![ESLint](https://github.com/node-3d/3d-core-raub/actions/workflows/eslint.yml/badge.svg)](https://github.com/node-3d/3d-core-raub/actions/workflows/eslint.yml) 7 | [![Test](https://github.com/node-3d/3d-core-raub/actions/workflows/test.yml/badge.svg)](https://github.com/node-3d/3d-core-raub/actions/workflows/test.yml) 8 | 9 | ```console 10 | npm i -s 3d-core-raub 11 | ``` 12 | 13 | > This package uses pre-compiled Node.js addons. **There is no compilation** during `npm i`. 14 | The addons are compiled for: Win64, Linux64, Linux ARM64, MacOS ARM64. 15 | 16 | ![Example](examples/screenshot.png) 17 | 18 | * WebGL/OpenGL on **Node.js** with support for web libs, such as **three.js**. 19 | * Multi-window apps, low-level window control with [glfw-raub](https://github.com/node-3d/glfw-raub). 20 | * Modern OpenGL functions also available, see [webgl-raub](https://github.com/node-3d/webgl-raub). 21 | * Image loading/saving in popular formats with [image-raub](https://github.com/node-3d/image-raub). 22 | 23 | This module exports 2 methods: 24 | 1. `export const init: (opts?: TInitOpts) => TCore3D;` 25 | 26 | Initialize Node3D. Creates the first window/document and sets up the global environment. 27 | This function can be called repeatedly, but will ignore further calls. 28 | The return value is cached and will be returned immediately for repeating calls. 29 | 2. `export const addThreeHelpers: (three: TUnknownObject, gl: typeof webgl) => void;` 30 | 31 | Teaches `three.FileLoader.load` to work with Node `fs`. Additionally implements 32 | `three.Texture.fromId` static method to create THREE textures from known GL resource IDs. 33 | 34 | 35 | See [TS declarations](/index.d.ts) for more details. 36 | 37 | ## Example 38 | 39 | (As in [crate-lean.mjs](/examples/crate-lean.mjs)): 40 | 41 | ```javascript 42 | import * as THREE from 'three'; 43 | 44 | import node3d from '../index.js'; 45 | const { init, addThreeHelpers } = node3d; 46 | 47 | const { gl, loop, Screen } = init({ 48 | isGles3: true, vsync: true, autoEsc: true, autoFullscreen: true, title: 'Crate', 49 | }); 50 | addThreeHelpers(THREE, gl); 51 | const screen = new Screen({ three: THREE, fov: 70, z: 2 }); 52 | 53 | const texture = new THREE.TextureLoader().load('three/textures/crate.gif'); 54 | texture.colorSpace = THREE.SRGBColorSpace; 55 | const geometry = new THREE.BoxGeometry(); 56 | const material = new THREE.MeshBasicMaterial({ map: texture }); 57 | const mesh = new THREE.Mesh(geometry, material); 58 | screen.scene.add(mesh); 59 | 60 | loop((now) => { 61 | mesh.rotation.x = now * 0.0005; 62 | mesh.rotation.y = now * 0.001; 63 | screen.draw(); 64 | }); 65 | ``` 66 | 67 | Example Notes: 68 | 69 | 1. You can use **mjs**, **tsx** or commonjs with `require()`. 70 | 1. `loop` is a convenience method, you can use `requestAnimationFrame` too. 71 | 1. `autoFullscreen` option enables "CTRL+F", "CTRL+SHIFT+F", "CTRL+ALT+F" to switch 72 | window modes. 73 | 1. `Screen` helps with **three.js**-oriented resource management, but is not required. 74 | 1. **three.js** uses VAO, so if not using `Screen`, handling the window mode changes 75 | (which creates a separate OpenGL context) is up to you. 76 | Basically, `doc.on('mode', () => {...})` - 77 | here you should [re-create THREE.WebGLRenderer](/js/objects/screen.js#L127). 78 | 79 | 80 | ## OpenGL Features 81 | 82 | 1. This is real **native OpenGL**, and you have direct access to GL resource IDs. This may be 83 | useful for resource sharing and compute interop: 84 | * [CUDA-GL interop](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__OPENGL.html). 85 | * [OpenCL-GL interop](https://registry.khronos.org/OpenCL/sdk/3.0/docs/man/html/clEnqueueAcquireGLObjects.html) - see [example](/examples/boids). 86 | * [Context sharing](https://www.glfw.org/docs/latest/context_guide.html#context_sharing). 87 | 1. The flag `isGles3` lets you use a **GL ES 3** preset, which is closest to "real" WebGL. 88 | If set to `false`, WebGL stuff (such as three.js) will still work, but now with some hacks. 89 | However, if you are planning to use non-WebGL features (e.g. **OpenGL 4.5** features), 90 | you might want it off, and then select a specific context version manually. 91 | 1. The flag `isWebGL2` impacts how web libraries recognize the WebGL version. 92 | But it doesn't really change the capabilities of the engine. 93 | 1. **Offscreen rendering** is possible on Windows and Linux, as demonstrated by the tests 94 | running in GitHub Actions. There are test cases that generate and compare screenshots. 95 | 1. OpenGL **context sharing** is enabled. You can obtain `HDC, HWND, CTX` for Windows and whatever 96 | those are called on Linux and MacOS. See [glfw-raub](https://github.com/node-3d/glfw-raub). 97 | 98 | 99 | ## License 100 | 101 | **You get this for free. Have fun!** 102 | 103 | Some of the components have their separate licenses, but all of them may be used 104 | commercially, without royalty. 105 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Latest major version. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Email: luisblanco1337@gmail.com. 10 | 11 | Telegram: [luisblanco_0](https://t.me/luisblanco_0) 12 | -------------------------------------------------------------------------------- /__screenshots__/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/__screenshots__/box.png -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('addon-tools-raub/utils/eslint-common'); 4 | -------------------------------------------------------------------------------- /examples/3d-core/brush.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Brush, loop } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Brush', 14 | }); 15 | 16 | const screen = new Screen({ three }); 17 | loop(() => screen.draw()); 18 | 19 | const brush = new Brush({ screen, color: 0x00FF00 }); 20 | 21 | screen.on('mousemove', (e) => brush.pos = [e.x, e.y]); 22 | -------------------------------------------------------------------------------- /examples/3d-core/lines.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Lines, loop, gl } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Lines', 14 | }); 15 | 16 | const VBO_SIZE = 10; 17 | 18 | const screen = new Screen({ three }); 19 | 20 | loop(() => screen.draw()); 21 | 22 | screen.camera.position.z = 300; 23 | 24 | 25 | const vertices1 = []; 26 | const colors1 = []; 27 | for (let i = VBO_SIZE; i > 0; i--) { 28 | vertices1.push( Math.random() * 200 - 100, Math.random() * 70 - 120, Math.random() * 200 - 100 ); 29 | colors1.push( Math.random(), Math.random(), Math.random() ); 30 | } 31 | 32 | const pos1 = gl.createBuffer(); 33 | gl.bindBuffer(gl.ARRAY_BUFFER, pos1); 34 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices1), gl.STATIC_DRAW); 35 | 36 | const rgb1 = gl.createBuffer(); 37 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb1); 38 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors1), gl.STATIC_DRAW); 39 | 40 | const lines1 = new Lines({ 41 | screen, 42 | count : VBO_SIZE, 43 | attrs : { 44 | position: { vbo: pos1, items: 3 }, 45 | color : { vbo: rgb1, items: 3 }, 46 | }, 47 | }); 48 | 49 | 50 | const vertices2 = []; 51 | const colors2 = []; 52 | for (let i = VBO_SIZE; i > 0; i--) { 53 | vertices2.push( Math.random() * 200 - 100, Math.random() * 70 - 50, Math.random() * 200 - 100 ); 54 | colors2.push( Math.random(), Math.random(), Math.random() ); 55 | } 56 | 57 | const pos2 = gl.createBuffer(); 58 | gl.bindBuffer(gl.ARRAY_BUFFER, pos2); 59 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices2), gl.STATIC_DRAW); 60 | 61 | const rgb2 = gl.createBuffer(); 62 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb2); 63 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors2), gl.STATIC_DRAW); 64 | 65 | const lines2 = new Lines({ 66 | screen, 67 | mode : 'segments', 68 | count : VBO_SIZE, 69 | attrs : { 70 | position: { vbo: pos2, items: 3 }, 71 | color: { vbo: rgb2, items: 3 }, 72 | }, 73 | }); 74 | 75 | 76 | const vertices3 = []; 77 | const colors3 = []; 78 | for (let i = VBO_SIZE; i > 0; i--) { 79 | vertices3.push( Math.random() * 200 - 100, Math.random() * 70 + 30, Math.random() * 200 - 100 ); 80 | colors3.push( Math.random(), Math.random(), Math.random() ); 81 | } 82 | 83 | const pos3 = gl.createBuffer(); 84 | gl.bindBuffer(gl.ARRAY_BUFFER, pos3); 85 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices3), gl.STATIC_DRAW); 86 | 87 | const rgb3 = gl.createBuffer(); 88 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb3); 89 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors3), gl.STATIC_DRAW); 90 | 91 | const lines3 = new Lines({ 92 | screen, 93 | mode : 'loop', 94 | count : VBO_SIZE, 95 | attrs : { 96 | position: { vbo: pos3, items: 3 }, 97 | color: { vbo: rgb3, items: 3 }, 98 | }, 99 | }); 100 | 101 | let isMoving = false; 102 | let mouse = { x: 0, y: 0 }; 103 | 104 | screen.on('mousedown', () => isMoving = true); 105 | screen.on('mouseup', () => isMoving = false); 106 | 107 | 108 | screen.on('mousemove', (e) => { 109 | const dx = mouse.x - e.x; 110 | const dy = mouse.y - e.y; 111 | 112 | mouse.x = e.x; 113 | mouse.y = e.y; 114 | 115 | if (!isMoving) { 116 | return; 117 | } 118 | 119 | lines1.mesh.rotation.y += dx * 0.001; 120 | lines1.mesh.rotation.x += dy * 0.001; 121 | 122 | lines2.mesh.rotation.y += dx * 0.001; 123 | lines2.mesh.rotation.x += dy * 0.001; 124 | 125 | lines3.mesh.rotation.y += dx * 0.001; 126 | lines3.mesh.rotation.x += dy * 0.001; 127 | }); 128 | -------------------------------------------------------------------------------- /examples/3d-core/mesh.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, loop, Image } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Mesh', 14 | }); 15 | 16 | const screen = new Screen({ three }); 17 | 18 | const icon = new Image(); 19 | icon.src = 'crate.jpg'; 20 | icon.on('load', () => { screen.icon = icon; }); 21 | 22 | const geometry = new three.IcosahedronGeometry(2, 1); 23 | const material = new three.MeshLambertMaterial({ 24 | color: 0x888888 + Math.round((0xFFFFFF - 0x888888) * Math.random()), 25 | emissive: 0x333333, 26 | }); 27 | 28 | const mesh = new three.Mesh(geometry, material); 29 | screen.scene.add( mesh ); 30 | 31 | const pointLight = new three.PointLight(0xFFFFFF, 1, 100); 32 | screen.scene.add(pointLight); 33 | pointLight.position.x = 2; 34 | pointLight.position.y = 20; 35 | pointLight.position.z = 5; 36 | 37 | 38 | loop((now) => { 39 | mesh.rotation.x = now * 0.0005; 40 | mesh.rotation.y = now * 0.001; 41 | mesh.rotation.z = now * 0.0007; 42 | screen.draw(); 43 | }); 44 | -------------------------------------------------------------------------------- /examples/3d-core/points.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Points, loop, gl } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | points: 'Points', 14 | }); 15 | 16 | const screen = new Screen({ three }); 17 | loop(() => screen.draw()); 18 | 19 | screen.camera.position.z = 200; 20 | 21 | const VBO_SIZE = 10000; 22 | 23 | const vertices = []; 24 | const colors = []; 25 | for (let i = VBO_SIZE * 3; i > 0; i--) { 26 | vertices.push( Math.random() * 200 - 100 ); 27 | colors.push( Math.random() ); 28 | } 29 | 30 | 31 | const pos = gl.createBuffer(); 32 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 33 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 34 | 35 | const rgb = gl.createBuffer(); 36 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb); 37 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 38 | 39 | 40 | const points = new Points({ 41 | screen, 42 | size : '7.0', 43 | count : VBO_SIZE, 44 | attrs : { 45 | position: { vbo: pos, items: 3 }, 46 | color: { vbo: rgb, items: 3 }, 47 | }, 48 | }); 49 | 50 | 51 | let isMoving = false; 52 | let mouse = { x: 0, y: 0 }; 53 | 54 | screen.on('mousedown', () => { isMoving = true; }); 55 | screen.on('mouseup', () => { isMoving = false; }); 56 | 57 | screen.on('mousemove', (e) => { 58 | const dx = mouse.x - e.x; 59 | const dy = mouse.y - e.y; 60 | 61 | mouse.x = e.x; 62 | mouse.y = e.y; 63 | 64 | if (!isMoving) { 65 | return; 66 | } 67 | 68 | points.mesh.rotation.y += dx * 0.001; 69 | points.mesh.rotation.x += dy * 0.001; 70 | }); 71 | -------------------------------------------------------------------------------- /examples/3d-core/rect.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Rect, loop } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Rect', 14 | }); 15 | 16 | const screen = new Screen({ three }); 17 | loop(() => screen.draw()); 18 | 19 | screen.camera.position.z = 500; 20 | 21 | const rect = new Rect({ screen }); 22 | 23 | const mouse = { x: screen.w / 2, y: screen.h / 2 }; 24 | 25 | const paint = () => { 26 | rect.mat.color.r = mouse.x / screen.w; 27 | rect.mat.color.g = mouse.y / screen.h; 28 | rect.mat.color.b = 1 - rect.mat.color.r * rect.mat.color.g; 29 | }; 30 | paint(); 31 | 32 | let isMoving = false; 33 | 34 | screen.on('mousedown', () => isMoving = true); 35 | screen.on('mouseup', () => isMoving = false); 36 | 37 | screen.on('mousemove', (e) => { 38 | const dx = mouse.x - e.x; 39 | const dy = mouse.y - e.y; 40 | 41 | mouse.x = e.x; 42 | mouse.y = e.y; 43 | 44 | paint(); 45 | 46 | if (!isMoving) { 47 | return; 48 | } 49 | 50 | rect.pos = rect.pos.plused([-dx, dy]); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/3d-core/snapshot.mjs: -------------------------------------------------------------------------------- 1 | import { screen } from './tris.mjs'; 2 | 3 | screen.title = 'Snapshot'; 4 | setTimeout(() => { 5 | const time = Date.now(); 6 | 7 | screen.snapshot(`${time}.png`); 8 | 9 | console.log(`Saved to "${time}.png".`); 10 | setTimeout(() => process.exit(0), 1000); 11 | }, 1000); 12 | -------------------------------------------------------------------------------- /examples/3d-core/surface.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Surface, Rect, Points, loop, gl } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Surface', 14 | }); 15 | 16 | const VBO_SIZE = 10000; 17 | 18 | const screen = new Screen({ three }); 19 | loop(() => screen.draw()); 20 | 21 | screen.camera.position.z = 400; 22 | 23 | const rect1 = new Rect({ screen, pos: [-500, -500], size: [1000, 1000] }); 24 | rect1.mat.color.r = 1; 25 | rect1.mat.color.g = 0; 26 | rect1.mat.color.b = 0; 27 | 28 | const surface = new Surface({ screen }); 29 | surface.camera.position.z = 400; 30 | 31 | const rect2 = new Rect({ screen: surface, pos: [-500, -500], size: [1000, 1000] }); 32 | rect2.mat.color.r = 0; 33 | rect2.mat.color.g = 1; 34 | rect2.mat.color.b = 0; 35 | 36 | const vertices = []; 37 | const colors = []; 38 | for (let i = VBO_SIZE * 3; i > 0; i--) { 39 | vertices.push( Math.random() * 600 - 300 ); 40 | colors.push( Math.random() ); 41 | } 42 | 43 | const pos = gl.createBuffer(); 44 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 45 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 46 | 47 | const rgb = gl.createBuffer(); 48 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb); 49 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 50 | 51 | const points = new Points({ 52 | screen: surface, 53 | count: VBO_SIZE, 54 | attrs: { 55 | position: { 56 | vbo: pos, 57 | items: 3, 58 | }, 59 | color: { 60 | vbo: rgb, 61 | items: 3, 62 | }, 63 | }, 64 | }); 65 | 66 | 67 | let isRotating = false; 68 | let mouse = { x: 0, y: 0 }; 69 | 70 | screen.on('mousedown', () => { isRotating = true; }); 71 | screen.on('mouseup', () => { isRotating = false; }); 72 | 73 | screen.on('mousemove', (e) => { 74 | const dx = mouse.x - e.x; 75 | const dy = mouse.y - e.y; 76 | 77 | mouse.x = e.x; 78 | mouse.y = e.y; 79 | 80 | if (!isRotating) { 81 | return; 82 | } 83 | 84 | points.mesh.rotation.y += dx * 0.001; 85 | points.mesh.rotation.x += dy * 0.001; 86 | 87 | surface.pos = surface.pos.plused([-dx, dy]); 88 | }); 89 | -------------------------------------------------------------------------------- /examples/3d-core/tris.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init } = node3d; 5 | 6 | 7 | const { Screen, Tris, loop, gl } = init({ 8 | isGles3: true, 9 | isWebGL2: true, 10 | autoEsc: true, 11 | autoFullscreen: true, 12 | vsync: true, 13 | title: 'Tris', 14 | }); 15 | 16 | export const screen = new Screen({ three }); 17 | loop(() => screen.draw()); 18 | 19 | screen.camera.position.z = 70; 20 | 21 | const VBO_SIZE = 3000; 22 | 23 | 24 | const vertices = []; 25 | const colors = []; 26 | for (let i = VBO_SIZE * 3; i > 0; i--) { 27 | vertices.push(Math.random() * 50 - 25); 28 | colors.push(Math.random()); 29 | } 30 | 31 | const pos = gl.createBuffer(); 32 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 33 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 34 | 35 | const rgb = gl.createBuffer(); 36 | gl.bindBuffer(gl.ARRAY_BUFFER, rgb); 37 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 38 | 39 | const tris = new Tris({ 40 | screen, 41 | count: VBO_SIZE, 42 | attrs: { 43 | position: { 44 | vbo: pos, 45 | items: 3, 46 | }, 47 | color: { 48 | vbo: rgb, 49 | items: 3, 50 | }, 51 | }, 52 | }); 53 | 54 | 55 | let isMoving = false; 56 | let mouse = { x: 0, y: 0 }; 57 | 58 | screen.on('mousedown', () => { isMoving = true; }); 59 | screen.on('mouseup', () => { isMoving = false; }); 60 | 61 | screen.on('mousemove', (e) => { 62 | const dx = mouse.x - e.x; 63 | const dy = mouse.y - e.y; 64 | 65 | mouse.x = e.x; 66 | mouse.y = e.y; 67 | 68 | if (!isMoving) { 69 | return; 70 | } 71 | 72 | tris.mesh.rotation.y += dx * 0.001; 73 | tris.mesh.rotation.x += dy * 0.001; 74 | }); 75 | -------------------------------------------------------------------------------- /examples/boids/README.md: -------------------------------------------------------------------------------- 1 | # Boids 2 | 3 | > Boids is an artificial life simulation originally developed by Craig Reynolds. 4 | The aim of the simulation was to replicate the behavior of flocks of birds. 5 | Instead of controlling the interactions of an entire flock, however, 6 | the Boids simulation only specifies the behavior of each individual bird. 7 | 8 | ```console 9 | npm ci 10 | npm run boids-gl 11 | npm run boids-cl 12 | ``` 13 | 14 | This example uses the package "opencl-raub", so a separate `npm ci` needs to be run here. 15 | 16 | ![Example](screenshot.png) 17 | 18 | 19 | * The original example was taken from Three.js examples 20 | [GPGPU Birds](https://github.com/mrdoob/three.js/blob/master/examples/webgl_gpgpu_birds.html). 21 | * The interop and some other notes for the OpenCL implementation were taken from this 22 | [presentation](http://web.engr.oregonstate.edu/~mjb/cs575/Handouts/opencl.opengl.vbo.1pp.pdf). 23 | * Some optimization ideas for the OpenCL demo were taken from this 24 | [guide](https://developer.download.nvidia.com/compute/DevZone/docs/html/OpenCL/doc/OpenCL_Best_Practices_Guide.pdf) 25 | 26 | The OpenCL implementation is similar to GLSL one algorithmically - i.e. the same N to N 27 | interaction is performed. This is a basic example, not a grid-based N-body. It only 28 | exists to illustrate how GLSL RTT compute can be swapped for OpenCL compute with Node3D. 29 | 30 | ## Minor changes 31 | 32 | Compared to the original Three.js example there are several edits: 33 | * Added OrbitControls - you may look around and zoom. The mouse-predator is only correct for the 34 | initial view position. 35 | * The birds are colored according to their flight direction. The background is black. 36 | * Some GLSL changes, like removing the unused variables and improving readability. 37 | * Extracted some functions and primitives into separate modules. Extracted the inline shaders. 38 | * Removed the unused attributes. Renamed some of the variables. 39 | * Bumped the number of birds from `32*32` to `128*128` (16k+). 40 | 41 | 42 | ## OpenCL notes 43 | 44 | The positions memory contains `phase` similar to the GLSL implementation. The `velocity.w` 45 | property is unused - we might as well use 3-component velocity. 46 | 47 | It is possible to use OpenGL textures in OpenCL - to directly map the GLSL compute implementation. 48 | But a more straightforward way of using shared VBOs was chosen. To implement that, the 49 | birds mesh was adjusted for instancing. The changes are mostly related to how the 50 | birds geometry is configured (see [GL](gl/bird-geometry.ts) vs [CL](cl/bird-geometry-cl.ts)). 51 | 52 | The call to `cl.enqueueAcquireGLObjects` is only performed once. That works for a combination 53 | of **Windows + nVidia**. It may turn out that on other platforms this should be called 54 | every frame. 55 | 56 | The naive implementation of N-body interaction performs similar in performance to GLSL - 57 | because they do basically the same. I.e. the same number of GPU memory reads and writes, 58 | and about the same math. But using shared/local GPU memory allows to optimize the number of 59 | memory reads. By doing so, we can observe around x2 overall performance. 60 | 61 | See [boids.cl](cl/boids.cl), where the first loop starts. The N-body interaction is split 62 | into chunks of 256 items, and that is matched by the **work group** size when launching the 63 | kernel. 64 | 1. For each iteration of the outer loop, the workgroup threads synchronize and 65 | copy 256 entries into local memory (1 entry per thread). 66 | 1. Threads synchronize again, and each thread does 256 iterations, 67 | but only reading from shared (and not **global**) memory. 68 | 1. Hence we use (the order of) N **global** reads, instead of N\*N. If N is **16384**, N\*N is **268,435,456**. 69 | Although, we still do that amount of calculations either way. 70 | -------------------------------------------------------------------------------- /examples/boids/boids-cl.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mrdoob/three.js/blob/master/examples/webgl_gpgpu_birds.html 2 | // http://web.engr.oregonstate.edu/~mjb/cs575/Handouts/opencl.opengl.vbo.1pp.pdf 3 | // https://developer.download.nvidia.com/compute/DevZone/docs/html/OpenCL/doc/OpenCL_Best_Practices_Guide.pdf 4 | 5 | import { readFileSync } from 'node:fs'; 6 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 7 | import cl from 'opencl-raub'; 8 | import { initCommon } from './utils/init-common.ts'; 9 | import { loopCommon } from './utils/loop-common.ts'; 10 | import { BirdMeshCl } from './cl/bird-mesh-cl.ts'; 11 | import { fillPositionAndPhase, fillVelocity } from './utils/fill-data.ts'; 12 | 13 | const BIRDS: number = 128 * 128; // 16384 14 | const BOUNDS: number = 800; 15 | const IS_PERF_MODE: boolean = true; 16 | 17 | const boidsSrc: string = readFileSync('cl/boids.cl').toString(); 18 | const { screen, doc, gl } = initCommon(IS_PERF_MODE, 'Boids CL'); 19 | const { platform, device } = cl.quickStart(!true); 20 | 21 | const context = cl.createContext( 22 | [ 23 | cl.GL_CONTEXT_KHR, doc.platformContext, 24 | cl.WGL_HDC_KHR, doc.platformDevice, 25 | cl.CONTEXT_PLATFORM, platform, 26 | ], 27 | [device], 28 | ); 29 | const queue = cl.createCommandQueue(context, device); 30 | 31 | const birdMesh = new BirdMeshCl(BIRDS); 32 | screen.scene.add(birdMesh); 33 | 34 | const controls = new OrbitControls(screen.camera, doc as unknown as HTMLElement); 35 | controls.update(); 36 | 37 | const { offsets, velocity } = birdMesh.vbos; 38 | fillPositionAndPhase(offsets.array, BOUNDS); 39 | gl.bindBuffer(gl.ARRAY_BUFFER, offsets.vbo); 40 | gl.bufferData(gl.ARRAY_BUFFER, offsets.array, gl.STATIC_DRAW); 41 | 42 | fillVelocity(velocity.array); 43 | gl.bindBuffer(gl.ARRAY_BUFFER, velocity.vbo); 44 | gl.bufferData(gl.ARRAY_BUFFER, velocity.array, gl.STATIC_DRAW); 45 | 46 | const memPos = cl.createFromGLBuffer(context, cl.MEM_READ_WRITE, offsets.vbo._); 47 | const memVel = cl.createFromGLBuffer(context, cl.MEM_READ_WRITE, velocity.vbo._); 48 | 49 | cl.enqueueAcquireGLObjects(queue, memPos); 50 | cl.enqueueAcquireGLObjects(queue, memVel); 51 | cl.finish(queue); 52 | 53 | // Create a program object 54 | const program = cl.createProgramWithSource(context, boidsSrc); 55 | cl.buildProgram(program, [device], `-cl-fast-relaxed-math -cl-mad-enable`); 56 | 57 | const kernelUpdate = cl.createKernel(program, 'update'); 58 | 59 | const separation = 20.0; 60 | const alignment = 20.0; 61 | const cohesion = 20.0; 62 | 63 | cl.setKernelArg(kernelUpdate, 0, 'uint', BIRDS); 64 | cl.setKernelArg(kernelUpdate, 1, 'float', 0.016); // dynamic 65 | cl.setKernelArg(kernelUpdate, 2, 'float', BOUNDS); 66 | cl.setKernelArg(kernelUpdate, 3, 'float', -10000); // dynamic 67 | cl.setKernelArg(kernelUpdate, 4, 'float', -10000); // dynamic 68 | cl.setKernelArg(kernelUpdate, 5, 'float', separation); 69 | cl.setKernelArg(kernelUpdate, 6, 'float', alignment); 70 | cl.setKernelArg(kernelUpdate, 7, 'float', cohesion); 71 | cl.setKernelArg(kernelUpdate, 8, 'float*', memPos); 72 | cl.setKernelArg(kernelUpdate, 9, 'float*', memVel); 73 | 74 | 75 | loopCommon(IS_PERF_MODE, (_now, delta, mouse) => { 76 | controls.update(); 77 | 78 | cl.setKernelArg(kernelUpdate, 1, 'float', delta); 79 | cl.setKernelArg(kernelUpdate, 3, 'float', mouse[0] * BOUNDS); 80 | cl.setKernelArg(kernelUpdate, 4, 'float', mouse[1] * BOUNDS); 81 | 82 | gl.finish(); 83 | 84 | cl.enqueueNDRangeKernel(queue, kernelUpdate, 1, null, [BIRDS], [256]); 85 | 86 | cl.finish(queue); 87 | 88 | screen.draw(); 89 | }); 90 | -------------------------------------------------------------------------------- /examples/boids/boids-gl.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mrdoob/three.js/blob/master/examples/webgl_gpgpu_birds.html 2 | 3 | import { readFileSync } from 'node:fs'; 4 | import * as THREE from 'three'; 5 | import { 6 | GPUComputationRenderer, type Variable, 7 | } from 'three/addons/misc/GPUComputationRenderer.js'; 8 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 9 | import { initCommon } from './utils/init-common.ts'; 10 | import { fillPositionAndPhase, fillVelocity } from './utils/fill-data.ts'; 11 | import { loopCommon } from './utils/loop-common.ts'; 12 | import { BirdMesh } from './gl/bird-mesh.ts'; 13 | 14 | 15 | const IS_PERF_MODE: boolean = true; 16 | const { screen, doc } = initCommon(IS_PERF_MODE, 'Boids GL'); 17 | 18 | const fragmentShaderPosition: string = readFileSync('gl/position-fs.glsl').toString(); 19 | const fragmentShaderVelocity: string = readFileSync('gl/velocity-fs.glsl').toString(); 20 | 21 | /* Texture size for simulation */ 22 | const WIDTH: number = 128; // 128^2 = 16384 birds. It ain't much, but it's honest work 23 | const BIRDS: number = WIDTH * WIDTH; 24 | 25 | const BOUNDS: number = 800; 26 | 27 | type TPositionUniforms = { 28 | time: THREE.Uniform, 29 | delta: THREE.Uniform, 30 | }; 31 | 32 | type TVelocityUniforms = TPositionUniforms & { 33 | testing: THREE.Uniform, 34 | separationDistance: THREE.Uniform, 35 | alignmentDistance: THREE.Uniform, 36 | cohesionDistance: THREE.Uniform, 37 | freedomFactor: THREE.Uniform, 38 | predator: THREE.Uniform, 39 | }; 40 | 41 | const controls = new OrbitControls(screen.camera, doc as unknown as HTMLElement); 42 | controls.update(); 43 | 44 | const gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, screen.renderer); 45 | const dtPosition = gpuCompute.createTexture(); 46 | fillPositionAndPhase(dtPosition.image.data, BOUNDS); 47 | const dtVelocity = gpuCompute.createTexture(); 48 | fillVelocity(dtVelocity.image.data); 49 | 50 | const velocityVariable: Variable = gpuCompute.addVariable( 51 | 'textureVelocity', fragmentShaderVelocity, dtVelocity, 52 | ); 53 | const positionVariable: Variable = gpuCompute.addVariable( 54 | 'texturePosition', fragmentShaderPosition, dtPosition, 55 | ); 56 | 57 | gpuCompute.setVariableDependencies(velocityVariable, [positionVariable, velocityVariable]); 58 | gpuCompute.setVariableDependencies(positionVariable, [positionVariable, velocityVariable]); 59 | 60 | const positionUniforms: TPositionUniforms = (positionVariable.material.uniforms as TPositionUniforms); 61 | const velocityUniforms: TVelocityUniforms = (velocityVariable.material.uniforms as TVelocityUniforms); 62 | 63 | positionUniforms.delta = new THREE.Uniform(0.0); 64 | 65 | velocityUniforms.delta = new THREE.Uniform(0.0) 66 | velocityUniforms.separationDistance = new THREE.Uniform(20.0); 67 | velocityUniforms.alignmentDistance = new THREE.Uniform(20.0); 68 | velocityUniforms.cohesionDistance = new THREE.Uniform(20.0); 69 | velocityUniforms.predator = new THREE.Uniform(new THREE.Vector3()); 70 | 71 | velocityVariable.material.defines['BOUNDS'] = BOUNDS.toFixed(2); 72 | velocityVariable.wrapS = THREE.RepeatWrapping; 73 | velocityVariable.wrapT = THREE.RepeatWrapping; 74 | positionVariable.wrapS = THREE.RepeatWrapping; 75 | positionVariable.wrapT = THREE.RepeatWrapping; 76 | 77 | const error = gpuCompute.init(); 78 | if (error) { 79 | console.error(error); 80 | } 81 | 82 | const birdMesh = new BirdMesh(BIRDS, WIDTH); 83 | screen.scene.add(birdMesh); 84 | 85 | loopCommon(IS_PERF_MODE, (_now, delta, mouse) => { 86 | controls.update(); 87 | 88 | positionUniforms.delta.value = delta; 89 | 90 | velocityUniforms.delta.value = delta; 91 | velocityUniforms.predator.value.set(mouse[0], mouse[1], 0); 92 | 93 | gpuCompute.compute(); 94 | 95 | birdMesh.uniforms.texturePosition.value = gpuCompute.getCurrentRenderTarget(positionVariable).texture; 96 | birdMesh.uniforms.textureVelocity.value = gpuCompute.getCurrentRenderTarget(velocityVariable).texture; 97 | 98 | screen.draw(); 99 | }); 100 | -------------------------------------------------------------------------------- /examples/boids/cl/bird-fs.glsl: -------------------------------------------------------------------------------- 1 | in vec4 vColor; 2 | in float vZ; 3 | 4 | out vec4 fragColor; 5 | 6 | uniform vec3 color; 7 | 8 | 9 | void main() { 10 | fragColor = vec4(mix(vColor.rgb, color, pow(vZ, 1.618)), 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /examples/boids/cl/bird-geometry-cl.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { type WebGLBuffer } from 'webgl-raub'; 3 | import node3d from '../../../index.js'; 4 | 5 | export type TVboInfo = { 6 | vbo: WebGLBuffer, 7 | array: Float32Array, 8 | attribute: THREE.BufferAttribute, 9 | }; 10 | 11 | export type TBirdVbos = { 12 | velocity: TVboInfo, 13 | offsets: TVboInfo, 14 | }; 15 | 16 | const createVbo = (count: number, elements: number): TVboInfo => { 17 | const { gl } = node3d.init(); 18 | const array = new Float32Array(count * elements); 19 | const vbo = gl.createBuffer(); 20 | const attribute = new THREE.GLBufferAttribute(vbo, gl.FLOAT, elements, 4, count); 21 | 22 | // HACK: instancing support 23 | const iattr = attribute as unknown as THREE.InstancedBufferAttribute; 24 | (iattr as { isInstancedBufferAttribute: boolean }).isInstancedBufferAttribute = true; 25 | iattr.meshPerAttribute = 1; 26 | 27 | return { 28 | vbo, 29 | array, 30 | attribute: iattr, 31 | }; 32 | }; 33 | 34 | 35 | // Custom Geometry - 3 triangles and instancing data. 36 | export class BirdGeometryCl extends THREE.InstancedBufferGeometry { 37 | vbos: TBirdVbos; 38 | 39 | constructor(population: number) { 40 | super(); 41 | 42 | this.instanceCount = population; 43 | 44 | const wingsSpan = 20; 45 | const vertData = [ 46 | // Body 47 | 0, -0, -wingsSpan, 48 | 0, 4, -wingsSpan, 49 | 0, 0, 30, 50 | 51 | // Wings 52 | 0, 0, -15, 53 | -wingsSpan, 0, 0, 54 | 0, 0, 15, 55 | 0, 0, 15, 56 | wingsSpan, 0, 0, 57 | 0, 0, -15, 58 | ].map(x => (x * 0.2)); 59 | const idxData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 60 | 61 | const vertices = new THREE.BufferAttribute(new Float32Array(vertData), 3); 62 | const vertidx = new THREE.BufferAttribute(new Uint32Array(idxData), 1); 63 | 64 | const velocity = createVbo(population, 4); 65 | const offsets = createVbo(population, 4); 66 | 67 | this.vbos = { velocity, offsets }; 68 | 69 | // Non-instanced part, one bird 70 | this.setAttribute('position', vertices); 71 | this.setAttribute('vertidx', vertidx); 72 | 73 | // Per-instance items 74 | this.setAttribute('velocity', velocity.attribute); 75 | this.setAttribute('offset', offsets.attribute); 76 | 77 | this.computeBoundingSphere = (() => { 78 | this.boundingSphere = new THREE.Sphere(undefined, Infinity); 79 | }); 80 | this.computeBoundingSphere(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/boids/cl/bird-mesh-cl.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import * as THREE from 'three'; 3 | import { BirdGeometryCl } from './bird-geometry-cl.ts'; 4 | 5 | const birdVS: string = readFileSync('cl/bird-vs.glsl').toString(); 6 | const birdFS: string = readFileSync('cl/bird-fs.glsl').toString(); 7 | 8 | export type TBirdUniforms = { 9 | color: THREE.Uniform, 10 | }; 11 | 12 | // Custom Mesh - BirdGeometry and some point-cloud adjustments. 13 | export class BirdMeshCl extends THREE.Mesh { 14 | get vbos() { return (this.geometry as BirdGeometryCl).vbos; } 15 | uniforms: TBirdUniforms; 16 | 17 | constructor(population: number) { 18 | const uniforms = { 19 | color: new THREE.Uniform(new THREE.Color(0)), 20 | } as const; 21 | 22 | const material = new THREE.ShaderMaterial({ 23 | vertexShader: birdVS, 24 | fragmentShader: birdFS, 25 | side: THREE.DoubleSide, 26 | forceSinglePass: true, 27 | transparent: false, 28 | uniforms, 29 | }); 30 | 31 | const geometry = new BirdGeometryCl(population); 32 | 33 | super(geometry, material); 34 | 35 | this.uniforms = uniforms; 36 | 37 | this.rotation.y = Math.PI / 2; 38 | this.matrixAutoUpdate = false; 39 | this.updateMatrix(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/boids/cl/bird-vs.glsl: -------------------------------------------------------------------------------- 1 | attribute uint vertidx; 2 | attribute vec4 offset; 3 | attribute vec4 velocity; 4 | 5 | out vec4 vColor; 6 | out float vZ; 7 | 8 | 9 | void main() { 10 | float phase = offset.w; 11 | vec3 newPosition = position; 12 | 13 | if (vertidx == 4U || vertidx == 7U) { 14 | // flap wings 15 | newPosition.y = sin(phase) * 5.0; 16 | } 17 | 18 | newPosition = mat3(modelMatrix) * newPosition; 19 | 20 | vec3 velTmp = normalize(velocity.xyz); 21 | vColor = vec4(0.5 * velTmp + vec3(0.5), 1.0); 22 | 23 | velTmp.z *= -1.0; 24 | float xz = length(velTmp.xz); 25 | float xyz = 1.0; 26 | float x = sqrt(1.0 - velTmp.y * velTmp.y); 27 | 28 | float cosry = velTmp.x / xz; 29 | float sinry = velTmp.z / xz; 30 | 31 | float cosrz = x / xyz; 32 | float sinrz = velTmp.y / xyz; 33 | 34 | mat3 maty = mat3( 35 | cosry, 0.0, -sinry, 36 | 0.0, 1.0, 0.0, 37 | sinry, 0.0, cosry 38 | ); 39 | 40 | mat3 matz = mat3( 41 | cosrz, sinrz, 0.0, 42 | -sinrz, cosrz, 0.0, 43 | 0.0, 0.0, 1.0 44 | ); 45 | 46 | newPosition = maty * matz * newPosition; 47 | newPosition += offset.xyz; 48 | 49 | vec4 projected = projectionMatrix * viewMatrix * vec4(newPosition, 1.0); 50 | vZ = projected.z * 0.0005; // z/2000 OR z/far 51 | gl_Position = projected; 52 | } 53 | -------------------------------------------------------------------------------- /examples/boids/cl/boids.cl: -------------------------------------------------------------------------------- 1 | #define CHUNK_SIZE 256U 2 | 3 | const float SPEED_LIMIT = 9.0f; 4 | const float PREY_RADIUS = 150.0f; 5 | const float PREY_RADIUS_SQ = PREY_RADIUS * PREY_RADIUS; 6 | const float M2PIF = M_PI_F * 2.0f; 7 | 8 | /** 9 | * A helper to interpret float buffers as 4D vectors. 10 | */ 11 | typedef struct { float x, y, z, w; } Xyzw; 12 | 13 | /** 14 | * Update birds. 15 | */ 16 | __kernel 17 | void update( 18 | const uint count, 19 | const float dt, 20 | const float BOUNDS, 21 | const float predatorX, 22 | const float predatorY, 23 | const float separation, 24 | const float alignment, 25 | const float cohesion, 26 | __global Xyzw* ioPosition, 27 | __global Xyzw* ioVelocity 28 | ) { 29 | uint i = get_global_id(0); 30 | if (i >= count) { 31 | return; 32 | } 33 | 34 | float3 velocity = (float3)(ioVelocity[i].x, ioVelocity[i].y, ioVelocity[i].z); 35 | float3 position = (float3)(ioPosition[i].x, ioPosition[i].y, ioPosition[i].z); 36 | float phase = ioPosition[i].w; 37 | 38 | float3 predator = (float3)(predatorX, predatorY, 0.f); 39 | float zoneRadius = separation + alignment + cohesion; 40 | float separationThresh = separation / zoneRadius; 41 | float alignmentThresh = (separation + alignment) / zoneRadius; 42 | float zoneRadiusSquared = zoneRadius * zoneRadius; 43 | float separationSquared = separation * separation; 44 | float cohesionSquared = cohesion * cohesion; 45 | float limit = SPEED_LIMIT; 46 | 47 | float dist; 48 | float3 dir; // direction 49 | float distSquared; 50 | float f; 51 | float percent; 52 | 53 | dir = predator - position; 54 | dir.z = 0.0f; 55 | distSquared = dot(dir, dir); 56 | 57 | // move birds away from predator 58 | if (distSquared < PREY_RADIUS_SQ) { 59 | f = (distSquared / PREY_RADIUS_SQ - 1.f) * dt * 100.f; 60 | velocity += normalize(dir) * f; 61 | limit += 5.0f; 62 | } 63 | 64 | // Attract flocks to the center 65 | dir = position; 66 | dir.y *= 2.5f; 67 | velocity -= normalize(dir) * dt * 5.f; 68 | 69 | // Optimize memory access with chunked local groups 70 | int chunkCount = count / CHUNK_SIZE; 71 | uint iLocal = get_local_id(0); 72 | __local float3 positionLocal[CHUNK_SIZE]; 73 | __local float3 velocityLocal[CHUNK_SIZE]; 74 | 75 | // For each chunk: 76 | // - load N positions and velocities into local 77 | // - then work on those local N from every thread 78 | for (uint k = 0U; k < chunkCount; k++) { 79 | barrier(CLK_LOCAL_MEM_FENCE); 80 | 81 | uint idx = k * CHUNK_SIZE + iLocal; 82 | float3 otherPosition = (float3)(ioPosition[idx].x, ioPosition[idx].y, ioPosition[idx].z); 83 | float3 otherVelocity = (float3)(ioVelocity[idx].x, ioVelocity[idx].y, ioVelocity[idx].z); 84 | positionLocal[iLocal] = otherPosition; 85 | velocityLocal[iLocal] = otherVelocity; 86 | 87 | barrier(CLK_LOCAL_MEM_FENCE); 88 | 89 | for (uint j = 0U; j < CHUNK_SIZE; j++) { 90 | float3 birdPosition = positionLocal[j]; 91 | float3 birdVelocity = velocityLocal[j]; 92 | dir = birdPosition - position; 93 | distSquared = dot(dir, dir); 94 | 95 | if (distSquared < 0.0001f || distSquared > zoneRadiusSquared) { 96 | continue; 97 | } 98 | 99 | dist = half_sqrt(distSquared); 100 | percent = distSquared / zoneRadiusSquared; 101 | 102 | if (percent < separationThresh) { // low 103 | // Separation - Move apart for comfort 104 | f = (separationThresh / percent - 1.0f) * dt; 105 | velocity -= dir * f / dist; 106 | continue; 107 | } 108 | 109 | if (percent < alignmentThresh) { // high 110 | // Alignment - fly the same direction 111 | float threshDelta = alignmentThresh - separationThresh; 112 | float adjustedPercent = (percent - separationThresh) / threshDelta; 113 | 114 | f = (0.5f - cos(adjustedPercent * M2PIF) * 0.5f + 0.5f) * dt; 115 | velocity += normalize(birdVelocity) * f; 116 | continue; 117 | } 118 | 119 | // Attraction / Cohesion - move closer 120 | float threshDelta = 1.0f - alignmentThresh; 121 | float adjustedPercent = 1.f; 122 | if (threshDelta >= 0.0001f) { 123 | adjustedPercent = (percent - alignmentThresh) / threshDelta; 124 | } 125 | 126 | f = (0.5f - (cos(adjustedPercent * M2PIF) * -0.5f + 0.5f)) * dt; 127 | velocity += dir * f / dist; 128 | } 129 | } 130 | 131 | // Speed Limits 132 | if (length(velocity) > limit) { 133 | velocity = normalize(velocity) * limit; 134 | } 135 | 136 | ioVelocity[i].x = velocity.x; 137 | ioVelocity[i].y = velocity.y; 138 | ioVelocity[i].z = velocity.z; 139 | 140 | position += velocity * dt * 15.f; 141 | phase = fmod( 142 | phase + dt + length(velocity.xz) * dt * 3.f + max(velocity.y, 0.f) * dt * 6.f, 143 | M_PI_F * 20.f 144 | ); 145 | 146 | ioPosition[i].x = position.x; 147 | ioPosition[i].y = position.y; 148 | ioPosition[i].z = position.z; 149 | ioPosition[i].w = phase; 150 | } 151 | -------------------------------------------------------------------------------- /examples/boids/gl/bird-fs.glsl: -------------------------------------------------------------------------------- 1 | in vec4 vColor; 2 | in float vZ; 3 | 4 | out vec4 fragColor; 5 | 6 | uniform vec3 color; 7 | 8 | 9 | void main() { 10 | fragColor = vec4(mix(vColor.rgb, color, pow(vZ, 1.618)), 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /examples/boids/gl/bird-geometry.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mrdoob/three.js/blob/master/examples/webgl_gpgpu_birds.html 2 | 3 | import * as THREE from 'three'; 4 | 5 | // Custom Geometry - using 3 triangles each. No UVs, no normals currently. 6 | export class BirdGeometry extends THREE.BufferGeometry { 7 | constructor(population: number, group: number) { 8 | super(); 9 | 10 | const trianglesPerBird = 3; 11 | const triangles = population * trianglesPerBird; 12 | const points = triangles * 3; 13 | 14 | const vertices = new THREE.BufferAttribute(new Float32Array(points * 3), 3); 15 | const birdColors = new THREE.BufferAttribute(new Float32Array(points * 3), 3); 16 | const references = new THREE.BufferAttribute(new Float32Array(points * 2), 2); 17 | const birdVertex = new THREE.BufferAttribute(new Float32Array(points), 1); 18 | 19 | this.setAttribute('position', vertices); 20 | this.setAttribute('birdColor', birdColors); 21 | this.setAttribute('reference', references); 22 | this.setAttribute('birdVertex', birdVertex); 23 | 24 | // this.setAttribute('normal', new Float32Array(points * 3), 3); 25 | 26 | let v = 0; 27 | const verts_push = (...args: number[]) => { 28 | for (let i = 0; i < args.length; i++) { 29 | vertices.array[v++] = args[i]; 30 | } 31 | }; 32 | 33 | const wingsSpan = 20; 34 | 35 | for (let f = 0; f < population; f++) { 36 | // Body 37 | verts_push( 38 | 0, -0, -20, 39 | 0, 4, -20, 40 | 0, 0, 30, 41 | ); 42 | 43 | // Wings 44 | verts_push( 45 | 0, 0, -15, 46 | -wingsSpan, 0, 0, 47 | 0, 0, 15, 48 | ); 49 | verts_push( 50 | 0, 0, 15, 51 | wingsSpan, 0, 0, 52 | 0, 0, -15, 53 | ); 54 | } 55 | 56 | for (let v = 0; v < points; v++) { 57 | const triangleIndex = ~ ~ (v / 3); 58 | const birdIndex = ~ ~ (triangleIndex / trianglesPerBird); 59 | const x = (birdIndex % group) / group; 60 | const y = ~ ~ (birdIndex / group) / group; 61 | 62 | const c = new THREE.Color( 63 | 0x666666 + 64 | ~ ~ (v / 9) / population * 0x666666 65 | ); 66 | 67 | birdColors.array[v * 3 + 0] = c.r; 68 | birdColors.array[v * 3 + 1] = c.g; 69 | birdColors.array[v * 3 + 2] = c.b; 70 | 71 | references.array[v * 2] = x; 72 | references.array[v * 2 + 1] = y; 73 | 74 | birdVertex.array[v] = v % 9; 75 | } 76 | 77 | this.scale(0.2, 0.2, 0.2); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/boids/gl/bird-mesh.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import * as THREE from 'three'; 3 | import { BirdGeometry } from './bird-geometry.ts'; 4 | 5 | const birdVS: string = readFileSync('gl/bird-vs.glsl').toString(); 6 | const birdFS: string = readFileSync('gl/bird-fs.glsl').toString(); 7 | 8 | export type TBirdUniforms = { 9 | texturePosition: THREE.Uniform, 10 | textureVelocity: THREE.Uniform, 11 | }; 12 | 13 | // Custom Mesh - BirdGeometry and some point-cloud adjustments. 14 | export class BirdMesh extends THREE.Mesh { 15 | uniforms: TBirdUniforms; 16 | 17 | constructor(population: number, group: number) { 18 | const uniforms = { 19 | texturePosition: { value: null } as THREE.Uniform, 20 | textureVelocity: { value: null } as THREE.Uniform, 21 | } as const; 22 | 23 | const material = new THREE.ShaderMaterial({ 24 | uniforms, 25 | vertexShader: birdVS, 26 | fragmentShader: birdFS, 27 | side: THREE.DoubleSide, 28 | forceSinglePass: true, 29 | transparent: false, 30 | }); 31 | 32 | const geometry = new BirdGeometry(population, group); 33 | 34 | super(geometry, material); 35 | 36 | this.uniforms = uniforms; 37 | 38 | this.rotation.y = Math.PI / 2; 39 | this.matrixAutoUpdate = false; 40 | this.updateMatrix(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/boids/gl/bird-vs.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 reference; 2 | attribute float birdVertex; 3 | 4 | uniform sampler2D texturePosition; 5 | uniform sampler2D textureVelocity; 6 | 7 | out vec4 vColor; 8 | out float vZ; 9 | 10 | 11 | void main() { 12 | vec4 tmpPos = texture2D(texturePosition, reference); 13 | vec3 pos = tmpPos.xyz; 14 | vec3 velocity = normalize(texture2D(textureVelocity, reference).xyz); 15 | vColor = vec4(0.5 * velocity + vec3(0.5), 1.0); 16 | 17 | vec3 newPosition = position; 18 | 19 | if (birdVertex == 4.0 || birdVertex == 7.0) { 20 | // flap wings 21 | newPosition.y = sin(tmpPos.w) * 5.0; 22 | } 23 | 24 | newPosition = mat3(modelMatrix) * newPosition; 25 | 26 | velocity.z *= -1.0; 27 | float xz = length(velocity.xz); 28 | float x = sqrt(1.0 - velocity.y * velocity.y); 29 | 30 | float cosry = velocity.x / xz; 31 | float sinry = velocity.z / xz; 32 | 33 | float cosrz = x; 34 | float sinrz = velocity.y; 35 | 36 | mat3 maty = mat3( 37 | cosry, 0, -sinry, 38 | 0, 1, 0 , 39 | sinry, 0, cosry 40 | ); 41 | 42 | mat3 matz = mat3( 43 | cosrz, sinrz, 0, 44 | -sinrz, cosrz, 0, 45 | 0 , 0, 1 46 | ); 47 | 48 | newPosition = maty * matz * newPosition; 49 | newPosition += pos; 50 | 51 | vec4 projected = projectionMatrix * viewMatrix * vec4(newPosition, 1.0); 52 | vZ = projected.z * 0.0005; // z/2000 OR z/far 53 | gl_Position = projected; 54 | } 55 | -------------------------------------------------------------------------------- /examples/boids/gl/position-fs.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform float delta; 3 | out vec4 fragColor; 4 | 5 | void main() { 6 | vec2 uv = gl_FragCoord.xy / resolution.xy; 7 | vec4 tmpPos = texture2D(texturePosition, uv); 8 | vec3 position = tmpPos.xyz; 9 | vec3 velocity = texture2D(textureVelocity, uv).xyz; 10 | 11 | float phase = tmpPos.w; 12 | 13 | phase = mod( 14 | phase + delta + length(velocity.xz) * delta * 3. + max(velocity.y, 0.0) * delta * 6.0, 15 | 62.83 16 | ); 17 | 18 | fragColor = vec4(position + velocity * delta * 15. , phase); 19 | } 20 | -------------------------------------------------------------------------------- /examples/boids/gl/velocity-fs.glsl: -------------------------------------------------------------------------------- 1 | out vec4 fragColor; 2 | 3 | uniform float time; 4 | uniform float testing; 5 | uniform float delta; // about 0.016 6 | uniform float separationDistance; // 20 7 | uniform float alignmentDistance; // 40 8 | uniform float cohesionDistance; // 9 | uniform float freedomFactor; 10 | uniform vec3 predator; 11 | 12 | const float width = resolution.x; 13 | const float height = resolution.y; 14 | 15 | const float PI = 3.141592653589793; 16 | const float PI_2 = PI * 2.0; 17 | // const float VISION = PI * 0.55; 18 | 19 | float zoneRadius = 40.0; 20 | float zoneRadiusSquared = 1600.0; 21 | 22 | float separationThresh = 0.45; 23 | float alignmentThresh = 0.65; 24 | 25 | const float UPPER_BOUNDS = BOUNDS; 26 | const float LOWER_BOUNDS = -UPPER_BOUNDS; 27 | const float SPEED_LIMIT = 9.0; 28 | 29 | void main() { 30 | zoneRadius = separationDistance + alignmentDistance + cohesionDistance; 31 | separationThresh = separationDistance / zoneRadius; 32 | alignmentThresh = (separationDistance + alignmentDistance) / zoneRadius; 33 | zoneRadiusSquared = zoneRadius * zoneRadius; 34 | 35 | vec2 uv = gl_FragCoord.xy / resolution.xy; 36 | vec3 birdPosition, birdVelocity; 37 | 38 | vec3 selfPosition = texture2D(texturePosition, uv).xyz; 39 | vec3 selfVelocity = texture2D(textureVelocity, uv).xyz; 40 | 41 | float dist; 42 | vec3 dir; // direction 43 | float distSquared; 44 | 45 | float separationSquared = separationDistance * separationDistance; 46 | float cohesionSquared = cohesionDistance * cohesionDistance; 47 | 48 | float f; 49 | float percent; 50 | 51 | vec3 velocity = selfVelocity; 52 | 53 | float limit = SPEED_LIMIT; 54 | 55 | dir = predator * UPPER_BOUNDS - selfPosition; 56 | dir.z = 0.0; 57 | dist = length(dir); 58 | distSquared = dist * dist; 59 | 60 | float preyRadius = 150.0; 61 | float preyRadiusSq = preyRadius * preyRadius; 62 | 63 | // move birds away from predator 64 | if (dist < preyRadius) { 65 | f = (distSquared / preyRadiusSq - 1.0) * delta * 100.; 66 | velocity += normalize(dir) * f; 67 | limit += 5.0; 68 | } 69 | 70 | // Attract flocks to the center 71 | vec3 central = vec3(0., 0., 0.); 72 | dir = selfPosition - central; 73 | dist = length(dir); 74 | 75 | dir.y *= 2.5; 76 | velocity -= normalize(dir) * delta * 5.; 77 | 78 | for (float y = 0.0; y < height; y++) { 79 | for (float x = 0.0; x < width; x++) { 80 | vec2 ref = vec2(x + 0.5, y + 0.5) / resolution.xy; 81 | birdPosition = texture2D(texturePosition, ref).xyz; 82 | 83 | dir = birdPosition - selfPosition; 84 | dist = length(dir); 85 | 86 | if (dist < 0.0001) continue; 87 | 88 | distSquared = dist * dist; 89 | 90 | if (distSquared > zoneRadiusSquared) continue; 91 | 92 | percent = distSquared / zoneRadiusSquared; 93 | 94 | if (percent < separationThresh) { // low 95 | // Separation - Move apart for comfort 96 | f = (separationThresh / percent - 1.0) * delta; 97 | velocity -= normalize(dir) * f; 98 | } else if (percent < alignmentThresh) { // high 99 | // Alignment - fly the same direction 100 | float threshDelta = alignmentThresh - separationThresh; 101 | float adjustedPercent = (percent - separationThresh) / threshDelta; 102 | 103 | birdVelocity = texture2D(textureVelocity, ref).xyz; 104 | 105 | f = (0.5 - cos(adjustedPercent * PI_2) * 0.5 + 0.5) * delta; 106 | velocity += normalize(birdVelocity) * f; 107 | } else { 108 | // Attraction / Cohesion - move closer 109 | float threshDelta = 1.0 - alignmentThresh; 110 | float adjustedPercent; 111 | if(threshDelta == 0.) adjustedPercent = 1.; 112 | else adjustedPercent = (percent - alignmentThresh) / threshDelta; 113 | 114 | f = (0.5 - (cos(adjustedPercent * PI_2) * -0.5 + 0.5)) * delta; 115 | 116 | velocity += normalize(dir) * f; 117 | } 118 | } 119 | } 120 | 121 | // Speed Limits 122 | if (length(velocity) > limit) { 123 | velocity = normalize(velocity) * limit; 124 | } 125 | 126 | gl_FragColor = vec4(velocity, 1.0); 127 | } 128 | -------------------------------------------------------------------------------- /examples/boids/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "example", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "opencl-raub": "^2.0.1" 12 | } 13 | }, 14 | "node_modules/addon-tools-raub": { 15 | "version": "9.3.0", 16 | "resolved": "https://registry.npmjs.org/addon-tools-raub/-/addon-tools-raub-9.3.0.tgz", 17 | "integrity": "sha512-YZnTOK7KbMnIZOyOtAo/TOQ80XfYgL8oYhNAi2qlrVV+YduOA+VWBixD3+vybjQvWLJOhWvDIue/QEjkROVPKA==", 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=22.14.0", 21 | "npm": ">=10.9.2" 22 | }, 23 | "peerDependencies": { 24 | "node-addon-api": "^8.3.0" 25 | }, 26 | "peerDependenciesMeta": { 27 | "node-addon-api": { 28 | "optional": true 29 | } 30 | } 31 | }, 32 | "node_modules/opencl-raub": { 33 | "version": "2.0.1", 34 | "resolved": "https://registry.npmjs.org/opencl-raub/-/opencl-raub-2.0.1.tgz", 35 | "integrity": "sha512-79KMwdSpO8yfd6Be4sJVn+rHHI5dan4Z4n1QX3jrGYPTnDYkKE5ufj6Xf7IF/PxB7+iC9PNAU1VXzbDKow/6TQ==", 36 | "hasInstallScript": true, 37 | "license": "MIT", 38 | "dependencies": { 39 | "addon-tools-raub": "^9.3.0", 40 | "segfault-raub": "^3.2.0" 41 | }, 42 | "engines": { 43 | "node": ">=22.14.0", 44 | "npm": ">=10.9.2" 45 | } 46 | }, 47 | "node_modules/segfault-raub": { 48 | "version": "3.2.0", 49 | "resolved": "https://registry.npmjs.org/segfault-raub/-/segfault-raub-3.2.0.tgz", 50 | "integrity": "sha512-Q1f3K6fAS3lvZwzkxn9bdljCCjZBSAV18lHon8XMAFuwOIicqbKf67v+Bzo9Nof68wlVzKePCi7pXoLegT/YYw==", 51 | "hasInstallScript": true, 52 | "license": "MIT, BSD-3-Clause, BSD-2-Clause", 53 | "dependencies": { 54 | "addon-tools-raub": "^9.3.0" 55 | }, 56 | "engines": { 57 | "node": ">=22.14.0", 58 | "npm": ">=10.9.2" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/boids/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "boids-gl": "node --no-warnings=ExperimentalWarning --experimental-strip-types boids-gl.ts", 8 | "boids-cl": "node --no-warnings=ExperimentalWarning --experimental-strip-types boids-cl.ts" 9 | }, 10 | "dependencies": { 11 | "opencl-raub": "^2.0.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/boids/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/boids/screenshot.png -------------------------------------------------------------------------------- /examples/boids/utils/bird-geometry-cl.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { type WebGLBuffer } from 'webgl-raub'; 3 | import node3d from '../../../index.js'; 4 | 5 | export type TVboInfo = { 6 | vbo: WebGLBuffer, 7 | array: Float32Array, 8 | attribute: THREE.BufferAttribute, 9 | }; 10 | 11 | export type TBirdVbos = { 12 | velocity: TVboInfo, 13 | offsets: TVboInfo, 14 | }; 15 | 16 | const createVbo = (count: number, elements: number): TVboInfo => { 17 | const { gl } = node3d.init(); 18 | const array = new Float32Array(count * elements); 19 | const vbo = gl.createBuffer(); 20 | const attribute = new THREE.GLBufferAttribute(vbo, gl.FLOAT, elements, 4, count); 21 | 22 | // HACK: instancing support 23 | const iattr = attribute as unknown as THREE.InstancedBufferAttribute; 24 | (iattr as { isInstancedBufferAttribute: boolean }).isInstancedBufferAttribute = true; 25 | iattr.meshPerAttribute = 1; 26 | 27 | return { 28 | vbo, 29 | array, 30 | attribute: iattr, 31 | }; 32 | }; 33 | 34 | 35 | // Custom Geometry - 3 triangles and instancing data. 36 | export class BirdGeometryCl extends THREE.InstancedBufferGeometry { 37 | vbos: TBirdVbos; 38 | 39 | constructor(population: number) { 40 | super(); 41 | 42 | this.instanceCount = population; 43 | 44 | const wingsSpan = 20; 45 | const vertData = [ 46 | // Body 47 | 0, -0, -wingsSpan, 48 | 0, 4, -wingsSpan, 49 | 0, 0, 30, 50 | 51 | // Wings 52 | 0, 0, -15, 53 | -wingsSpan, 0, 0, 54 | 0, 0, 15, 55 | 0, 0, 15, 56 | wingsSpan, 0, 0, 57 | 0, 0, -15, 58 | ].map(x => (x * 0.2)); 59 | const idxData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 60 | 61 | const vertices = new THREE.BufferAttribute(new Float32Array(vertData), 3); 62 | const vertidx = new THREE.BufferAttribute(new Uint32Array(idxData), 1); 63 | 64 | const velocity = createVbo(population, 4); 65 | const offsets = createVbo(population, 4); 66 | 67 | this.vbos = { velocity, offsets }; 68 | 69 | // Non-instanced part, one bird 70 | this.setAttribute('position', vertices); 71 | this.setAttribute('vertidx', vertidx); 72 | 73 | // Per-instance items 74 | this.setAttribute('velocity', velocity.attribute); 75 | this.setAttribute('offset', offsets.attribute); 76 | 77 | this.computeBoundingSphere = (() => { 78 | this.boundingSphere = new THREE.Sphere(undefined, Infinity); 79 | }); 80 | this.computeBoundingSphere(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/boids/utils/bird-mesh-cl.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import * as THREE from 'three'; 3 | import { BirdGeometryCl } from './bird-geometry-cl.ts'; 4 | 5 | const birdVS: string = readFileSync('cl/bird-vs.glsl').toString(); 6 | const birdFS: string = readFileSync('cl/bird-fs.glsl').toString(); 7 | 8 | export type TBirdUniforms = { 9 | color: THREE.Uniform, 10 | }; 11 | 12 | // Custom Mesh - BirdGeometry and some point-cloud adjustments. 13 | export class BirdMeshCl extends THREE.Mesh { 14 | get vbos() { return (this.geometry as BirdGeometryCl).vbos; } 15 | uniforms: TBirdUniforms; 16 | 17 | constructor(population: number) { 18 | const uniforms = { 19 | color: new THREE.Uniform(new THREE.Color(0)), 20 | } as const; 21 | 22 | const material = new THREE.ShaderMaterial({ 23 | vertexShader: birdVS, 24 | fragmentShader: birdFS, 25 | side: THREE.DoubleSide, 26 | forceSinglePass: true, 27 | transparent: false, 28 | uniforms, 29 | }); 30 | 31 | const geometry = new BirdGeometryCl(population); 32 | 33 | super(geometry, material); 34 | 35 | this.uniforms = uniforms; 36 | 37 | this.rotation.y = Math.PI / 2; 38 | this.matrixAutoUpdate = false; 39 | this.updateMatrix(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/boids/utils/fill-data.ts: -------------------------------------------------------------------------------- 1 | export const randBound = (extents: number = 1) => { 2 | const half = extents * 0.5; 3 | return [ 4 | Math.random() * extents - half, 5 | Math.random() * extents - half, 6 | Math.random() * extents - half, 7 | ]; 8 | }; 9 | 10 | export const fillPositionAndPhase = (data: ArrayBufferView, extents: number) => { 11 | const asF32 = new Float32Array(data.buffer); 12 | const len = asF32.length; 13 | 14 | for (let i = 0; i < len; i += 4) { 15 | const [x, y, z] = randBound(extents); 16 | asF32[i + 0] = x; 17 | asF32[i + 1] = y; 18 | asF32[i + 2] = z; 19 | asF32[i + 3] = 1; 20 | } 21 | }; 22 | 23 | export const fillVelocity = (data: ArrayBufferView) => { 24 | const asF32 = new Float32Array(data.buffer); 25 | const len = asF32.length; 26 | 27 | for (let i = 0; i < len; i += 4) { 28 | const [x, y, z] = randBound(1); 29 | asF32[i + 0] = x * 10; 30 | asF32[i + 1] = y * 10; 31 | asF32[i + 2] = z * 10; 32 | asF32[i + 3] = 1; 33 | } 34 | }; 35 | 36 | export const fillColorBuffer = (array: Float32Array): void => { 37 | const len = array.length; 38 | 39 | for (let i = 0; i < len; i += 3) { 40 | const c = 0.4 + 0.4 * (i / len); 41 | array[i + 0] = c; 42 | array[i + 1] = c; 43 | array[i + 2] = c; 44 | array[i + 3] = 1; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /examples/boids/utils/init-common.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import node3d, { type Screen, type TCore3D } from '../../../index.js'; 3 | import { debugShaders } from '../../utils/debug-shaders.ts'; 4 | 5 | const { init, addThreeHelpers } = node3d; 6 | 7 | type TInitResult = { 8 | doc: TCore3D['doc'], 9 | screen: Screen, 10 | loop: TCore3D['loop'], 11 | gl: TCore3D['gl'], 12 | }; 13 | 14 | export const initCommon = (isPerf: boolean, title: string): TInitResult => { 15 | const { 16 | doc, gl, Screen, loop, 17 | } = init({ 18 | isGles3: true, 19 | isWebGL2: true, 20 | autoEsc: true, 21 | autoFullscreen: true, 22 | title, 23 | vsync: !isPerf, 24 | }); 25 | addThreeHelpers(THREE, gl); 26 | 27 | const screen = new Screen({ three: THREE, fov: 75, near: 1, far: 2000 }); 28 | screen.camera.position.z = 350; 29 | 30 | debugShaders(screen.renderer, true); 31 | 32 | // screen.scene.background = new THREE.Color(0x87ceeb); 33 | // screen.scene.background = new THREE.Color(0xffffff); 34 | // screen.scene.background = new THREE.Color(0x0); 35 | // screen.scene.fog = new THREE.Fog(0x87ceeb, 100, 1000); 36 | 37 | return { 38 | doc, screen, loop, gl, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /examples/boids/utils/loop-common.ts: -------------------------------------------------------------------------------- 1 | import { type TMouseMoveEvent } from 'glfw-raub'; 2 | import node3d from '../../../index.js'; 3 | import { countFrame } from '../../utils/perf.ts'; 4 | 5 | type TCbLoop = (now: number, dt: number, mouse: [number, number]) => void; 6 | 7 | export const loopCommon = (isPerf: boolean, cb: TCbLoop) => { 8 | const { doc, loop } = node3d.init(); 9 | 10 | let mouseX = -10000; 11 | let mouseY = -10000; 12 | 13 | doc.on('mousemove', (event) => { 14 | const windowHalfX = window.innerWidth / 2; 15 | const windowHalfY = window.innerHeight / 2; 16 | mouseX = (event as TMouseMoveEvent).clientX - windowHalfX; 17 | mouseY = (event as TMouseMoveEvent).clientY - windowHalfY; 18 | }); 19 | 20 | let last: number = performance.now(); 21 | 22 | return loop((now: number) => { 23 | let delta = (now - last) / 1000; 24 | 25 | if (delta > 0.1) { 26 | delta = 0.1; // safety cap on large deltas 27 | } 28 | last = now; 29 | 30 | let windowHalfX = window.innerWidth / 2; 31 | let windowHalfY = window.innerHeight / 2; 32 | 33 | cb(now, delta, [0.5 * mouseX / windowHalfX, - 0.5 * mouseY / windowHalfY]); 34 | 35 | if (isPerf) { 36 | countFrame(now); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/crate-lean.mjs: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import node3d from '../index.js'; 4 | const { init, addThreeHelpers } = node3d; 5 | 6 | const { gl, loop, Screen } = init({ 7 | isGles3: true, vsync: true, autoEsc: true, autoFullscreen: true, title: 'Crate', 8 | }); 9 | addThreeHelpers(THREE, gl); 10 | const screen = new Screen({ three: THREE, fov: 70, z: 2 }); 11 | 12 | const texture = new THREE.TextureLoader().load('three/textures/crate.gif'); 13 | texture.colorSpace = THREE.SRGBColorSpace; 14 | const geometry = new THREE.BoxGeometry(); 15 | const material = new THREE.MeshBasicMaterial({ map: texture }); 16 | const mesh = new THREE.Mesh(geometry, material); 17 | screen.scene.add(mesh); 18 | 19 | loop((now) => { 20 | mesh.rotation.x = now * 0.0005; 21 | mesh.rotation.y = now * 0.001; 22 | screen.draw(); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/knot.mjs: -------------------------------------------------------------------------------- 1 | // Init Node3D environment 2 | import * as THREE from 'THREE'; 3 | import node3d from '../index.js'; 4 | const { init, addThreeHelpers } = node3d; 5 | const { doc, gl, loop, Screen } = init({ 6 | isGles3: true, 7 | // isGles3: false, // - works too 8 | vsync: true, 9 | autoEsc: true, 10 | autoFullscreen: true, 11 | title: 'Knot', 12 | }); 13 | addThreeHelpers(THREE, gl); 14 | 15 | // Three.js rendering setup 16 | const scene = new THREE.Scene(); 17 | const camera = new THREE.PerspectiveCamera(70, doc.w / doc.h, 0.2, 500); 18 | camera.position.z = 35; 19 | scene.background = new THREE.Color(0x333333); 20 | const screen = new Screen({ THREE: THREE, camera, scene }); 21 | 22 | // Add scene lights 23 | scene.add(new THREE.AmbientLight(0xc1c1c1, 0.5)); 24 | const sun = new THREE.DirectionalLight(0xffffff, 2); 25 | sun.position.set(-1, 0.5, 1); 26 | scene.add(sun); 27 | 28 | // Original knot mesh 29 | const knotGeometry = new THREE.TorusKnotGeometry(10, 1.85, 256, 20, 2, 7); 30 | const knotMaterial = new THREE.MeshToonMaterial({ color: 0x6cc24a }); 31 | const knotMesh = new THREE.Mesh(knotGeometry, knotMaterial); 32 | scene.add(knotMesh); 33 | 34 | // A slightly larger knot mesh, inside-out black - for outline 35 | const outlineGeometry = new THREE.TorusKnotGeometry(10, 2, 256, 20, 2, 7); 36 | const outlineMaterial = new THREE.MeshBasicMaterial({ color: 0, side: THREE.BackSide }); 37 | const outlineMesh = new THREE.Mesh(outlineGeometry, outlineMaterial); 38 | knotMesh.add(outlineMesh); 39 | 40 | // Called repeatedly to render new frames 41 | loop((now) => { 42 | knotMesh.rotation.x = now * 0.0005; 43 | knotMesh.rotation.y = now * 0.001; 44 | screen.draw(); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/palette/models/LittlestTokyo.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/palette/models/LittlestTokyo.glb -------------------------------------------------------------------------------- /examples/palette/palette.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import * as THREE from 'three'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import glfw from 'glfw-raub'; 5 | import Image from 'image-raub'; 6 | 7 | import node3d from '../../index.js'; 8 | const { init, addThreeHelpers } = node3d; 9 | import { generatePalette } from './utils/palette.js'; 10 | import { debugShaders } from './utils/debug-shaders.js'; 11 | import { createPostMaterial } from './utils/create-post-material.js'; 12 | import { createColorQuads } from './utils/create-color-quads.js'; 13 | import { createRenderTarget } from './utils/create-render-target.js'; 14 | import { populateScene } from './utils/populate-scene.js'; 15 | 16 | 17 | const IS_PERF_MODE = !true; 18 | 19 | const hueModes = [ 20 | 'monochromatic', 'analagous', 'complementary', 'triadic', 'tetradic', 21 | ]; 22 | 23 | const { extraCodes } = glfw; 24 | 25 | const { 26 | doc, gl, Screen, loop, 27 | } = init({ 28 | isGles3: true, 29 | isWebGL2: true, 30 | autoEsc: true, 31 | autoFullscreen: true, 32 | title: 'Palette Swap', 33 | vsync: !IS_PERF_MODE, 34 | }); 35 | addThreeHelpers(THREE, gl); 36 | 37 | const icon = new Image('textures/icon.png'); 38 | icon.on('load', () => { doc.icon = icon; }); 39 | 40 | const screen = new Screen({ three: THREE, fov: 50, near: 1, far: 1000 }); 41 | screen.renderer.shadowMap.enabled = true; 42 | screen.camera.position.z = 9; 43 | 44 | const cameraOrtho = new THREE.OrthographicCamera( 45 | -doc.w * 0.5, doc.w * 0.5, doc.h * 0.5, -doc.h * 0.5, - 10, 10, 46 | ); 47 | cameraOrtho.position.z = 5; 48 | 49 | const controls = new OrbitControls(screen.camera, doc); 50 | controls.update(); 51 | 52 | let mesh; 53 | populateScene(screen.scene, (m) => { mesh = m; }); 54 | 55 | const scenePost = new THREE.Scene(); 56 | 57 | let isSwap = false; 58 | let modeGrayscale = 0; 59 | let modeHue = 0; 60 | let numColors = 9; 61 | 62 | const rawPalette0 = generatePalette(hueModes[modeHue], numColors); 63 | let palette = rawPalette0.map((c) => (new THREE.Color(...c))); 64 | 65 | const fragmentShader = readFileSync('post.glsl').toString(); 66 | let materialPost = createPostMaterial(THREE, numColors, isSwap, modeGrayscale, palette, fragmentShader); 67 | 68 | let rt = createRenderTarget(THREE, materialPost, doc.w, doc.h); 69 | 70 | let quadPost = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), materialPost); 71 | scenePost.add(quadPost); 72 | 73 | const quadHelp = new THREE.Mesh( 74 | new THREE.PlaneGeometry(256, 256), 75 | new THREE.MeshBasicMaterial({ 76 | side: THREE.CullFaceFront, 77 | depthTest: false, 78 | depthWrite: false, 79 | transparent: true, 80 | map: new THREE.TextureLoader().load('textures/help.png'), 81 | }), 82 | ); 83 | quadHelp.position.set(doc.w * 0.5 - 128, -doc.h * 0.5 + 128, 1); 84 | scenePost.add(quadHelp); 85 | 86 | let colorQuads = createColorQuads(THREE, scenePost, palette, isSwap); 87 | 88 | const setPalette = (newValue) => { 89 | palette = newValue; 90 | materialPost.uniforms.colors.value = palette; 91 | if (palette.length !== colorQuads.length) { 92 | if (colorQuads) { 93 | colorQuads.forEach((q) => scenePost.remove(q)); 94 | } 95 | colorQuads = createColorQuads(THREE, scenePost, palette, isSwap); 96 | } else { 97 | palette.forEach((color, i) => { 98 | colorQuads[i].material.uniforms.color.value = color; 99 | }); 100 | } 101 | }; 102 | 103 | const setModeGrayscale = (newValue) => { 104 | if (isSwap && !newValue) { 105 | newValue = 1; 106 | } 107 | modeGrayscale = newValue; 108 | 109 | if (modeGrayscale == 1) { 110 | console.log('Grayscale mode: Luminosity.'); 111 | } else if (modeGrayscale == 2) { 112 | console.log('Grayscale mode: Lightness.'); 113 | } else if (modeGrayscale == 3) { 114 | console.log('Grayscale mode: Average.'); 115 | } else { 116 | console.log('Grayscale mode: OFF.'); 117 | } 118 | 119 | materialPost.uniforms.modeGrayscale.value = modeGrayscale; 120 | }; 121 | 122 | const setIsSwap = (newValue) => { 123 | isSwap = newValue; 124 | 125 | if (isSwap && !modeGrayscale) { 126 | setModeGrayscale(1); 127 | } else if (!isSwap) { 128 | setModeGrayscale(0); 129 | } 130 | 131 | materialPost.uniforms.isSwap.value = isSwap; 132 | palette.forEach((color, i) => { 133 | colorQuads[i].visible = isSwap; 134 | }); 135 | }; 136 | 137 | const randomizePalette = () => { 138 | const rawPalette = generatePalette(hueModes[modeHue], numColors); 139 | const colorPalette = rawPalette.map((c) => (new THREE.Color(...c))); 140 | setPalette(colorPalette); 141 | }; 142 | 143 | const setModeHue = (newValue) => { 144 | modeHue = newValue; 145 | randomizePalette(); 146 | }; 147 | 148 | const setNumColors = (newValue) => { 149 | if (numColors === newValue) { 150 | return; 151 | } 152 | numColors = newValue; 153 | 154 | const rawPalette = generatePalette(hueModes[modeHue], numColors); 155 | const colorPalette = rawPalette.map((c) => (new THREE.Color(...c))); 156 | materialPost = createPostMaterial( 157 | THREE, numColors, isSwap, modeGrayscale, colorPalette, fragmentShader, 158 | ); 159 | materialPost.uniforms.t.value = rt.texture; 160 | 161 | scenePost.remove(quadPost); 162 | quadPost = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), materialPost); 163 | scenePost.add(quadPost); 164 | 165 | randomizePalette(); 166 | }; 167 | 168 | doc.on('keydown', (e) => { 169 | if (e.keyCode === glfw.KEY_P) { 170 | randomizePalette(); 171 | return; 172 | } 173 | if (e.keyCode === glfw.KEY_M) { 174 | setModeHue((modeHue + 1) % hueModes.length); 175 | return; 176 | } 177 | if (e.keyCode === glfw.KEY_S) { 178 | setIsSwap(!isSwap); 179 | return; 180 | } 181 | if (e.keyCode === glfw.KEY_G) { 182 | setModeGrayscale((modeGrayscale + 1) % 4); 183 | return; 184 | } 185 | if (e.keyCode === extraCodes[glfw.KEY_EQUAL]) { 186 | setNumColors(Math.min(16, numColors + 1)); 187 | return; 188 | } 189 | if (e.keyCode === extraCodes[glfw.KEY_MINUS]) { 190 | setNumColors(Math.max(2, numColors - 1)); 191 | return; 192 | } 193 | if (e.keyCode === glfw.KEY_H || e.keyCode === extraCodes[glfw.KEY_F1]) { 194 | quadHelp.visible = !quadHelp.visible; 195 | return; 196 | } 197 | }); 198 | 199 | debugShaders(screen.renderer, false); 200 | 201 | doc.on('resize', () => { 202 | cameraOrtho.left = -doc.w * 0.5; 203 | cameraOrtho.right = doc.w * 0.5; 204 | cameraOrtho.top = doc.h * 0.5; 205 | cameraOrtho.bottom = -doc.h * 0.5; 206 | cameraOrtho.updateProjectionMatrix(); 207 | 208 | quadHelp.position.set(doc.w * 0.5 - 128, -doc.h * 0.5 + 128, 1); 209 | 210 | if (rt) { 211 | rt.dispose(); 212 | rt = null; 213 | } 214 | rt = createRenderTarget(THREE, materialPost, doc.w, doc.h); 215 | }); 216 | 217 | const render = () => { 218 | const rtOld = screen.renderer.getRenderTarget(); 219 | screen.renderer.setRenderTarget(rt); 220 | screen.draw(); 221 | screen.renderer.setRenderTarget(rtOld); 222 | 223 | screen.renderer.render(scenePost, cameraOrtho); 224 | }; 225 | 226 | let prevTime = Date.now(); 227 | let frames = 0; 228 | 229 | loop((now) => { 230 | controls.update(); 231 | 232 | if (mesh) { 233 | mesh.rotation.y = now * 0.00005; 234 | } 235 | 236 | render(); 237 | 238 | if (!IS_PERF_MODE) { 239 | return; 240 | } 241 | 242 | frames++; 243 | if (now >= prevTime + 2000) { 244 | console.log( 245 | 'FPS:', Math.floor((frames * 1000) / (now - prevTime)), 246 | ); 247 | prevTime = now; 248 | frames = 0; 249 | } 250 | }); 251 | -------------------------------------------------------------------------------- /examples/palette/post.glsl: -------------------------------------------------------------------------------- 1 | in vec2 tc; 2 | out vec4 fragColor; 3 | 4 | #ifndef NUM_COLORS 5 | #define NUM_COLORS 8 6 | #endif 7 | 8 | #define IDX_LAST (NUM_COLORS - 1) 9 | 10 | uniform bool isSwap; 11 | uniform int modeGrayscale; 12 | uniform sampler2D t; 13 | uniform vec3 colors[NUM_COLORS]; 14 | 15 | 16 | int getIndex(float value) { 17 | return clamp(int(value * float(IDX_LAST) + 0.5), 0, IDX_LAST); 18 | } 19 | 20 | 21 | vec3 paletteSwap(float gray, vec3 colors[NUM_COLORS]) { 22 | int index = getIndex(gray); 23 | return colors[index]; 24 | } 25 | 26 | float toGrayscale(vec3 rgb) { 27 | if (modeGrayscale == 1) { 28 | return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b; // Luminosity 29 | } 30 | if (modeGrayscale == 2) { 31 | return 0.5 * (max(rgb.r, max(rgb.g, rgb.b)) + min(rgb.r, min(rgb.g, rgb.b))); // Lightness 32 | } 33 | return (rgb.r + rgb.g + rgb.b) * 0.3333333; // Average 34 | } 35 | 36 | void main() { 37 | vec4 rgba = sRGBTransferOETF(texture(t, tc)); 38 | float gray = toGrayscale(rgba.rgb); 39 | vec3 finalColor = rgba.rgb; 40 | if (modeGrayscale > 0) { 41 | finalColor = vec3(gray); 42 | } 43 | fragColor = vec4(mix(finalColor, paletteSwap(gray, colors), bvec3(isSwap)), rgba.a); 44 | } 45 | -------------------------------------------------------------------------------- /examples/palette/textures/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/palette/textures/help.png -------------------------------------------------------------------------------- /examples/palette/textures/help.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/palette/textures/help.xcf -------------------------------------------------------------------------------- /examples/palette/textures/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/palette/textures/icon.png -------------------------------------------------------------------------------- /examples/palette/utils/DRACOLoader.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Color, 5 | FileLoader, 6 | Loader, 7 | LinearSRGBColorSpace, 8 | SRGBColorSpace 9 | } from 'three'; 10 | 11 | const _taskCache = new WeakMap(); 12 | 13 | class DRACOLoader extends Loader { 14 | 15 | constructor( manager ) { 16 | 17 | super( manager ); 18 | 19 | this.decoderPath = ''; 20 | this.decoderConfig = {}; 21 | this.decoderBinary = null; 22 | this.decoderPending = null; 23 | 24 | this.workerLimit = 4; 25 | this.workerPool = []; 26 | this.workerNextTaskID = 1; 27 | this.workerSourceURL = ''; 28 | 29 | this.defaultAttributeIDs = { 30 | position: 'POSITION', 31 | normal: 'NORMAL', 32 | color: 'COLOR', 33 | uv: 'TEX_COORD' 34 | }; 35 | this.defaultAttributeTypes = { 36 | position: 'Float32Array', 37 | normal: 'Float32Array', 38 | color: 'Float32Array', 39 | uv: 'Float32Array' 40 | }; 41 | 42 | } 43 | 44 | setDecoderPath( path ) { 45 | 46 | this.decoderPath = path; 47 | 48 | return this; 49 | 50 | } 51 | 52 | setDecoderConfig( config ) { 53 | 54 | this.decoderConfig = config; 55 | 56 | return this; 57 | 58 | } 59 | 60 | setWorkerLimit( workerLimit ) { 61 | 62 | this.workerLimit = workerLimit; 63 | 64 | return this; 65 | 66 | } 67 | 68 | load( url, onLoad, onProgress, onError ) { 69 | 70 | const loader = new FileLoader( this.manager ); 71 | 72 | loader.setPath( this.path ); 73 | loader.setResponseType( 'arraybuffer' ); 74 | loader.setRequestHeader( this.requestHeader ); 75 | loader.setWithCredentials( this.withCredentials ); 76 | 77 | loader.load( url, ( buffer ) => { 78 | 79 | this.parse( buffer, onLoad, onError ); 80 | 81 | }, onProgress, onError ); 82 | 83 | } 84 | 85 | parse( buffer, onLoad, onError ) { 86 | 87 | this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError ); 88 | 89 | } 90 | 91 | decodeDracoFile( 92 | buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, 93 | ) { 94 | 95 | const taskConfig = { 96 | attributeIDs: attributeIDs || this.defaultAttributeIDs, 97 | attributeTypes: attributeTypes || this.defaultAttributeTypes, 98 | useUniqueIDs: !! attributeIDs, 99 | vertexColorSpace: vertexColorSpace, 100 | }; 101 | 102 | return this.decodeGeometry( buffer, taskConfig ).then( callback ); 103 | 104 | } 105 | 106 | decodeGeometry( buffer, taskConfig ) { 107 | 108 | const taskKey = JSON.stringify( taskConfig ); 109 | 110 | // Check for an existing task using this buffer. A transferred buffer cannot be transferred 111 | // again from this thread. 112 | if ( _taskCache.has( buffer ) ) { 113 | 114 | const cachedTask = _taskCache.get( buffer ); 115 | 116 | if ( cachedTask.key === taskKey ) { 117 | 118 | return cachedTask.promise; 119 | 120 | } else if ( buffer.byteLength === 0 ) { 121 | 122 | // Technically, it would be possible to wait for the previous task to complete, 123 | // transfer the buffer back, and decode again with the second configuration. That 124 | // is complex, and I don't know of any reason to decode a Draco buffer twice in 125 | // different ways, so this is left unimplemented. 126 | throw new Error( 127 | 128 | 'THREE.DRACOLoader: Unable to re-decode a buffer with different ' + 129 | 'settings. Buffer has already been transferred.' 130 | 131 | ); 132 | 133 | } 134 | 135 | } 136 | 137 | // 138 | 139 | let worker; 140 | const taskID = this.workerNextTaskID ++; 141 | const taskCost = buffer.byteLength; 142 | 143 | // Obtain a worker and assign a task, and construct a geometry instance 144 | // when the task completes. 145 | const geometryPending = this._getWorker( taskID, taskCost ) 146 | .then( ( _worker ) => { 147 | 148 | worker = _worker; 149 | 150 | return new Promise( ( resolve, reject ) => { 151 | 152 | worker._callbacks[ taskID ] = { resolve, reject }; 153 | 154 | worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] ); 155 | 156 | // this.debug(); 157 | 158 | } ); 159 | 160 | } ) 161 | .then( ( message ) => this._createGeometry( message.geometry ) ); 162 | 163 | // Remove task from the task list. 164 | // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) 165 | geometryPending 166 | .catch( () => true ) 167 | .then( () => { 168 | 169 | if ( worker && taskID ) { 170 | 171 | this._releaseTask( worker, taskID ); 172 | 173 | // this.debug(); 174 | 175 | } 176 | 177 | } ); 178 | 179 | // Cache the task result. 180 | _taskCache.set( buffer, { 181 | 182 | key: taskKey, 183 | promise: geometryPending 184 | 185 | } ); 186 | 187 | return geometryPending; 188 | 189 | } 190 | 191 | _createGeometry( geometryData ) { 192 | 193 | const geometry = new BufferGeometry(); 194 | 195 | if ( geometryData.index ) { 196 | 197 | geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) ); 198 | 199 | } 200 | 201 | for ( let i = 0; i < geometryData.attributes.length; i ++ ) { 202 | 203 | const result = geometryData.attributes[ i ]; 204 | const name = result.name; 205 | const array = result.array; 206 | const itemSize = result.itemSize; 207 | 208 | const attribute = new BufferAttribute( array, itemSize ); 209 | 210 | if ( name === 'color' ) { 211 | 212 | this._assignVertexColorSpace( attribute, result.vertexColorSpace ); 213 | 214 | attribute.normalized = ( array instanceof Float32Array ) === false; 215 | 216 | } 217 | 218 | geometry.setAttribute( name, attribute ); 219 | 220 | } 221 | 222 | return geometry; 223 | 224 | } 225 | 226 | _assignVertexColorSpace( attribute, inputColorSpace ) { 227 | 228 | // While .drc files do not specify colorspace, the only 'official' tooling 229 | // is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc 230 | // file is passed into .load() or .parse(). GLTFLoader uses internal APIs 231 | // to decode geometry, and vertex colors are already Linear-sRGB in there. 232 | 233 | if ( inputColorSpace !== SRGBColorSpace ) return; 234 | 235 | const _color = new Color(); 236 | 237 | for ( let i = 0, il = attribute.count; i < il; i ++ ) { 238 | 239 | _color.fromBufferAttribute( attribute, i ).convertSRGBToLinear(); 240 | attribute.setXYZ( i, _color.r, _color.g, _color.b ); 241 | 242 | } 243 | 244 | } 245 | 246 | _loadLibrary( url, responseType ) { 247 | 248 | const loader = new FileLoader( this.manager ); 249 | loader.setPath( this.decoderPath ); 250 | loader.setResponseType( responseType ); 251 | loader.setWithCredentials( this.withCredentials ); 252 | 253 | return new Promise( ( resolve, reject ) => { 254 | 255 | loader.load( url, resolve, undefined, reject ); 256 | 257 | } ); 258 | 259 | } 260 | 261 | preload() { 262 | 263 | this._initDecoder(); 264 | 265 | return this; 266 | 267 | } 268 | 269 | _initDecoder() { 270 | 271 | if ( this.decoderPending ) return this.decoderPending; 272 | 273 | const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; 274 | const librariesPending = []; 275 | 276 | if ( useJS ) { 277 | 278 | librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) ); 279 | 280 | } else { 281 | 282 | librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) ); 283 | librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) ); 284 | 285 | } 286 | 287 | this.decoderPending = Promise.all( librariesPending ) 288 | .then( ( libraries ) => { 289 | 290 | const jsContent = libraries[ 0 ]; 291 | 292 | if ( ! useJS ) { 293 | 294 | this.decoderConfig.wasmBinary = libraries[ 1 ]; 295 | 296 | } 297 | 298 | const fn = DRACOWorker.toString(); 299 | 300 | const body = [ 301 | '/* draco decoder */', 302 | jsContent, 303 | '', 304 | '/* worker */', 305 | fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) 306 | ].join( '\n' ); 307 | 308 | this.workerSourceURL = new URL(`data:text/javascript;utf8,${escape(body)}`); 309 | // this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) ); 310 | 311 | } ); 312 | 313 | return this.decoderPending; 314 | 315 | } 316 | 317 | _getWorker( taskID, taskCost ) { 318 | 319 | return this._initDecoder().then( () => { 320 | 321 | if ( this.workerPool.length < this.workerLimit ) { 322 | 323 | const worker = new global.Worker( this.workerSourceURL ); 324 | 325 | worker._callbacks = {}; 326 | worker._taskCosts = {}; 327 | worker._taskLoad = 0; 328 | 329 | worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } ); 330 | 331 | worker.on('message', function ( e ) { 332 | 333 | const message = e; 334 | 335 | switch ( message.type ) { 336 | 337 | case 'decode': 338 | worker._callbacks[ message.id ].resolve( message ); 339 | break; 340 | 341 | case 'error': 342 | worker._callbacks[ message.id ].reject( message ); 343 | break; 344 | 345 | default: 346 | console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' ); 347 | 348 | } 349 | 350 | }); 351 | 352 | this.workerPool.push( worker ); 353 | 354 | } else { 355 | 356 | this.workerPool.sort( function ( a, b ) { 357 | 358 | return a._taskLoad > b._taskLoad ? - 1 : 1; 359 | 360 | } ); 361 | 362 | } 363 | 364 | const worker = this.workerPool[ this.workerPool.length - 1 ]; 365 | worker._taskCosts[ taskID ] = taskCost; 366 | worker._taskLoad += taskCost; 367 | return worker; 368 | 369 | } ); 370 | 371 | } 372 | 373 | _releaseTask( worker, taskID ) { 374 | 375 | worker._taskLoad -= worker._taskCosts[ taskID ]; 376 | delete worker._callbacks[ taskID ]; 377 | delete worker._taskCosts[ taskID ]; 378 | 379 | } 380 | 381 | debug() { 382 | 383 | console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); 384 | 385 | } 386 | 387 | dispose() { 388 | 389 | for ( let i = 0; i < this.workerPool.length; ++ i ) { 390 | 391 | this.workerPool[ i ].terminate(); 392 | 393 | } 394 | 395 | this.workerPool.length = 0; 396 | 397 | if ( this.workerSourceURL !== '' ) { 398 | 399 | URL.revokeObjectURL( this.workerSourceURL ); 400 | 401 | } 402 | 403 | return this; 404 | 405 | } 406 | 407 | } 408 | 409 | /* WEB WORKER */ 410 | 411 | function DRACOWorker() { 412 | 413 | let decoderConfig; 414 | let decoderPending; 415 | 416 | const { parentPort } = require('worker_threads'); 417 | global.self = global; 418 | global.globalThis = global; 419 | 420 | parentPort.on('message', function ( e ) { 421 | const message = e; 422 | const buffer = message.buffer; 423 | const taskConfig = message.taskConfig; 424 | 425 | switch ( message.type ) { 426 | 427 | case 'init': 428 | decoderConfig = message.decoderConfig; 429 | decoderPending = new Promise( function ( resolve/*, reject*/ ) { 430 | 431 | decoderConfig.onModuleLoaded = function ( draco ) { 432 | 433 | // Module is Promise-like. Wrap before resolving to avoid loop. 434 | resolve( { draco: draco } ); 435 | 436 | }; 437 | 438 | DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef 439 | 440 | } ); 441 | break; 442 | 443 | case 'decode': 444 | decoderPending.then( ( module ) => { 445 | 446 | const draco = module.draco; 447 | const decoder = new draco.Decoder(); 448 | 449 | try { 450 | 451 | const geometry = decodeGeometry( 452 | draco, decoder, new Int8Array( buffer ), taskConfig, 453 | ); 454 | 455 | const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); 456 | 457 | if ( geometry.index ) buffers.push( geometry.index.array.buffer ); 458 | 459 | parentPort.postMessage( { type: 'decode', id: message.id, geometry }, buffers ); 460 | 461 | } catch ( error ) { 462 | 463 | console.error( error ); 464 | 465 | parentPort.postMessage( { type: 'error', id: message.id, error: error.message } ); 466 | 467 | } finally { 468 | 469 | draco.destroy( decoder ); 470 | 471 | } 472 | 473 | } ); 474 | break; 475 | 476 | } 477 | 478 | }); 479 | 480 | 481 | 482 | function decodeGeometry( draco, decoder, array, taskConfig ) { 483 | 484 | const attributeIDs = taskConfig.attributeIDs; 485 | const attributeTypes = taskConfig.attributeTypes; 486 | 487 | let dracoGeometry; 488 | let decodingStatus; 489 | 490 | const geometryType = decoder.GetEncodedGeometryType( array ); 491 | 492 | if ( geometryType === draco.TRIANGULAR_MESH ) { 493 | 494 | dracoGeometry = new draco.Mesh(); 495 | decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry ); 496 | 497 | } else if ( geometryType === draco.POINT_CLOUD ) { 498 | 499 | dracoGeometry = new draco.PointCloud(); 500 | decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry ); 501 | 502 | } else { 503 | 504 | throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' ); 505 | 506 | } 507 | 508 | if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) { 509 | 510 | throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() ); 511 | 512 | } 513 | 514 | const geometry = { index: null, attributes: [] }; 515 | 516 | // Gather all vertex attributes. 517 | for ( const attributeName in attributeIDs ) { 518 | 519 | const attributeType = global[ attributeTypes[ attributeName ] ]; 520 | 521 | let attribute; 522 | let attributeID; 523 | 524 | // A Draco file may be created with default vertex attributes, whose attribute IDs 525 | // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, 526 | // a Draco file may contain a custom set of attributes, identified by known unique 527 | // IDs. glTF files always do the latter, and `.drc` files typically do the former. 528 | if ( taskConfig.useUniqueIDs ) { 529 | 530 | attributeID = attributeIDs[ attributeName ]; 531 | attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID ); 532 | 533 | } else { 534 | 535 | attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] ); 536 | 537 | if ( attributeID === - 1 ) continue; 538 | 539 | attribute = decoder.GetAttribute( dracoGeometry, attributeID ); 540 | 541 | } 542 | 543 | const attributeResult = decodeAttribute( 544 | draco, decoder, dracoGeometry, attributeName, attributeType, attribute, 545 | ); 546 | 547 | if ( attributeName === 'color' ) { 548 | 549 | attributeResult.vertexColorSpace = taskConfig.vertexColorSpace; 550 | 551 | } 552 | 553 | geometry.attributes.push( attributeResult ); 554 | 555 | } 556 | 557 | // Add index. 558 | if ( geometryType === draco.TRIANGULAR_MESH ) { 559 | 560 | geometry.index = decodeIndex( draco, decoder, dracoGeometry ); 561 | 562 | } 563 | 564 | draco.destroy( dracoGeometry ); 565 | 566 | return geometry; 567 | 568 | } 569 | 570 | function decodeIndex( draco, decoder, dracoGeometry ) { 571 | 572 | const numFaces = dracoGeometry.num_faces(); 573 | const numIndices = numFaces * 3; 574 | const byteLength = numIndices * 4; 575 | 576 | const ptr = draco._malloc( byteLength ); 577 | decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr ); 578 | const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); 579 | draco._free( ptr ); 580 | 581 | return { array: index, itemSize: 1 }; 582 | 583 | } 584 | 585 | function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { 586 | 587 | const numComponents = attribute.num_components(); 588 | const numPoints = dracoGeometry.num_points(); 589 | const numValues = numPoints * numComponents; 590 | const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; 591 | const dataType = getDracoDataType( draco, attributeType ); 592 | 593 | const ptr = draco._malloc( byteLength ); 594 | decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr ); 595 | const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); 596 | draco._free( ptr ); 597 | 598 | return { 599 | name: attributeName, 600 | array: array, 601 | itemSize: numComponents 602 | }; 603 | 604 | } 605 | 606 | function getDracoDataType( draco, attributeType ) { 607 | 608 | switch ( attributeType ) { 609 | 610 | case Float32Array: return draco.DT_FLOAT32; 611 | case Int8Array: return draco.DT_INT8; 612 | case Int16Array: return draco.DT_INT16; 613 | case Int32Array: return draco.DT_INT32; 614 | case Uint8Array: return draco.DT_UINT8; 615 | case Uint16Array: return draco.DT_UINT16; 616 | case Uint32Array: return draco.DT_UINT32; 617 | 618 | } 619 | 620 | } 621 | 622 | } 623 | 624 | export { DRACOLoader }; 625 | -------------------------------------------------------------------------------- /examples/palette/utils/create-color-quads.js: -------------------------------------------------------------------------------- 1 | const createColorQuads = (THREE, scenePost, palette, isSwap) => { 2 | const colorQuads = []; 3 | 4 | palette.forEach((color, i) => { 5 | const materialColor = new THREE.ShaderMaterial({ 6 | side: THREE.CullFaceFront, 7 | depthTest: false, 8 | depthWrite: false, 9 | transparent: false, 10 | lights: false, 11 | uniforms: { 12 | color: { value: color }, 13 | }, 14 | vertexShader: ` 15 | void main() { 16 | gl_Position = vec4(position, 1.0); 17 | } 18 | `, 19 | fragmentShader: ` 20 | uniform vec3 color; 21 | void main() { 22 | gl_FragColor = vec4(color, 1.0); 23 | } 24 | `, 25 | }); 26 | const quadColor = new THREE.Mesh( 27 | new THREE.PlaneGeometry(0.1, 2 / palette.length), materialColor, 28 | ); 29 | quadColor.geometry.translate(-0.95, -1 + 2 * (i + 0.5) / palette.length, 1); 30 | quadColor.visible = isSwap; 31 | scenePost.add(quadColor); 32 | colorQuads.push(quadColor); 33 | }); 34 | 35 | return colorQuads; 36 | }; 37 | 38 | module.exports = { createColorQuads }; 39 | -------------------------------------------------------------------------------- /examples/palette/utils/create-post-material.js: -------------------------------------------------------------------------------- 1 | const createPostMaterial = (THREE, numColors, isSwap, modeGrayscale, palette, fragmentShader) => { 2 | return new THREE.ShaderMaterial({ 3 | side: THREE.CullFaceFront, 4 | uniforms: { 5 | isSwap: { value: isSwap }, 6 | modeGrayscale: { value: modeGrayscale }, 7 | t: { value: null }, 8 | colors: { value: palette }, 9 | }, 10 | defines: { 11 | NUM_COLORS: numColors, 12 | }, 13 | depthWrite: false, 14 | depthTest: false, 15 | transparent: false, 16 | lights: false, 17 | vertexShader: ` 18 | out vec2 tc; 19 | void main() { 20 | tc = uv; 21 | gl_Position = vec4(position, 1.0); 22 | } 23 | `, 24 | fragmentShader, 25 | glslVersion: '300 es', 26 | }); 27 | }; 28 | 29 | module.exports = { createPostMaterial }; 30 | -------------------------------------------------------------------------------- /examples/palette/utils/create-render-target.js: -------------------------------------------------------------------------------- 1 | const createRenderTarget = (THREE, materialPost, w, h) => { 2 | const newRt = new THREE.WebGLRenderTarget( 3 | w, 4 | h, 5 | { 6 | minFilter: THREE.LinearFilter, 7 | magFilter: THREE.NearestFilter, 8 | format: THREE.RGBAFormat, 9 | colorSpace: THREE.LinearSRGBColorSpace, 10 | } 11 | ); 12 | 13 | materialPost.uniforms.t.value = newRt.texture; 14 | 15 | return newRt; 16 | }; 17 | 18 | module.exports = { createRenderTarget }; 19 | -------------------------------------------------------------------------------- /examples/palette/utils/debug-shaders.js: -------------------------------------------------------------------------------- 1 | const debugShaders = (renderer, isEnabled) => { 2 | renderer.debug.checkShaderErrors = isEnabled; 3 | 4 | if (!isEnabled) { 5 | renderer.debug.onShaderError = null; 6 | return; 7 | } 8 | 9 | renderer.debug.onShaderError = (gl, _program, vs, fs) => { 10 | const parseForErrors = (shader, name) => { 11 | const errors = (gl.getShaderInfoLog(shader) || '').trim(); 12 | const prefix = 'Errors in ' + name + ':' + '\n\n' + errors; 13 | 14 | if (errors !== '') { 15 | const code = (gl.getShaderSource(shader) || '').replace(/\t/g, ' '); 16 | const lines = code.split('\n'); 17 | var linedCode = ''; 18 | var i = 1; 19 | for (var line of lines) { 20 | linedCode += (i < 10 ? ' ' : '') + i + ':\t\t' + line + '\n'; 21 | i++; 22 | } 23 | 24 | console.error(prefix + '\n' + linedCode); 25 | } 26 | }; 27 | 28 | parseForErrors(vs, 'Vertex Shader'); 29 | parseForErrors(fs, 'Fragment Shader'); 30 | }; 31 | }; 32 | 33 | module.exports = { debugShaders }; 34 | -------------------------------------------------------------------------------- /examples/palette/utils/palette.js: -------------------------------------------------------------------------------- 1 | // Derived from https://evannorton.github.io/acerolas-epic-color-palettes/ 2 | 3 | const oklabToLinearSrgb = (L, a, b) => { 4 | let l_ = L + 0.3963377774 * a + 0.2158037573 * b; 5 | let m_ = L - 0.1055613458 * a - 0.0638541728 * b; 6 | let s_ = L - 0.0894841775 * a - 1.2914855480 * b; 7 | 8 | let l = l_ * l_ * l_; 9 | let m = m_ * m_ * m_; 10 | let s = s_ * s_ * s_; 11 | 12 | return [ 13 | (+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), 14 | (-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), 15 | (-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s), 16 | ]; 17 | }; 18 | 19 | const oklchToOklab = (L, c, h) => { 20 | return [(L), (c * Math.cos(h)), (c * Math.sin(h))]; 21 | }; 22 | 23 | const lerp = (min, max, t) => { 24 | return min + (max - min) * t; 25 | }; 26 | 27 | 28 | const generateOKLCH = (hueMode, settings) => { 29 | let oklchColors = []; 30 | 31 | let hueBase = settings.hueBase * 2 * Math.PI; 32 | let hueContrast = lerp(0.33, 1.0, settings.hueContrast); 33 | 34 | let chromaBase = lerp(0.01, 0.1, settings.saturationBase); 35 | let chromaContrast = lerp(0.075, 0.125 - chromaBase, settings.saturationContrast); 36 | let chromaFixed = lerp(0.01, 0.125, settings.fixed); 37 | 38 | let lightnessBase = lerp(0.3, 0.6, settings.luminanceBase); 39 | let lightnessContrast = lerp(0.3, 1.0 - lightnessBase, settings.luminanceContrast); 40 | let lightnessFixed = lerp(0.6, 0.9, settings.fixed); 41 | 42 | let chromaConstant = settings.saturationConstant; 43 | let lightnessConstant = !chromaConstant; 44 | 45 | if (hueMode === 'monochromatic') { 46 | chromaConstant = false; 47 | lightnessConstant = false; 48 | } 49 | 50 | for (let i = 0; i < settings.colorCount; ++i) { 51 | let linearIterator = (i) / (settings.colorCount - 1); 52 | 53 | let hueOffset = linearIterator * hueContrast * 2 * Math.PI + (Math.PI / 4); 54 | 55 | if (hueMode === 'monochromatic') hueOffset *= 0.0; 56 | if (hueMode === 'analagous') hueOffset *= 0.25; 57 | if (hueMode === 'complementary') hueOffset *= 0.33; 58 | if (hueMode === 'triadic') hueOffset *= 0.66; 59 | if (hueMode === 'tetradic') hueOffset *= 0.75; 60 | 61 | if (hueMode !== 'monochromatic') 62 | hueOffset += (Math.random() * 2 - 1) * 0.01; 63 | 64 | let chroma = chromaBase + linearIterator * chromaContrast; 65 | let lightness = lightnessBase + linearIterator * lightnessContrast; 66 | 67 | if (chromaConstant) chroma = chromaFixed; 68 | if (lightnessConstant) lightness = lightnessFixed; 69 | 70 | let lab = oklchToOklab(lightness, chroma, hueBase + hueOffset); 71 | let rgb = oklabToLinearSrgb(lab[0], lab[1], lab[2]); 72 | 73 | rgb[0] = Math.max(0.0, Math.min(rgb[0], 1.0)); 74 | rgb[1] = Math.max(0.0, Math.min(rgb[1], 1.0)); 75 | rgb[2] = Math.max(0.0, Math.min(rgb[2], 1.0)); 76 | 77 | oklchColors.push(rgb); 78 | } 79 | 80 | return oklchColors; 81 | }; 82 | 83 | const createSettings = (colorCount) => { 84 | return { 85 | hueBase: Math.random(), 86 | hueContrast: Math.random(), 87 | saturationBase: Math.random(), 88 | saturationContrast: Math.random(), 89 | luminanceBase: Math.random(), 90 | luminanceContrast: Math.random(), 91 | fixed: Math.random(), 92 | saturationConstant: true, 93 | colorCount: colorCount, 94 | }; 95 | }; 96 | 97 | const generatePalette = (hueMode, colorCount) => { 98 | let paletteSettings = createSettings(colorCount); 99 | let lch = generateOKLCH(hueMode, paletteSettings); 100 | console.log('New palette:', hueMode, lch); 101 | return lch; 102 | }; 103 | 104 | const hueOffsets = { 105 | monochromatic: 0.0, 106 | analagous: 0.25, 107 | complementary: 0.33, 108 | triadic: 0.66, 109 | tetradic: 0.75, 110 | }; 111 | 112 | const hueModes = Object.keys(hueOffsets); 113 | 114 | module.exports = { 115 | hueModes, 116 | generatePalette, 117 | }; 118 | -------------------------------------------------------------------------------- /examples/palette/utils/populate-scene.js: -------------------------------------------------------------------------------- 1 | const { Worker } = require('node:worker_threads'); 2 | 3 | 4 | const flipUv = (attribute) => { 5 | if (!attribute) { 6 | return; 7 | } 8 | for (let i = 1; i < attribute.array.length; i += 2) { 9 | attribute.array[i] = 1 - attribute.array[i]; 10 | } 11 | }; 12 | 13 | 14 | const populateScene = (scene, cb) => { 15 | global.Worker = class Worker2 extends Worker { 16 | constructor(name, options) { 17 | const nameStr = name.toString(); 18 | if (nameStr.startsWith('data:')) { 19 | const [, body] = nameStr.toString().split(','); 20 | super(unescape(body), { ...options, eval: true }); 21 | return; 22 | } 23 | 24 | super(name, options); 25 | } 26 | }; 27 | 28 | (async () => { 29 | const THREE = await import('three'); 30 | const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js'); 31 | const { DRACOLoader } = await import('./DRACOLoader.mjs'); 32 | 33 | const ambientLight = new THREE.AmbientLight(0xeeffee, 0.3); 34 | scene.add(ambientLight); 35 | 36 | const directionalLight1 = new THREE.DirectionalLight(0xeeeeff, 2.4); 37 | directionalLight1.position.set(20, 20, 20); 38 | scene.add(directionalLight1); 39 | 40 | const d = 10; 41 | directionalLight1.castShadow = true; 42 | directionalLight1.shadow.camera.left = -d; 43 | directionalLight1.shadow.camera.right = d; 44 | directionalLight1.shadow.camera.top = d; 45 | directionalLight1.shadow.camera.bottom = -d; 46 | directionalLight1.shadow.camera.near = 5; 47 | directionalLight1.shadow.camera.far = 60; 48 | directionalLight1.shadow.mapSize.x = 2048; 49 | directionalLight1.shadow.mapSize.y = 2048; 50 | directionalLight1.shadow.intensity = 0.55; 51 | 52 | const directionalLight2 = new THREE.DirectionalLight(0xffaaaa, 0.7); 53 | directionalLight2.position.set(-20, 5, 20).normalize(); 54 | scene.add(directionalLight2); 55 | 56 | const directionalLight3 = new THREE.DirectionalLight(0xffddaa, 0.5); 57 | directionalLight3.position.set(5, -20, -5).normalize(); 58 | scene.add(directionalLight3); 59 | 60 | scene.background = new THREE.Color(0x87ceeb); 61 | 62 | const dracoLoader = new DRACOLoader(); 63 | dracoLoader.setDecoderPath(`${__dirname}/../../../node_modules/three/examples/jsm/libs/draco/gltf/`); 64 | 65 | const loader = new GLTFLoader(); 66 | loader.setDRACOLoader(dracoLoader); 67 | loader.load(__dirname + '/../models/LittlestTokyo.glb', (gltf) => { 68 | gltf.scene.scale.set(0.01, 0.01, 0.01); 69 | 70 | gltf.scene.traverse((node) => { 71 | if (!node.isMesh) { 72 | return; 73 | } 74 | node.castShadow = true; 75 | flipUv(node.geometry.attributes.uv); 76 | flipUv(node.geometry.attributes.uv1); 77 | }); 78 | 79 | scene.add(gltf.scene); 80 | cb(gltf.scene); 81 | }); 82 | 83 | const floorMaterial = new THREE.MeshStandardMaterial({ color: 0xface8d }); 84 | const geoFloor = new THREE.PlaneGeometry(100, 100, 4, 4); 85 | const meshFloor = new THREE.Mesh(geoFloor, floorMaterial); 86 | meshFloor.rotation.x = -Math.PI * 0.5; 87 | meshFloor.position.y = -2; 88 | meshFloor.receiveShadow = true; 89 | scene.add(meshFloor); 90 | })(); 91 | }; 92 | 93 | module.exports = { populateScene }; 94 | -------------------------------------------------------------------------------- /examples/pixi/index.mjs: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import node3d from '../../index.js'; 3 | const { init } = node3d; 4 | 5 | const { canvas, doc, window } = init({ 6 | isGles3: true, 7 | isWebGL2: true, 8 | autoEsc: true, 9 | vsync: true, 10 | title: 'PIXI', 11 | }); 12 | 13 | const createOld = doc.createElement.bind(doc); 14 | doc.createElement = (name) => { 15 | if (name === 'div' || name === 'a') { 16 | return { style: {} }; 17 | } 18 | return createOld(name); 19 | }; 20 | 21 | // based on https://pixijs.io/examples/#/demos-basic/container.js 22 | 23 | const app = new PIXI.Application({ 24 | backgroundColor: 0x1099bb, 25 | resolution: window.devicePixelRatio || 1, 26 | view: canvas, 27 | }); 28 | 29 | const container = new PIXI.Container(); 30 | 31 | app.stage.addChild(container); 32 | 33 | // Create a new texture 34 | const texture = PIXI.Texture.from('https://pixijs.io/examples/examples/assets/bunny.png'); 35 | 36 | // Create a 5x5 grid of bunnies 37 | for (let i = 0; i < 25; i++) { 38 | const bunny = new PIXI.Sprite(texture); 39 | bunny.anchor.set(0.5); 40 | bunny.x = (i % 5) * 40; 41 | bunny.y = Math.floor(i / 5) * 40; 42 | container.addChild(bunny); 43 | } 44 | 45 | // Move container to the center 46 | container.x = app.screen.width / 2; 47 | container.y = app.screen.height / 2; 48 | 49 | // Center bunny sprite in local container coordinates 50 | container.pivot.x = container.width / 2; 51 | container.pivot.y = container.height / 2; 52 | 53 | app.ticker.add((delta) => { 54 | container.rotation -= 0.01 * delta; 55 | }); 56 | -------------------------------------------------------------------------------- /examples/pixi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "dependencies": { 7 | "pixi.js": "^7.3.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/screenshot.png -------------------------------------------------------------------------------- /examples/screenshot.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/screenshot.xcf -------------------------------------------------------------------------------- /examples/srgb/.gitignore: -------------------------------------------------------------------------------- 1 | screenshot-*.png 2 | -------------------------------------------------------------------------------- /examples/srgb/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | DataTexture, LinearSRGBColorSpace, NoToneMapping, SRGBColorSpace, Uniform, WebGLRenderer, 4 | } from 'three'; 5 | 6 | import Img from 'image-raub'; 7 | import { init } from '3d-core-raub'; 8 | import { SimpleTextureProcessor } from './simple_texture_processor.ts'; 9 | 10 | init({ 11 | isGles3: true, 12 | isWebGL2: true, 13 | isVisible: false, 14 | }); 15 | 16 | const renderer = new WebGLRenderer({ 17 | antialias: false, 18 | alpha: false, 19 | depth: false, 20 | powerPreference: 'high-performance', 21 | preserveDrawingBuffer: false, 22 | }); 23 | renderer.shadowMap.autoUpdate = false; 24 | renderer.outputColorSpace = LinearSRGBColorSpace; 25 | renderer.toneMapping = NoToneMapping; 26 | 27 | const processor = new SimpleTextureProcessor(512, renderer); 28 | 29 | // A simple shader that passes through the texture 30 | const shader = ` 31 | uniform sampler2D tex; 32 | 33 | void main() { 34 | vec2 vUv = gl_FragCoord.xy / resolution.xy; 35 | gl_FragColor = texture2D(tex, vUv); 36 | } 37 | `; 38 | 39 | /** Create a datatexture of size 512x512 filled with constant value */ 40 | function createDataTexture() { 41 | const texture = new DataTexture(); 42 | texture.image = { 43 | data: new Uint8ClampedArray(512 * 512 * 4).fill(100, 0, 512 * 512 * 2).fill(200, 512 * 512 * 2), 44 | width: 512, 45 | height: 512, 46 | }; 47 | return texture; 48 | } 49 | 50 | function processAndSave(savePath: string, texture: DataTexture) { 51 | const res = processor.process(shader, { 52 | tex: new Uniform(texture), 53 | }); 54 | console.log(res.slice(0, 4)); 55 | 56 | const img = Img.fromPixels(512, 512, 32, Buffer.from(res)); 57 | img.save(savePath); 58 | } 59 | 60 | console.log('LinearSRGBColorSpace'); 61 | const texture2 = createDataTexture(); 62 | texture2.colorSpace = LinearSRGBColorSpace; 63 | texture2.needsUpdate = true; 64 | processAndSave('screenshot-linear.png', texture2); 65 | 66 | console.log('SRGBColorSpace'); 67 | const texture3 = createDataTexture(); 68 | texture3.colorSpace = SRGBColorSpace; 69 | texture3.needsUpdate = true; 70 | processAndSave('screenshot-srgb.png', texture3); 71 | -------------------------------------------------------------------------------- /examples/srgb/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test", 9 | "version": "0.0.0", 10 | "devDependencies": { 11 | "3d-core-raub": "file:../..", 12 | "tsconfig-paths": "^4.2.0" 13 | } 14 | }, 15 | "../..": { 16 | "version": "5.2.0", 17 | "dev": true, 18 | "license": "MIT", 19 | "dependencies": { 20 | "addon-tools-raub": "^9.3.0", 21 | "glfw-raub": "^6.4.0", 22 | "image-raub": "^5.2.0", 23 | "webgl-raub": "^5.2.0" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.20.0", 27 | "@types/node": "^22.13.4", 28 | "@types/three": "0.173.0", 29 | "eslint": "^9.20.1", 30 | "pixelmatch": "^6.0.0", 31 | "three": "0.174.0", 32 | "typescript": "^5.7.3" 33 | }, 34 | "engines": { 35 | "node": ">=22.14.0", 36 | "npm": ">=10.9.2" 37 | } 38 | }, 39 | "node_modules/3d-core-raub": { 40 | "resolved": "../..", 41 | "link": true 42 | }, 43 | "node_modules/json5": { 44 | "version": "2.2.3", 45 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 46 | "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 47 | "dev": true, 48 | "license": "MIT", 49 | "bin": { 50 | "json5": "lib/cli.js" 51 | }, 52 | "engines": { 53 | "node": ">=6" 54 | } 55 | }, 56 | "node_modules/minimist": { 57 | "version": "1.2.8", 58 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 59 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 60 | "dev": true, 61 | "license": "MIT", 62 | "funding": { 63 | "url": "https://github.com/sponsors/ljharb" 64 | } 65 | }, 66 | "node_modules/strip-bom": { 67 | "version": "3.0.0", 68 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 69 | "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", 70 | "dev": true, 71 | "license": "MIT", 72 | "engines": { 73 | "node": ">=4" 74 | } 75 | }, 76 | "node_modules/tsconfig-paths": { 77 | "version": "4.2.0", 78 | "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", 79 | "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", 80 | "dev": true, 81 | "license": "MIT", 82 | "dependencies": { 83 | "json5": "^2.2.2", 84 | "minimist": "^1.2.6", 85 | "strip-bom": "^3.0.0" 86 | }, 87 | "engines": { 88 | "node": ">=6" 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/srgb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "node --experimental-strip-types main.ts" 8 | }, 9 | "devDependencies": { 10 | "3d-core-raub": "file:../..", 11 | "tsconfig-paths": "^4.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/srgb/simple_texture_processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Camera, 3 | Mesh, 4 | PlaneGeometry, 5 | Scene, 6 | ShaderMaterial, 7 | Uniform, 8 | WebGLRenderTarget, 9 | WebGLRenderer, 10 | } from "three"; 11 | 12 | export class SimpleTextureProcessor { 13 | renderer: WebGLRenderer; 14 | texture_size: number; 15 | constructor(texture_size: number, renderer: WebGLRenderer) { 16 | this.renderer = renderer; 17 | this.texture_size = texture_size; 18 | } 19 | process(shaderCode: string, uniforms: Record) { 20 | const [width, height] = [this.texture_size, this.texture_size]; 21 | 22 | const scene = new Scene(); 23 | // @ts-ignore 24 | const camera = new Camera(); 25 | camera.position.z = 1; 26 | 27 | // Virtual plane 28 | const material = createShaderMaterial(shaderCode, uniforms, width, height); 29 | const mesh = new Mesh(new PlaneGeometry(2, 2), material); 30 | scene.add(mesh); 31 | 32 | // Render 33 | const renderTarget = new WebGLRenderTarget(width, height, { 34 | depthBuffer: false, 35 | }); 36 | this.renderer.setRenderTarget(renderTarget); 37 | this.renderer.render(scene, camera); 38 | 39 | // Retrieve 40 | const pixels = new Uint8Array(width * height * 4); // 4 for RGBA 41 | this.renderer.readRenderTargetPixels( 42 | renderTarget, 43 | 0, 44 | 0, 45 | width, 46 | height, 47 | pixels 48 | ); 49 | 50 | return pixels; 51 | } 52 | } 53 | 54 | function createShaderMaterial( 55 | computeFragmentShader: string, 56 | uniforms: Record, 57 | sizeX: number, 58 | sizeY: number 59 | ) { 60 | uniforms = uniforms || {}; 61 | 62 | const material = new ShaderMaterial({ 63 | uniforms: uniforms, 64 | vertexShader: ` 65 | void main() { 66 | gl_Position = vec4( position, 1.0 ); 67 | }`, 68 | fragmentShader: computeFragmentShader, 69 | }); 70 | 71 | material.defines["resolution"] = 72 | "vec2( " + sizeX.toFixed(1) + ", " + sizeY.toFixed(1) + " )"; 73 | 74 | return material; 75 | } 76 | -------------------------------------------------------------------------------- /examples/srgb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "require": ["tsconfig-paths/register"] 4 | }, 5 | "compilerOptions": { 6 | "baseUrl": "./", 7 | "paths": { 8 | "3d-core-raub*": ["../../index"], 9 | }, 10 | "target": "es2017", 11 | "moduleResolution": "nodenext", 12 | "module": "NodeNext", 13 | "allowJs": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "allowImportingTsExtensions": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "noEmit": true, 20 | "noImplicitOverride": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "allowUnreachableCode": false, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/three/crate.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | 3 | import node3d from '../../index.js'; 4 | const { init, addThreeHelpers } = node3d; 5 | 6 | const { window, document, gl, requestAnimationFrame, Screen } = init({ 7 | isGles3: true, 8 | // isWebGL2: true, 9 | autoEsc: true, 10 | autoFullscreen: true, 11 | vsync: true, 12 | title: 'Crate', 13 | }); 14 | addThreeHelpers(three, gl); 15 | 16 | var camera, scene, renderer, mesh; 17 | 18 | initExample(); 19 | 20 | function initExample() { 21 | camera = new three.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 ); 22 | camera.position.z = 2; 23 | scene = new three.Scene(); 24 | 25 | var texture = new three.TextureLoader().load( 'textures/crate.gif' ); 26 | texture.colorSpace = three.SRGBColorSpace; 27 | var geometry = new three.BoxGeometry(); 28 | var material = new three.MeshBasicMaterial( { map: texture } ); 29 | mesh = new three.Mesh( geometry, material ); 30 | scene.add( mesh ); 31 | 32 | renderer = new three.WebGLRenderer(); 33 | renderer.setPixelRatio( window.devicePixelRatio ); 34 | renderer.setSize( window.innerWidth, window.innerHeight ); 35 | document.body.appendChild( renderer.domElement ); 36 | 37 | window.addEventListener( 'resize', onWindowResize, false ); 38 | } 39 | 40 | function onWindowResize() { 41 | camera.aspect = window.innerWidth / window.innerHeight; 42 | camera.updateProjectionMatrix(); 43 | renderer.setSize( window.innerWidth, window.innerHeight ); 44 | } 45 | 46 | const screen = new Screen({ three }); 47 | 48 | function animate() { 49 | const time = Date.now(); 50 | mesh.rotation.x = time * 0.0005; 51 | mesh.rotation.y = time * 0.001; 52 | screen.renderer.render( scene, camera ); 53 | requestAnimationFrame( animate ); 54 | } 55 | 56 | requestAnimationFrame( animate ); 57 | -------------------------------------------------------------------------------- /examples/three/models/LeePerrySmith.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/models/LeePerrySmith.glb -------------------------------------------------------------------------------- /examples/three/points-buffer.mjs: -------------------------------------------------------------------------------- 1 | import * as three from 'three'; 2 | import { init } from '../../index.js'; 3 | 4 | console.log('https://threejs.org/examples/#webgl_points_random'); 5 | 6 | const { Screen, loop, gl, doc } = init({ 7 | isGles3: true, 8 | isWebGL2: true, 9 | autoEsc: true, 10 | autoFullscreen: true, 11 | vsync: true, 12 | }); 13 | 14 | const camera = new three.PerspectiveCamera(75, doc.width / doc.height, 1, 3000); 15 | const scene = new three.Scene(); 16 | const screen = new Screen({ three, camera, scene }); 17 | 18 | const REAL_SIZE = 20000; 19 | 20 | var particles, materials = [], i, h, color, size; 21 | var mouseX = 0, mouseY = 0; 22 | var windowHalfX = screen.width / 2; 23 | var windowHalfY = screen.height / 2; 24 | 25 | let cloud = null; 26 | 27 | 28 | const onWindowResize = () => { 29 | windowHalfX = screen.width / 2; 30 | windowHalfY = screen.height / 2; 31 | }; 32 | 33 | const onDocumentMouseMove = (event) => { 34 | mouseX = event.clientX - windowHalfX; 35 | mouseY = event.clientY - windowHalfY; 36 | }; 37 | 38 | const onDocumentTouchStart = (event) => { 39 | if (event.touches.length === 1) { 40 | event.preventDefault(); 41 | mouseX = event.touches[ 0 ].pageX - windowHalfX; 42 | mouseY = event.touches[ 0 ].pageY - windowHalfY; 43 | } 44 | }; 45 | 46 | const onDocumentTouchMove = (event) => { 47 | if (event.touches.length === 1) { 48 | event.preventDefault(); 49 | mouseX = event.touches[ 0 ].pageX - windowHalfX; 50 | mouseY = event.touches[ 0 ].pageY - windowHalfY; 51 | } 52 | }; 53 | 54 | 55 | const addCloud = () => { 56 | const geo = new three.BufferGeometry(); 57 | geo.computeBoundingSphere = (() => { 58 | geo.boundingSphere = new three.Sphere(undefined, Infinity); 59 | }); 60 | geo.computeBoundingSphere(); 61 | geo.setDrawRange(0, 0); 62 | 63 | const vertices = []; 64 | for (i = 0; i < REAL_SIZE; i++) { 65 | vertices.push(Math.random() * 2000 - 1000); 66 | vertices.push(Math.random() * 2000 - 1000); 67 | vertices.push(Math.random() * 2000 - 1000); 68 | } 69 | const vbo = gl.createBuffer(); 70 | gl.bindBuffer(gl.ARRAY_BUFFER, vbo); 71 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 72 | const posAttr = new three.GLBufferAttribute(vbo, gl.FLOAT, 3, 4, REAL_SIZE); 73 | geo.setAttribute('position', posAttr); 74 | geo.setDrawRange(0, REAL_SIZE); 75 | 76 | color = [1, 1, 0.5]; 77 | size = 5; 78 | materials[0] = new three.PointsMaterial({ size: size }); 79 | materials[0].color.setHSL(color[0], color[1], color[2]); 80 | particles = new three.Points(geo, materials[0]); 81 | scene.add(particles); 82 | 83 | return particles; 84 | }; 85 | 86 | 87 | camera.position.z = 1000; 88 | scene.fog = new three.FogExp2(0x000000, 0.0007); 89 | 90 | cloud = addCloud(); 91 | 92 | screen.document.addEventListener('mousemove', onDocumentMouseMove, false); 93 | screen.document.addEventListener('touchstart', onDocumentTouchStart, false); 94 | screen.document.addEventListener('touchmove', onDocumentTouchMove, false); 95 | 96 | screen.document.addEventListener('resize', onWindowResize, false); 97 | 98 | 99 | loop(() => { 100 | var time = Date.now() * 0.00005; 101 | camera.position.x += (mouseX - camera.position.x) * 0.05; 102 | camera.position.y += (-mouseY - camera.position.y) * 0.05; 103 | camera.lookAt(scene.position); 104 | 105 | for (i = 0; i < materials.length; i++) { 106 | color = [1, 1, 0.5]; 107 | h = (360 * (color[0] + time) % 360) / 360; 108 | materials[i].color.setHSL(h, color[1], color[2]); 109 | } 110 | 111 | cloud.rotation.y = time * (i < 4 ? i + 1 : -(i + 1)); 112 | 113 | screen.draw(scene, camera); 114 | }); 115 | -------------------------------------------------------------------------------- /examples/three/post.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | // Based on https://threejs.org/examples/?q=postprocess#webgl_postprocessing_advanced 3 | 4 | import { dirname } from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | import * as THREE from 'three'; 7 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; 8 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; 9 | import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'; 10 | import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js'; 11 | import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'; 12 | import { MaskPass, ClearMaskPass } from 'three/examples/jsm/postprocessing/MaskPass.js'; 13 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; 14 | import { TexturePass } from 'three/examples/jsm/postprocessing/TexturePass.js'; 15 | import { BleachBypassShader } from 'three/examples/jsm/shaders/BleachBypassShader.js'; 16 | import { ColorifyShader } from 'three/examples/jsm/shaders/ColorifyShader.js'; 17 | import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js'; 18 | import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js'; 19 | import { SepiaShader } from 'three/examples/jsm/shaders/SepiaShader.js'; 20 | import { VignetteShader } from 'three/examples/jsm/shaders/VignetteShader.js'; 21 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; 22 | 23 | import { init, addThreeHelpers } from '../../index.js'; 24 | 25 | 26 | const IS_PERF_MODE = true; 27 | 28 | const __dirname = dirname(fileURLToPath(import.meta.url)); 29 | 30 | 31 | const { 32 | window, doc, loop, gl, Image, 33 | } = init({ 34 | isGles3: true, 35 | // isWebGL2: true, 36 | autoEsc: true, 37 | autoFullscreen: true, 38 | vsync: !IS_PERF_MODE, 39 | title: 'Postprocessing', 40 | }); 41 | addThreeHelpers(THREE, gl); 42 | 43 | const icon = new Image(); 44 | icon.src = 'textures/three.png'; 45 | icon.on('load', () => { doc.icon = icon; }); 46 | 47 | let container; 48 | 49 | let composerScene, composer1, composer2, composer3, composer4; 50 | 51 | let cameraOrtho, cameraPerspective, sceneModel, sceneBG, renderer, mesh, directionalLight; 52 | 53 | const width = window.innerWidth || 2; 54 | const height = window.innerHeight || 2; 55 | 56 | let halfWidth = width / 2; 57 | let halfHeight = height / 2; 58 | 59 | let quadBG, quadMask, renderScene; 60 | 61 | const delta = 0.01; 62 | 63 | 64 | container = doc.getElementById('container'); 65 | 66 | // 67 | 68 | cameraOrtho = new THREE.OrthographicCamera(- halfWidth, halfWidth, halfHeight, - halfHeight, - 10000, 10000); 69 | cameraOrtho.position.z = 100; 70 | 71 | cameraPerspective = new THREE.PerspectiveCamera(50, width / height, 1, 10000); 72 | cameraPerspective.position.z = 900; 73 | 74 | // 75 | 76 | sceneModel = new THREE.Scene(); 77 | sceneBG = new THREE.Scene(); 78 | 79 | // 80 | 81 | directionalLight = new THREE.DirectionalLight(0xffffff, 3); 82 | directionalLight.position.set(0, - 0.1, 1).normalize(); 83 | sceneModel.add(directionalLight); 84 | 85 | const loader = new GLTFLoader(); 86 | loader.load('models/LeePerrySmith.glb', function (gltf) { 87 | createMesh(gltf.scene.children[0].geometry, sceneModel, 100); 88 | }); 89 | 90 | // 91 | 92 | const diffuseMap = new THREE.TextureLoader().load('textures/pz.jpg'); 93 | // diffuseMap.colorSpace = THREE.SRGBColorSpace; 94 | 95 | const materialColor = new THREE.MeshBasicMaterial({ 96 | map: diffuseMap, 97 | depthTest: false 98 | }); 99 | 100 | quadBG = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), materialColor); 101 | quadBG.position.z = - 500; 102 | quadBG.scale.set(width, height, 1); 103 | sceneBG.add(quadBG); 104 | 105 | // 106 | 107 | const sceneMask = new THREE.Scene(); 108 | 109 | quadMask = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ color: 0xffaa00 })); 110 | quadMask.position.z = - 300; 111 | quadMask.scale.set(width / 2, height / 2, 1); 112 | sceneMask.add(quadMask); 113 | 114 | // 115 | 116 | // When switching from fullscreen and back, reset renderer to update VAO/FBO objects 117 | const resetRenderer = () => { 118 | if (renderer) { 119 | renderer.dispose(); 120 | } 121 | 122 | renderer = new THREE.WebGLRenderer({ 123 | context: gl, 124 | antialias: true, 125 | canvas: doc, 126 | alpha: true, 127 | }); 128 | 129 | renderer.setPixelRatio(doc.devicePixelRatio); 130 | renderer.setSize(doc.width, doc.height); 131 | renderer.autoClear = false; 132 | 133 | if (composerScene) { 134 | composerScene.renderer = renderer; 135 | composer1.renderer = renderer; 136 | composer2.renderer = renderer; 137 | composer3.renderer = renderer; 138 | composer4.renderer = renderer; 139 | } 140 | }; 141 | 142 | resetRenderer(); 143 | doc.on('mode', resetRenderer); 144 | 145 | // 146 | 147 | container.appendChild(renderer.domElement); 148 | 149 | // 150 | 151 | const shaderBleach = BleachBypassShader; 152 | const shaderSepia = SepiaShader; 153 | const shaderVignette = VignetteShader; 154 | 155 | const effectBleach = new ShaderPass(shaderBleach); 156 | const effectSepia = new ShaderPass(shaderSepia); 157 | const effectVignette = new ShaderPass(shaderVignette); 158 | // const gammaCorrection = new ShaderPass(GammaCorrectionShader); 159 | 160 | effectBleach.uniforms['opacity'].value = 0.95; 161 | 162 | effectSepia.uniforms['amount'].value = 0.9; 163 | 164 | effectVignette.uniforms['offset'].value = 0.95; 165 | effectVignette.uniforms['darkness'].value = 1.6; 166 | 167 | const effectBloom = new BloomPass(0.5); 168 | const effectFilm = new FilmPass(0.35); 169 | const effectFilmBW = new FilmPass(0.35, true); 170 | const effectDotScreen = new DotScreenPass(new THREE.Vector2(0, 0), 0.5, 0.8); 171 | 172 | const effectHBlur = new ShaderPass(HorizontalBlurShader); 173 | const effectVBlur = new ShaderPass(VerticalBlurShader); 174 | effectHBlur.uniforms['h'].value = 2 / (width / 2); 175 | effectVBlur.uniforms['v'].value = 2 / (height / 2); 176 | 177 | const effectColorify1 = new ShaderPass(ColorifyShader); 178 | const effectColorify2 = new ShaderPass(ColorifyShader); 179 | effectColorify1.uniforms['color'] = new THREE.Uniform(new THREE.Color(1, 0.8, 0.8)); 180 | effectColorify2.uniforms['color'] = new THREE.Uniform(new THREE.Color(1, 0.75, 0.5)); 181 | 182 | const clearMask = new ClearMaskPass(); 183 | const renderMask = new MaskPass(sceneModel, cameraPerspective); 184 | const renderMaskInverse = new MaskPass(sceneModel, cameraPerspective); 185 | 186 | renderMaskInverse.inverse = true; 187 | 188 | // 189 | 190 | const rtParameters = { stencilBuffer: true }; 191 | const rtWidth = width / 2; 192 | const rtHeight = height / 2; 193 | 194 | // 195 | 196 | composerScene = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth * 2, rtHeight * 2, rtParameters)); 197 | 198 | const renderBackground = new RenderPass(sceneBG, cameraOrtho); 199 | const renderModel = new RenderPass(sceneModel, cameraPerspective); 200 | 201 | renderModel.clear = false; 202 | 203 | composerScene.addPass(renderBackground); 204 | composerScene.addPass(renderModel); 205 | composerScene.addPass(renderMaskInverse); 206 | composerScene.addPass(effectHBlur); 207 | composerScene.addPass(effectVBlur); 208 | composerScene.addPass(clearMask); 209 | 210 | // 211 | 212 | renderScene = new TexturePass(composerScene.renderTarget2.texture); 213 | 214 | // 215 | 216 | composer1 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); 217 | 218 | composer1.addPass(renderScene); 219 | // composer1.addPass(gammaCorrection); 220 | composer1.addPass(effectFilmBW); 221 | composer1.addPass(effectVignette); 222 | 223 | // 224 | 225 | composer2 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); 226 | 227 | composer2.addPass(renderScene); 228 | // composer2.addPass(gammaCorrection); 229 | composer2.addPass(effectDotScreen); 230 | composer2.addPass(renderMask); 231 | composer2.addPass(effectColorify1); 232 | composer2.addPass(clearMask); 233 | composer2.addPass(renderMaskInverse); 234 | composer2.addPass(effectColorify2); 235 | composer2.addPass(clearMask); 236 | composer2.addPass(effectVignette); 237 | 238 | // 239 | 240 | composer3 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); 241 | 242 | composer3.addPass(renderScene); 243 | // composer3.addPass(gammaCorrection); 244 | composer3.addPass(effectSepia); 245 | composer3.addPass(effectFilm); 246 | composer3.addPass(effectVignette); 247 | 248 | // 249 | 250 | composer4 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); 251 | 252 | composer4.addPass(renderScene); 253 | // composer4.addPass(gammaCorrection); 254 | composer4.addPass(effectBloom); 255 | composer4.addPass(effectFilm); 256 | composer4.addPass(effectBleach); 257 | composer4.addPass(effectVignette); 258 | 259 | renderScene.uniforms['tDiffuse'].value = composerScene.renderTarget2.texture; 260 | 261 | window.addEventListener('resize', onWindowResize); 262 | 263 | 264 | function onWindowResize() { 265 | halfWidth = window.innerWidth / 2; 266 | halfHeight = window.innerHeight / 2; 267 | 268 | cameraPerspective.aspect = window.innerWidth / window.innerHeight; 269 | cameraPerspective.updateProjectionMatrix(); 270 | 271 | cameraOrtho.left = - halfWidth; 272 | cameraOrtho.right = halfWidth; 273 | cameraOrtho.top = halfHeight; 274 | cameraOrtho.bottom = - halfHeight; 275 | 276 | cameraOrtho.updateProjectionMatrix(); 277 | 278 | renderer.setSize(window.innerWidth, window.innerHeight); 279 | 280 | composerScene.setSize(halfWidth * 2, halfHeight * 2); 281 | 282 | composer1.setSize(halfWidth, halfHeight); 283 | composer2.setSize(halfWidth, halfHeight); 284 | composer3.setSize(halfWidth, halfHeight); 285 | composer4.setSize(halfWidth, halfHeight); 286 | 287 | renderScene.uniforms['tDiffuse'].value = composerScene.renderTarget2.texture; 288 | 289 | quadBG.scale.set(window.innerWidth, window.innerHeight, 1); 290 | quadMask.scale.set(window.innerWidth / 2, window.innerHeight / 2, 1); 291 | } 292 | 293 | function createMesh(geometry, scene, scale) { 294 | const diffuseMap = new THREE.TextureLoader().load(__dirname + '/textures/Map-COL.jpg'); 295 | // diffuseMap.colorSpace = THREE.SRGBColorSpace; 296 | 297 | const mat2 = new THREE.MeshPhongMaterial({ 298 | 299 | color: 0xcbcbcb, 300 | specular: 0x080808, 301 | shininess: 20, 302 | map: diffuseMap, 303 | normalMap: new THREE.TextureLoader().load(__dirname + '/textures/Infinite-Level_02_Tangent_SmoothUV.jpg'), 304 | normalScale: new THREE.Vector2(0.75, 0.75) 305 | 306 | }); 307 | 308 | mesh = new THREE.Mesh(geometry, mat2); 309 | mesh.position.set(0, - 50, 0); 310 | mesh.scale.set(scale, scale, scale); 311 | 312 | scene.add(mesh); 313 | } 314 | 315 | 316 | let prevTime = Date.now(); 317 | let frames = 0; 318 | 319 | loop((now) => { 320 | const timedRotation = now * 0.0004; 321 | 322 | if (mesh) mesh.rotation.y = -timedRotation; 323 | 324 | renderer.setViewport(0, 0, halfWidth, halfHeight); 325 | composerScene.render(delta); 326 | 327 | renderer.setViewport(0, 0, halfWidth, halfHeight); 328 | composer1.render(delta); 329 | 330 | renderer.setViewport(halfWidth, 0, halfWidth, halfHeight); 331 | composer2.render(delta); 332 | 333 | renderer.setViewport(0, halfHeight, halfWidth, halfHeight); 334 | composer3.render(delta); 335 | 336 | renderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight); 337 | composer4.render(delta); 338 | 339 | if (!IS_PERF_MODE) { 340 | return; 341 | } 342 | 343 | frames++; 344 | if (now >= prevTime + 2000) { 345 | console.log( 346 | 'FPS:', Math.floor((frames * 1000) / (now - prevTime)), 347 | ); 348 | prevTime = now; 349 | frames = 0; 350 | } 351 | }); 352 | -------------------------------------------------------------------------------- /examples/three/textures/Infinite-Level_02_Tangent_SmoothUV.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/textures/Infinite-Level_02_Tangent_SmoothUV.jpg -------------------------------------------------------------------------------- /examples/three/textures/Map-COL.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/textures/Map-COL.jpg -------------------------------------------------------------------------------- /examples/three/textures/crate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/textures/crate.gif -------------------------------------------------------------------------------- /examples/three/textures/pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/textures/pz.jpg -------------------------------------------------------------------------------- /examples/three/textures/three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/examples/three/textures/three.png -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "moduleResolution": "nodenext", 5 | "module": "nodenext", 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "noImplicitOverride": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "allowUnreachableCode": false, 16 | "allowImportingTsExtensions": true, 17 | } 18 | } -------------------------------------------------------------------------------- /examples/utils/debug-shaders.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | 4 | export const debugShaders = (renderer: THREE.WebGLRenderer, isEnabled: boolean): void => { 5 | renderer.debug.checkShaderErrors = isEnabled; 6 | 7 | if (!isEnabled) { 8 | renderer.debug.onShaderError = null; 9 | return; 10 | } 11 | 12 | renderer.debug.onShaderError = ( 13 | gl: WebGLRenderingContext, 14 | _program: WebGLProgram, 15 | vs: WebGLShader, 16 | fs: WebGLShader, 17 | ) => { 18 | const parseForErrors = (shader: WebGLShader, name: string) => { 19 | const errors = (gl.getShaderInfoLog(shader) || '').trim(); 20 | const prefix = 'Errors in ' + name + ':' + '\n\n' + errors; 21 | 22 | if (errors !== '') { 23 | const code = (gl.getShaderSource(shader) || '').replace(/\t/g, ' '); 24 | const lines = code.split('\n'); 25 | var linedCode = ''; 26 | var i = 1; 27 | for (var line of lines) { 28 | linedCode += (i < 10 ? ' ' : '') + i + ':\t\t' + line + '\n'; 29 | i++; 30 | } 31 | 32 | console.error(prefix + '\n' + linedCode); 33 | } 34 | }; 35 | 36 | parseForErrors(vs, 'Vertex Shader'); 37 | parseForErrors(fs, 'Fragment Shader'); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /examples/utils/perf.ts: -------------------------------------------------------------------------------- 1 | let prevTime = Date.now(); 2 | let frames = 0; 3 | 4 | export const countFrame = (now: number) => { 5 | frames++; 6 | if (now >= prevTime + 2000) { 7 | console.log( 8 | 'FPS:', Math.floor((frames * 1000) / (now - prevTime)), 9 | ); 10 | prevTime = now; 11 | frames = 0; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace node3d { 2 | type EventEmitter = import('node:events').EventEmitter; 3 | type Img = typeof import('image-raub'); 4 | type TThree = typeof import('three'); 5 | type TScene = import('three').Scene; 6 | type TRenderer = import('three').WebGLRenderer; 7 | type TCamera = import('three').Camera; 8 | type TWebgl = typeof import('webgl-raub'); 9 | type TImage = typeof import('image-raub'); 10 | type TGlfw = typeof import('glfw-raub'); 11 | type TDocumentOpts = import('glfw-raub').TDocumentOpts; 12 | type Document = import('glfw-raub').Document; 13 | type Window = import('glfw-raub').Window; 14 | 15 | 16 | type TUnknownObject = Readonly<{ [id: string]: unknown }>; 17 | 18 | class WebVRManager { 19 | readonly enabled: boolean; 20 | 21 | constructor(); 22 | 23 | isPresenting(): boolean; 24 | dispose(): void; 25 | setAnimationLoop(): void; 26 | getCamera(): TUnknownObject; 27 | submitFrame(): void; 28 | } 29 | 30 | type TLocation = Readonly<{ 31 | href: string, 32 | ancestorOrigins: TUnknownObject, 33 | origin: string, 34 | protocol: string, 35 | host: string, 36 | hostname: string, 37 | port: string, 38 | pathname: string, 39 | search: string, 40 | hash: string, 41 | }>; 42 | 43 | type TNavigator = Readonly<{ 44 | appCodeName: string, 45 | appName: string, 46 | appVersion: string, 47 | bluetooth: TUnknownObject, 48 | clipboard: TUnknownObject, 49 | connection: { 50 | onchange: unknown, 51 | effectiveType: string, 52 | rtt: number, 53 | downlink: number, 54 | saveData: boolean, 55 | }, 56 | cookieEnabled: boolean, 57 | credentials: TUnknownObject, 58 | deviceMemory: number, 59 | doNotTrack: unknown, 60 | geolocation: TUnknownObject, 61 | hardwareConcurrency: number, 62 | keyboard: TUnknownObject, 63 | language: string, 64 | languages: string[], 65 | locks: TUnknownObject, 66 | maxTouchPoints: number, 67 | mediaCapabilities: TUnknownObject, 68 | mediaDevices: { ondevicechange: unknown }, 69 | mimeTypes: { length: number }, 70 | onLine: boolean, 71 | permissions: TUnknownObject, 72 | platform: string, 73 | plugins: { length: number }, 74 | presentation: { defaultRequest: unknown, receiver: unknown }, 75 | product: string, 76 | productSub: string, 77 | serviceWorker: { 78 | ready: Promise, 79 | controller: unknown, 80 | oncontrollerchange: unknown, 81 | onmessage: unknown 82 | }, 83 | storage: TUnknownObject, 84 | usb: { 85 | onconnect: unknown, 86 | ondisconnect: unknown, 87 | }, 88 | userAgent: string, 89 | vendor: string, 90 | vendorSub: string, 91 | webkitPersistentStorage: TUnknownObject, 92 | webkitTemporaryStorage: TUnknownObject, 93 | }>; 94 | 95 | type TScreenOpts = Readonly<{ 96 | three?: TThree, 97 | THREE?: TThree, 98 | gl?: TWebgl, 99 | doc?: Document, 100 | document?: Document, 101 | Image?: TImage, 102 | title?: string, 103 | camera?: TCamera, 104 | scene?: TScene, 105 | renderer?: TRenderer, 106 | fov?: number, 107 | near?: number, 108 | far?: number, 109 | z?: number, 110 | }>; 111 | 112 | export class Screen implements EventEmitter { 113 | constructor(opts?: TScreenOpts); 114 | 115 | readonly context: TWebgl; 116 | readonly three: TThree; 117 | readonly renderer: TRenderer; 118 | readonly scene: TScene; 119 | readonly camera: TCamera; 120 | 121 | readonly document: Document; 122 | readonly canvas: Document; 123 | 124 | readonly width: number; 125 | readonly height: number; 126 | readonly w: number; 127 | readonly h: number; 128 | readonly size: TThree['Vector2']; 129 | 130 | title: string; 131 | icon: Document['icon']; 132 | fov: number; 133 | mode: Document['mode']; 134 | 135 | draw(): void; 136 | snapshot(name?: string): void; 137 | 138 | 139 | // ------ implements EventEmitter 140 | 141 | addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; 142 | on(eventName: string | symbol, listener: (...args: any[]) => void): this; 143 | once(eventName: string | symbol, listener: (...args: any[]) => void): this; 144 | removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; 145 | off(eventName: string | symbol, listener: (...args: any[]) => void): this; 146 | removeAllListeners(event?: string | symbol): this; 147 | setMaxListeners(n: number): this; 148 | getMaxListeners(): number; 149 | listeners(eventName: string | symbol): Function[]; 150 | rawListeners(eventName: string | symbol): Function[]; 151 | emit(eventName: string | symbol, ...args: any[]): boolean; 152 | listenerCount(eventName: string | symbol, listener?: Function): number; 153 | prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this; 154 | prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this; 155 | eventNames(): Array; 156 | } 157 | 158 | 159 | type TCore3D = { 160 | /** 161 | * Almost the same as `Image` in a browser. Also `document.createElement('img')` 162 | * does the same thing as `new Image()`. For more info see 163 | * [image-raub](https://github.com/node-3d/image-raub#image-for-nodejs). 164 | */ 165 | Image: Img, 166 | 167 | /** 168 | * This constructor spawns a new platform window **with a web-document like interface**. 169 | * For more info see [glfw-raub](https://github.com/node-3d/glfw-raub#class-document). 170 | */ 171 | Document: TGlfw['Document'], 172 | 173 | /** 174 | * This constructor spawns a new OS window. 175 | * For more info see [glfw-raub](https://github.com/node-3d/glfw-raub#class-window). 176 | */ 177 | Window: TGlfw['Window'], 178 | 179 | /** 180 | * A WebGL context instance. This is **almost** the same as real WebGL stuff. 181 | * For more info see [webgl-raub](https://github.com/node-3d/webgl-raub#webgl-for-nodejs). 182 | */ 183 | gl: TWebgl, 184 | 185 | /** 186 | * Low level GLFW interface. 187 | * For more info see glfw-raub](https://github.com/node-3d/glfw-raub#glfw-for-nodejs) 188 | */ 189 | glfw: TGlfw, 190 | 191 | /** 192 | * The default instance of Document - created automatically when `init()` is called. 193 | */ 194 | doc: Document, 195 | 196 | /** 197 | * @alias doc 198 | */ 199 | canvas: Document, 200 | /** 201 | * @alias doc 202 | */ 203 | document: Document, 204 | /** 205 | * @alias doc 206 | */ 207 | window: Document, 208 | 209 | /** 210 | * Optimized loop method that will continuously generate frames with given `cb`. 211 | * 212 | * The returned function may be called to break the loop. 213 | * @alias doc.loop 214 | */ 215 | loop(cb: (dateNow: number) => void): (() => void); 216 | 217 | /** 218 | * Similar to `requestAnimationFrame` on web. It uses `setImmediate`, and can 219 | * therefore be cancelled (with `doc.cancelAnimationFrame`). 220 | * 221 | * On immediate, it will process events (input), then call `cb`, then swap buffers. 222 | * Swap buffers is blocking when VSYNC is on - that's how FPS is sync'd. 223 | * @alias doc.requestAnimationFrame 224 | */ 225 | requestAnimationFrame: (cb: (dateNow: number) => void) => number, 226 | 227 | /** 228 | * A wrapper for `document` that automates some common operations. 229 | * 230 | * * Pass `three`, or `THREE`. If not, it will refer to `global.THREE` 231 | * * Pass your own `camera`, or it will create a new one - with default parameters, or yours. 232 | * * Pass your `scene`, or it will create a new one. 233 | * * Pass a `renderer`, or don't - that's just fine. 234 | * * Call `screen.draw()` - equivalent of `renderer.render(scene, camera)`. 235 | * 236 | * It will also propagate the `document` input events and handle the `'mode'` event. 237 | * The latter is important to correctly update any VAO-based geometry. The `'mode'` 238 | * event will be propagated after necessary handling (re-creation of `renderer`). 239 | */ 240 | Screen: typeof Screen, 241 | }; 242 | 243 | type TPluginDecl = string | ((core3d: TCore3D) => void) | Readonly<{ name: string, opts: TUnknownObject }>; 244 | 245 | type TInitOpts = TDocumentOpts & Readonly<{ 246 | /** 247 | * Use GLES 3.2 profile instead of default. 248 | * 249 | * In this mode, shader augmentation is disabled, as the context becomes compatible 250 | * with WebGL API set (as GLES intended). Some things, such as 251 | * `texture.colorSpace = three.SRGBColorSpace;` work better or even exclusively 252 | * in this mode. 253 | */ 254 | isGles3?: boolean, 255 | 256 | /** 257 | * EXPERIMENTAL. Defines WebGL2 context, so three.js uses WebGL2 pathways. 258 | * 259 | * At this point, most of the stuff stops working in this mode. Some additional 260 | * shader tweaks or API exports may be required to fully support running web 261 | * libs in WebGL2 mode. 262 | * 263 | * Note: for non-web libs this has no effect, since it only affects common "isWebGL2" checks. 264 | */ 265 | isWebGL2?: boolean, 266 | 267 | /** 268 | * Is default window visible? 269 | * 270 | * For "headless" mode, use `false`. The window will be created in GLFW hidden mode 271 | * (this is how headless GL works anyway). The default value is `true` - visible window. 272 | */ 273 | isVisible?: boolean, 274 | 275 | /** 276 | * An override for WebGL implementation. 277 | */ 278 | webgl?: TWebgl, 279 | 280 | /** 281 | * An override for Image implementation. 282 | */ 283 | Image?: Img, 284 | 285 | /** 286 | * An override for GLFW implementation. 287 | */ 288 | glfw?: TGlfw, 289 | 290 | /** 291 | * An override for the `location` object. 292 | */ 293 | location?: TLocation, 294 | 295 | /** 296 | * An override for the `navigator` object. 297 | */ 298 | navigator?: TNavigator, 299 | 300 | /** 301 | * An override for the `WebVRManager` object. 302 | */ 303 | WebVRManager?: WebVRManager, 304 | }>; 305 | 306 | /** 307 | * Initialize Node3D. Creates the first window/document and sets up the global environment. 308 | * This function can be called repeatedly, but will ignore further calls. 309 | * The return value is cached and will be returned immediately for repeating calls. 310 | */ 311 | export const init: (opts?: TInitOpts) => TCore3D; 312 | 313 | /** 314 | * Teaches `three.FileLoader.load` to work with Node `fs`. Additionally implements 315 | * `three.Texture.fromId` static method to create THREE textures from known GL resource IDs. 316 | */ 317 | export const addThreeHelpers: (three: TThree, gl: TWebgl) => void; 318 | 319 | } 320 | 321 | export = node3d; 322 | 323 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./js'); 4 | -------------------------------------------------------------------------------- /js/core/location.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const location = { 5 | href: 'https://www.google.com/_/chrome/newtab?ie=UTF-8', 6 | ancestorOrigins: {}, 7 | origin: 'https://www.google.com', 8 | protocol: 'https:', 9 | host: 'www.google.com', 10 | hostname: 'www.google.com', 11 | port: '', 12 | pathname: '/_/chrome/newtab', 13 | search: '?ie=UTF-8', 14 | hash: '' 15 | }; 16 | 17 | 18 | module.exports = location; 19 | -------------------------------------------------------------------------------- /js/core/navigator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const navigator = { 5 | appCodeName: 'Mozilla', 6 | appName: 'Netscape', 7 | appVersion: 'Node3D', 8 | bluetooth: {}, 9 | clipboard: {}, 10 | connection: { 11 | onchange: null, effectiveType: '4g', rtt: 50, downlink: 3.3, saveData: false 12 | }, 13 | cookieEnabled: false, 14 | credentials: {}, 15 | deviceMemory: 8, 16 | doNotTrack: null, 17 | geolocation: {}, 18 | hardwareConcurrency: 4, 19 | keyboard: {}, 20 | language: 'en', 21 | languages: ['en', 'en-US'], 22 | locks: {}, 23 | maxTouchPoints: 0, 24 | mediaCapabilities: {}, 25 | mediaDevices: { ondevicechange: null }, 26 | mimeTypes: { length: 0 }, 27 | onLine: false, 28 | permissions: {}, 29 | platform: 'Any', 30 | plugins: { length: 0 }, 31 | presentation: { defaultRequest: null, receiver: null }, 32 | product: 'Node3D', 33 | productSub: '1', 34 | serviceWorker: { 35 | ready: Promise.resolve(false), 36 | controller: null, 37 | oncontrollerchange: null, 38 | onmessage: null 39 | }, 40 | storage: {}, 41 | usb: { onconnect: null, ondisconnect: null }, 42 | userAgent: 'Mozilla/Node3D', 43 | vendor: 'Node3D', 44 | vendorSub: '', 45 | webkitPersistentStorage: {}, 46 | webkitTemporaryStorage: {}, 47 | }; 48 | 49 | 50 | module.exports = navigator; 51 | -------------------------------------------------------------------------------- /js/core/threejs-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Blob } = require('node:buffer'); 4 | 5 | 6 | const finishLoad = (three, responseType, mimeType, onLoad, buffer) => { 7 | if (responseType === 'arraybuffer') { 8 | onLoad((new Uint8Array(buffer)).buffer); 9 | return; 10 | } 11 | 12 | if (responseType === 'blob') { 13 | onLoad(new Blob([buffer])); 14 | return; 15 | } 16 | 17 | if (responseType === 'document') { 18 | onLoad({}); 19 | return; 20 | } 21 | 22 | if (responseType === 'json') { 23 | try { 24 | onLoad(JSON.parse(buffer.toString())); 25 | } catch (_e) { 26 | onLoad({}); 27 | } 28 | return; 29 | } 30 | 31 | if (!mimeType) { 32 | onLoad(buffer.toString()); 33 | return; 34 | } 35 | 36 | // sniff encoding 37 | const re = /charset="?([^;"\s]*)"?/i; 38 | const exec = re.exec(mimeType); 39 | const label = exec && exec[1] ? exec[1].toLowerCase() : undefined; 40 | const decoder = new three.TextDecoder(label); 41 | 42 | onLoad(decoder.decode(buffer)); 43 | }; 44 | 45 | 46 | module.exports = (three, gl) => { 47 | three.FileLoader.prototype.load = function (url, onLoad, onProgress, onError) { 48 | // Data URI 49 | if (/^data:/.test(url)) { 50 | const [head, body] = url.split(','); 51 | const isBase64 = head.indexOf('base64') > -1; 52 | const data = isBase64 ? Buffer.from(body, 'base64') : Buffer.from(unescape(body)); 53 | finishLoad(three, this.responseType, this.mimeType, onLoad, data); 54 | return; 55 | } 56 | 57 | // Remote URI 58 | if (/^https?:\/\//i.test(url)) { 59 | const download = require('addon-tools-raub/download'); 60 | 61 | download(url).then( 62 | (data) => finishLoad(three, this.responseType, this.mimeType, onLoad, data), 63 | (err) => typeof onError === 'function' ? onError(err) : console.error(err) 64 | ); 65 | 66 | return; 67 | } 68 | 69 | // Filesystem URI 70 | if (this.path !== undefined) { 71 | url = this.path + url; 72 | } 73 | require('fs').readFile(url, (err, data) => { 74 | if (err) { 75 | return typeof onError === 'function' ? onError(err) : console.error(err); 76 | } 77 | finishLoad(three, this.responseType, this.mimeType, onLoad, data); 78 | }); 79 | }; 80 | 81 | three.Texture.fromId = (id, renderer) => { 82 | const rawTexture = gl.createTexture(); 83 | rawTexture._ = id; 84 | 85 | const texture = new three.Texture(); 86 | 87 | let properties = null; 88 | if (!renderer.properties) { 89 | properties = texture; 90 | } else { 91 | properties = renderer.properties.get(texture); // !!!! 92 | } 93 | 94 | properties.__webglTexture = rawTexture; 95 | properties.__webglInit = true; 96 | 97 | return texture; 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /js/core/vr-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | class WebVRManager { 5 | get enabled() { return false; } 6 | 7 | constructor() {} 8 | 9 | isPresenting() { return false; } 10 | dispose() {} 11 | setAnimationLoop() {} 12 | getCamera() { return {}; } 13 | submitFrame() {} 14 | } 15 | 16 | 17 | module.exports = WebVRManager; 18 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const _init = (_opts = {}) => { 5 | const opts = { 6 | mode: 'windowed', 7 | vsync: true, 8 | webgl: _opts.webgl || require('webgl-raub'), 9 | Image: _opts.Image || require('image-raub'), 10 | glfw: _opts.glfw || require('glfw-raub'), 11 | location: _opts.location || require('./core/location'), 12 | navigator: _opts.navigator || require('./core/navigator'), 13 | WebVRManager: _opts.WebVRManager || require('./core/vr-manager'), 14 | ..._opts, 15 | }; 16 | 17 | const { 18 | webgl, 19 | Image, 20 | glfw, 21 | location, 22 | navigator, 23 | WebVRManager, 24 | isWebGL2, 25 | isGles3, 26 | isVisible, 27 | ...optsDoc 28 | } = opts; 29 | 30 | const { Document, Window } = glfw; 31 | 32 | Document.setWebgl(webgl); 33 | Document.setImage(Image); 34 | if (!Image.prototype.fillRect) { 35 | Image.prototype.fillRect = () => {}; 36 | } 37 | 38 | if (isWebGL2) { 39 | webgl.useWebGL2(); 40 | } 41 | 42 | const onBeforeWindow = (window, glfw) => { 43 | if (isGles3) { 44 | glfw.windowHint(glfw.OPENGL_PROFILE, glfw.OPENGL_ANY_PROFILE); 45 | glfw.windowHint(glfw.CONTEXT_VERSION_MAJOR, 3); 46 | glfw.windowHint(glfw.CONTEXT_VERSION_MINOR, 2); 47 | glfw.windowHint(glfw.CLIENT_API, glfw.OPENGL_ES_API); 48 | } 49 | 50 | if (isVisible === false) { 51 | glfw.windowHint(glfw.VISIBLE, glfw.FALSE); 52 | } 53 | 54 | if (optsDoc.onBeforeWindow) { 55 | optsDoc.onBeforeWindow(window, glfw); 56 | } 57 | }; 58 | 59 | if (!isGles3) { 60 | const shaderSource = webgl.shaderSource; 61 | webgl.shaderSource = (shader, code) => shaderSource( 62 | shader, 63 | code.replace( 64 | /^\s*?(#version|precision).*?($|;)/gm, '' 65 | ).replace( 66 | /^/, '#extension GL_ARB_shading_language_420pack : require\n' 67 | ).replace( 68 | /^/, '#extension GL_ARB_explicit_attrib_location : enable\n' 69 | ).replace( 70 | /^/, '#version 140\n' 71 | ).replace( 72 | /gl_FragDepthEXT/g, 'gl_FragDepth' 73 | ).replace( 74 | '#extension GL_EXT_frag_depth : enable', '' 75 | ).replace( 76 | /\bhighp\s+/g, '' 77 | ) 78 | ); 79 | } 80 | 81 | const doc = new Document({ ...optsDoc, onBeforeWindow }); 82 | 83 | if (!global.self) { 84 | global.self = global; 85 | } 86 | 87 | if (!global.globalThis) { 88 | global.globalThis = global; 89 | } 90 | 91 | global.document = doc; 92 | global.window = doc; 93 | global.body = doc; 94 | global.cwrap = null; 95 | global.addEventListener = doc.addEventListener.bind(doc); 96 | global.removeEventListener = doc.removeEventListener.bind(doc); 97 | global.requestAnimationFrame = doc.requestAnimationFrame; 98 | global.cancelAnimationFrame = doc.cancelAnimationFrame; 99 | 100 | if (!global.location) { 101 | global.location = location; 102 | } 103 | doc.location = global.location; 104 | 105 | if (!global.navigator) { 106 | global.navigator = navigator; 107 | } 108 | 109 | global.WebVRManager = WebVRManager; 110 | global.Image = Image; 111 | global._gl = webgl; 112 | 113 | webgl.canvas = doc; 114 | 115 | const core3d = { 116 | Image, 117 | Document, 118 | Window, 119 | gl: webgl, 120 | glfw, 121 | doc, 122 | canvas: doc, 123 | document: doc, 124 | window: doc, 125 | loop: doc.loop, 126 | requestAnimationFrame : doc.requestAnimationFrame, 127 | addThreeHelpers, 128 | ...require('./math'), 129 | ...require('./objects'), 130 | }; 131 | 132 | return core3d; 133 | }; 134 | 135 | 136 | let inited = null; 137 | const init = (opts) => { 138 | if (inited) { 139 | return inited; 140 | } 141 | inited = _init(opts); 142 | return inited; 143 | }; 144 | 145 | const addThreeHelpers = (three, webgl) => { 146 | require('./core/threejs-helpers')(three, webgl); 147 | }; 148 | 149 | module.exports = { 150 | init, 151 | addThreeHelpers, 152 | }; 153 | -------------------------------------------------------------------------------- /js/math/color.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const Vec4 = require('./vec4'); 5 | const Vec3 = require('./vec3'); 6 | 7 | /** 8 | * Four-component color 9 | * @author Luis Blanco 10 | * @extends Vec4 11 | */ 12 | class Color extends Vec4 { 13 | /** 14 | * @constructs Color 15 | * @desc Takes four numbers, or single array, or an object with `.r`, `.g`, `.b`, and `.a` properties. 16 | * - If no arguments passed, constructs `Color(0, 0, 0, 1)`. 17 | * - If only one number is given, constructs `Color(r, r, r, 1)`. 18 | * - If only two numbers are given, constructs `Color(r, r, r, g)`. 19 | * - If three numbers are given, constructs `Color(r, g, b, 1)`. 20 | * @arg {number|number[]|object} [x=0] 21 | * @arg {Number} [y=0] 22 | * @arg {Number} [z=0] 23 | * @arg {Number} [w=0] 24 | */ 25 | constructor() { 26 | const args = arguments; 27 | 28 | let method = 'rgbaFromEmpty'; 29 | 30 | if (args.length) { 31 | if (typeof args[0] === 'object') { 32 | method = 'rgbaFromObject'; 33 | } else if (typeof args[0] === 'number' && args[0] < 256) { 34 | method = 'rgbaFromFloats'; 35 | } else if (typeof args[0] === 'number' || typeof args[0] === 'string') { 36 | method = 'rgbaFromString'; 37 | } 38 | } 39 | 40 | const { r, g, b, a } = Color[method](args); 41 | 42 | super(r, g, b, a); 43 | } 44 | 45 | 46 | static clampTo1(x) { 47 | return x > 1 ? x / 255 : x; 48 | } 49 | 50 | 51 | static rgbaFromEmpty() { 52 | let r = 0, g = 0, b = 0, a = 1; 53 | return { r, g, b, a }; 54 | } 55 | 56 | 57 | static rgbaFromObject(args) { 58 | let r = 0, g = 0, b = 0, a = 1; 59 | 60 | if (args[0] === null) { 61 | return { r, g, b, a }; 62 | } 63 | 64 | // [] or {} or Vec2 65 | if (args[0].constructor === Array || args[0].constructor === Color) { 66 | r = args[0][0]; 67 | g = args[0][1]; 68 | b = args[0][2]; 69 | a = typeof args[0][3] === 'number' ? args[0][3] : 1; 70 | } else if ( 71 | typeof args[0].r === 'number' && typeof args[0].g === 'number' && 72 | typeof args[0].b === 'number' 73 | ) { 74 | r = args[0].r; 75 | g = args[0].g; 76 | b = args[0].b; 77 | a = typeof args[0].a === 'number' ? args[0].a : 1; 78 | } else if (args[0].constructor === Vec3) { 79 | r = args[0].x; 80 | g = args[0].y; 81 | b = args[0].z; 82 | a = typeof args[1] === 'number' ? args[1] : 1; 83 | } 84 | 85 | r = Color.clampTo1(r); 86 | g = Color.clampTo1(g); 87 | b = Color.clampTo1(b); 88 | a = Color.clampTo1(a); 89 | 90 | return { r, g, b, a }; 91 | } 92 | 93 | 94 | static rgbaFromFloats(args) { 95 | let r = 0, g = 0, b = 0, a = 1; 96 | 97 | if (isNaN(args[0])) { 98 | return { r, g, b, a }; 99 | } 100 | 101 | r = args[0]; 102 | g = (typeof args[1] === 'number' && typeof args[2] === 'number') ? args[1] : args[0]; 103 | b = (typeof args[2] === 'number') ? args[2] : args[0]; 104 | 105 | if (typeof args[3] === 'number') { 106 | a = args[3]; 107 | } else if (typeof args[2] === 'number') { 108 | a = 1; 109 | } else if (typeof args[1] === 'number') { 110 | a = args[1]; 111 | } else { 112 | a = 1; 113 | } 114 | 115 | r = Color.clampTo1(r); 116 | g = Color.clampTo1(g); 117 | b = Color.clampTo1(b); 118 | a = Color.clampTo1(a); 119 | 120 | return { r, g, b, a }; 121 | } 122 | 123 | 124 | static rgbaFromString(args) { 125 | let r = 0, g = 0, b = 0, a = 1; 126 | 127 | let rest = 0; 128 | 129 | if (typeof args[0] === 'string') { 130 | rest = parseInt(args[0], 16); 131 | } else { 132 | rest = args[0]; 133 | } 134 | 135 | if (isNaN(rest)) { 136 | return { r, g, b, a }; 137 | } 138 | 139 | if (args[0] > 256 * 256 * 256) { 140 | a = rest % 256; 141 | rest = Math.floor(rest / 256); 142 | } 143 | 144 | b = rest % 256; rest = Math.floor(rest / 256); 145 | g = rest % 256; rest = Math.floor(rest / 256); 146 | r = rest % 256; 147 | 148 | r = Color.clampTo1(r); 149 | g = Color.clampTo1(g); 150 | b = Color.clampTo1(b); 151 | a = Color.clampTo1(a); 152 | 153 | return { r, g, b, a }; 154 | } 155 | 156 | 157 | /** 158 | * The value of colors' red component 159 | * @return {Number} 160 | */ 161 | get r() { return this.x; } 162 | set r(_r) { this.x = _r; } 163 | 164 | 165 | /** 166 | * The value of colors' green component 167 | * @return {Number} 168 | */ 169 | get g() { return this.y; } 170 | set g(_g) { this.y = _g; } 171 | 172 | 173 | /** 174 | * The value of colors' blue component 175 | * @return {Number} 176 | */ 177 | get b() { return this.z; } 178 | set b(_b) { this.z = _b; } 179 | 180 | 181 | /** 182 | * The value of colors' alpha component 183 | * @return {Number} 184 | */ 185 | get a() { return this.w; } 186 | set a(_a) { this.w = _a; } 187 | 188 | 189 | /** 190 | * The **new** color, constructed as `Color(this.r, this.g, this.b)` 191 | * @return {Color} rgb 192 | */ 193 | get rgb() { return new Color(this.r, this.g, this.b); } 194 | set rgb(_rgb) { this.xyz = _rgb; } 195 | 196 | 197 | /** 198 | * The **new** color, constructed as `Color(this.r, this.g, this.b, this.a)` 199 | * @return {Color} rgb 200 | */ 201 | get rgba() { return new Color(this.r, this.g, this.b, this.a); } 202 | set rgba(_rgba) { this.xyzw = _rgba; } 203 | 204 | 205 | /** 206 | * The value of colors' alpha component 207 | * @return {Number} 208 | */ 209 | get opacity() { return this.a; } 210 | 211 | 212 | /** 213 | * Integer representation of the color (without alpha) 214 | * @return {Number} color 215 | */ 216 | get hex() { 217 | const scaled = this.scale(255).rounded; 218 | return scaled.b + 256 * scaled.g + 256 * 256 * scaled.r; 219 | } 220 | 221 | /** 222 | * Integer representation of the color (without alpha) 223 | * @return {Number} color 224 | */ 225 | toHex() { 226 | return this.hex; 227 | } 228 | 229 | /** 230 | * Integer representation of the color (including alpha) 231 | * @return {Number} color 232 | */ 233 | get hexA() { 234 | return Math.round(255 * this.a) + 256 * this.toHex; 235 | } 236 | 237 | /** 238 | * Integer representation of the color (including alpha) 239 | * @return {Number} color 240 | */ 241 | toHexA() { 242 | return this.hexA; 243 | } 244 | 245 | 246 | /** 247 | * String representation of the color (without alpha) 248 | * @return {String} color 249 | */ 250 | toString() { 251 | const r = Math.round(255 * this.r); 252 | return (r > 15 ? '' : '0') + this.hex; 253 | } 254 | 255 | /** 256 | * String representation of the color (including alpha) 257 | * @return {String} color 258 | */ 259 | toStringA() { 260 | const r = Math.round(255 * this.r); 261 | return (r > 15 ? '' : '0') + this.hexA; 262 | } 263 | } 264 | 265 | module.exports = Color; 266 | -------------------------------------------------------------------------------- /js/math/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = { 5 | Color : require('./color'), 6 | Vec2 : require('./vec2'), 7 | Vec3 : require('./vec3'), 8 | Vec4 : require('./vec4'), 9 | }; 10 | -------------------------------------------------------------------------------- /js/math/vec3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Vec2 = require('./vec2'); 4 | 5 | 6 | /** 7 | * Three-dimensional vector 8 | * @note All 'ed() methods modify **INPLACE**, no-'ed methods - **MAKE A COPY** 9 | * @author Luis Blanco 10 | * @extends Vec2 11 | */ 12 | class Vec3 extends Vec2 { 13 | /** 14 | * @constructs Vec3 15 | * @desc Takes three numbers, or single array, or an object with `.x`, `.y`, and `.z` properties. 16 | * - If no arguments passed, constructs `Vec3(0, 0, 0)`. 17 | * - If only one number is given, constructs `Vec3(x, x, x)`. 18 | * @arg {number|number[]|object} [x=0] 19 | * @arg {Number} [y=0] 20 | * @arg {Number} [z=0] 21 | */ 22 | constructor(x, y, z) { 23 | const args = arguments; 24 | 25 | super(x, y, z); 26 | 27 | this.z = 0; 28 | 29 | if (!args.length) { 30 | return; 31 | } 32 | 33 | if (typeof args[0] === 'object') { 34 | 35 | if (args[0] === null) { 36 | return; 37 | } 38 | 39 | // [] or {} or Vec2 40 | if (args[0].constructor === Array || args[0].constructor === Vec3) { 41 | this.z = args[0][2]; 42 | } else if (typeof args[0].z === 'number') { 43 | this.z = args[0].z; 44 | } else if (args[0].constructor === Vec2) { 45 | if (args[1].constructor === Array || args[1].constructor === Vec2) { 46 | this.z = args[1][0]; 47 | } else if (typeof args[1] === 'number') { 48 | this.z = args[1]; 49 | } 50 | } 51 | 52 | } else if (typeof args[0] === 'number') { 53 | 54 | if (isNaN(args[0])) { 55 | return; 56 | } 57 | 58 | this.z = (typeof args[2] === 'number') ? args[2] : args[0]; 59 | 60 | } 61 | } 62 | 63 | /** 64 | * The value of vector's z-component 65 | * @return {Number} z 66 | */ 67 | get z() { return this[2]; } 68 | set z(_z) { this[2] = _z; } 69 | 70 | /** 71 | * The **new** vector, constructed as `Vec3(this.x, this.y, this.z)` 72 | * @return {Vec} xyz 73 | */ 74 | get xyz() { return new Vec3(this); } 75 | set xyz(_xyz) { this[0] = _xyz[0]; this[1] = _xyz[1]; this[2] = _xyz[2]; } 76 | 77 | /** 78 | * The **new** vector, constructed as `Vec3(this.x, this.y, this.z)` 79 | * @return {Vec} yxz 80 | */ 81 | get yxz() { return new Vec3([this[1], this[0], this[2]]); } 82 | set yxz(_xyz) { this[1] = _xyz[0]; this[0] = _xyz[1]; this[2] = _xyz[2]; } 83 | 84 | /** 85 | * The **new** vector, constructed as `Vec3(this.z, this.y, this.x)` 86 | * @return {Vec} zyx 87 | */ 88 | get zyx() { return new Vec3([this[2], this[1], this[0]]); } 89 | set zyx(_xyz) { this[2] = _xyz[0]; this[1] = _xyz[1]; this[0] = _xyz[2]; } 90 | 91 | /** 92 | * The **new** vector, constructed as `Vec3(this.y, this.z, this.x)` 93 | * @return {Vec} yzx 94 | */ 95 | get yzx() { return new Vec3([this[1], this[2], this[0]]); } 96 | set yzx(_xyz) { this[0] = _xyz[0]; this[1] = _xyz[1]; this[2] = _xyz[2]; } 97 | 98 | /** 99 | * The **new** vector, constructed as `Vec3(this.x, this.z, this.y)` 100 | * @return {Vec} xzy 101 | */ 102 | get xzy() { return new Vec3([this[0], this[2], this[1]]); } 103 | set xzy(_xyz) { this[0] = _xyz[0]; this[2] = _xyz[1]; this[1] = _xyz[2]; } 104 | 105 | /** @override */ 106 | plused(other) { super.plused(other); this[2] += other[2]; return this; } 107 | /** @override */ 108 | minused(other) { super.minused(other); this[2] -= other[2]; return this; } 109 | /** @override */ 110 | muled(other) { super.muled(other); this[2] *= other[2]; return this; } 111 | /** @override */ 112 | dived(other) { super.dived(other); this[2] /= other[2]; return this; } 113 | /** @override */ 114 | maxed(other) { super.maxed(other); this[2] = Math.max(this[2], other[2]); return this; } 115 | /** @override */ 116 | mined(other) { super.mined(other); this[2] = Math.min(this[2], other[2]); return this; } 117 | 118 | /** @override */ 119 | get neged() { super.neged(); this[2] = -this[2]; return this; } 120 | 121 | /** @override */ 122 | scaled(scalar) { super.scaled(scalar); this[2] *= scalar; return this; } 123 | /** @override */ 124 | fracted(scalar) { super.fracted(scalar); this[2] /= scalar; return this; } 125 | 126 | /** @override */ 127 | get rounded() { this[2] = Math.round(this[2]); return super.rounded; } 128 | /** @override */ 129 | get floored() { this[2] = Math.floor(this[2]); return super.floored; } 130 | /** @override */ 131 | get ceiled() { this[2] = Math.ceil(this[2]); return super.ceiled; } 132 | 133 | 134 | /** @override */ 135 | get isZero() { return super.isZero() && this[2] === 0; } 136 | 137 | /** @override */ 138 | cmp(cb) { return super.cmp(cb) && cb(this[2], 2); } 139 | 140 | 141 | /** @override */ 142 | dot(other) { return super.dot(other) + this[2] * other[2]; } 143 | 144 | 145 | /** @override */ 146 | toString() { return 'Vec3(' + this[0] + ', ' + this[1] + ', ' + this[2] + ')'; } 147 | } 148 | 149 | module.exports = Vec3; 150 | -------------------------------------------------------------------------------- /js/math/vec4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Vec3 = require('./vec3'); 4 | const Vec2 = require('./vec2'); 5 | 6 | /** 7 | * Four-dimensional vector. 8 | * @note All 'ed() methods modify **INPLACE**, no-'ed methods - **MAKE A COPY** 9 | * @author Luis Blanco 10 | * @extends Vec3 11 | */ 12 | class Vec4 extends Vec3 { 13 | /** 14 | * @constructs Vec4 15 | * @desc Takes four numbers, or single array, or an object with `.x`, `.y`, `.z`, and `.w` properties. 16 | * - If no arguments passed, constructs `Vec4(0, 0, 0, 0)`. 17 | * - If only one number is given, constructs `Vec4(x, x, x, x)`. 18 | * - If only two numbers are given, constructs `Vec4(x, x, x, y)`. 19 | * @arg {number|number[]|object} [x=0] 20 | * @arg {Number} [y=0] 21 | * @arg {Number} [z=0] 22 | * @arg {Number} [w=0] 23 | */ 24 | constructor(x, y, z, w) { 25 | const args = arguments; 26 | 27 | super(x, y, z); 28 | 29 | this.w = 1; 30 | 31 | if (!args.length) { 32 | return; 33 | } 34 | 35 | if (typeof args[0] === 'object') { 36 | this.w = Vec4.wFromObject(args); 37 | } else if (typeof args[0] === 'number') { 38 | if (isNaN(args[0])) { 39 | return; 40 | } 41 | 42 | this.w = (typeof args[3] === 'number') ? w : args[0]; 43 | } 44 | } 45 | 46 | 47 | static wFromObject(args) { 48 | if (args[0] === null) { 49 | return 1; 50 | } 51 | 52 | // [] or {} or Vec2 53 | if (args[0].constructor === Array || args[0].constructor === Vec4) { 54 | return args[0][3]; 55 | } else if (typeof args[0].w === 'number') { 56 | return args[0].w; 57 | } else if (args[0].constructor === Vec3) { 58 | return args[1]; 59 | } else if (args[0].constructor === Vec2) { 60 | if ( 61 | args[1].constructor === Array || 62 | args[1].constructor === Vec2 || 63 | args[1].constructor === Vec3 64 | ) { 65 | return args[1][1]; 66 | } else if (typeof args[1] === 'number') { 67 | return args[1]; 68 | } 69 | } 70 | } 71 | 72 | 73 | /** 74 | * The value of vector's w-component 75 | * @return {Number} 76 | */ 77 | get w() { return this[3]; } 78 | set w(_w) { this[3] = _w; } 79 | 80 | /** 81 | * The **new** vector, constructed as `Vec4(this.x, this.y, this.z, this.w)` 82 | * @return {Vec} xyzw 83 | */ 84 | get xyzw() { return new Vec4(this); } 85 | set xyzw(_xyzw) { 86 | this[0] = _xyzw[0]; this[1] = _xyzw[1]; this[2] = _xyzw[2]; this[3] = _xyzw[3]; 87 | } 88 | 89 | /** 90 | * The **new** vector, constructed as `Vec4(this.y, this.x, this.z, this.w)` 91 | * @return {Vec} yxzw 92 | */ 93 | get yxzw() { return new Vec4([this[1], this[0], this[2], this[3]]); } 94 | set yxzw(_xyzw) { 95 | this[1] = _xyzw[0]; this[0] = _xyzw[1]; this[2] = _xyzw[2]; this[3] = _xyzw[3]; 96 | } 97 | 98 | /** 99 | * The **new** vector, constructed as `Vec4(this.z, this.y, this.x, this.w)` 100 | * @return {Vec} zyxw 101 | */ 102 | get zyxw() { return new Vec4([this[2], this[1], this[0], this[3]]); } 103 | set zyxw(_xyzw) { 104 | this[2] = _xyzw[0]; this[1] = _xyzw[1]; this[0] = _xyzw[2]; this[3] = _xyzw[3]; 105 | } 106 | 107 | /** 108 | * The **new** vector, constructed as `Vec4(this.y, this.z, this.x, this.w)` 109 | * @return {Vec} yzxw 110 | */ 111 | get yzxw() { return new Vec4([this[1], this[2], this[0], this[3]]); } 112 | set yzxw(_xyzw) { 113 | this[0] = _xyzw[0]; this[1] = _xyzw[1]; this[2] = _xyzw[2]; this[3] = _xyzw[3]; 114 | } 115 | 116 | /** 117 | * The **new** vector, constructed as `Vec4(this.x, this.z, this.y, this.w)` 118 | * @return {Vec} xzyw 119 | */ 120 | get xzyw() { return new Vec4([this[0], this[2], this[1], this[3]]); } 121 | set xzyw(_xyzw) { 122 | this[0] = _xyzw[0]; this[2] = _xyzw[1]; this[1] = _xyzw[2]; this[3] = _xyzw[3]; 123 | } 124 | 125 | 126 | /** @override */ 127 | plused(other) { super.plused(other); this[3] += other[3]; return this; } 128 | /** @override */ 129 | minused(other) { super.minused(other); this[3] -= other[3]; return this; } 130 | /** @override */ 131 | muled(other) { super.muled(other); this[3] *= other[3]; return this; } 132 | /** @override */ 133 | dived(other) { super.dived(other); this[3] /= other[3]; return this; } 134 | /** @override */ 135 | maxed(other) { 136 | super.maxed(other); this[3] = Math.max(this[3], other[3]); return this; 137 | } 138 | /** @override */ 139 | mined(other) { 140 | super.mined(other); this[3] = Math.min(this[3], other[3]); return this; 141 | } 142 | 143 | 144 | /** @override */ 145 | get neged() { super.neged(); this[3] = -this[3]; return this; } 146 | 147 | 148 | /** @override */ 149 | scaled(scalar) { super.scaled(scalar); this[3] *= scalar; return this; } 150 | /** @override */ 151 | fracted(scalar) { super.fracted(scalar); this[3] /= scalar; return this; } 152 | 153 | 154 | /** @override */ 155 | get rounded() { this[3] = Math.round(this[3]); return super.rounded; } 156 | /** @override */ 157 | get floored() { this[3] = Math.floor(this[3]); return super.floored; } 158 | /** @override */ 159 | get ceiled() { this[3] = Math.ceil(this[3]); return super.ceiled; } 160 | 161 | 162 | /** @override */ 163 | get isZero() { return super.isZero() && this[3] === 0; } 164 | 165 | /** @override */ 166 | cmp(cb) { return super.cmp(cb) && cb(this[3], 2); } 167 | 168 | 169 | /** @override */ 170 | dot(other) { return super.dot(other) + this[3] * other[3]; } 171 | 172 | 173 | /** @override */ 174 | toString() { 175 | return 'Vec4(' + this[0] + ', ' + this[1] + ', ' + this[2] + ', ' + this[3] + ')'; 176 | } 177 | } 178 | 179 | module.exports = Vec4; 180 | -------------------------------------------------------------------------------- /js/objects/brush.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Vec2 = require('../math/vec2'); 4 | const Drawable = require('./drawable'); 5 | 6 | 7 | class Brush extends Drawable { 8 | constructor(opts) { 9 | super({ screen: opts.screen, color: opts.color }); 10 | 11 | this._size = opts.size || 100; 12 | this._pos = opts.pos || new Vec2(); 13 | 14 | if (opts.visible !== undefined && !opts.visible) { 15 | this.visible = false; 16 | } 17 | 18 | this.screen.on('resize', () => { 19 | this._mesh.material.uniforms.aspect.value = this.screen.w / this.screen.h; 20 | this._mesh.material.uniforms.size.value = this._size / this.screen.h; 21 | }); 22 | } 23 | 24 | 25 | get size() { return this._size; } 26 | set size(v) { 27 | this._size = v; 28 | if (this.visible) { 29 | this._mesh.material.uniforms.size.value = this._size; 30 | } 31 | } 32 | 33 | 34 | get pos() { return this._pos; } 35 | set pos(v) { 36 | this._pos.copy(v); 37 | if (this.visible) { 38 | this._mesh.material.uniforms.pos.value = new this.screen.three.Vector2( 39 | (this._pos.x / this.screen.w - 0.5) * 2, 40 | (-this._pos.y / this.screen.h + 0.5) * 2 41 | ); 42 | } 43 | } 44 | 45 | 46 | get visible() { return super.visible; } 47 | set visible(v) { 48 | super.visible = v; 49 | 50 | if (this.visible) { 51 | this._mesh.material.uniforms.pos.value = new this.screen.three.Vector2( 52 | this._pos.x, 53 | this._pos.y 54 | ); 55 | 56 | this._mesh.material.uniforms.size.value = this._size / this.screen.h; 57 | 58 | this._mesh.material.uniforms.color.value = new this.screen.three.Vector3( 59 | this._color.r, 60 | this._color.g, 61 | this._color.b 62 | ); 63 | } 64 | } 65 | 66 | 67 | get color() { return this._color; } 68 | set color(v) { 69 | this._color = v; 70 | if (this.visible) { 71 | this.mat.uniforms.color.value = new this.screen.three.Vector3( 72 | this._color.r, 73 | this._color.g, 74 | this._color.b 75 | ); 76 | } 77 | } 78 | 79 | 80 | _geo() { 81 | const geo = new this.screen.three.PlaneGeometry(2, 2); 82 | geo.computeBoundingSphere = 83 | () => geo.boundingSphere = new this.screen.three.Sphere( 84 | undefined, Infinity 85 | ); 86 | geo.computeBoundingSphere(); 87 | geo.computeBoundingBox = () => geo.boundingBox = new this.screen.three.Box3(); 88 | geo.computeBoundingBox(); 89 | return geo; 90 | } 91 | 92 | 93 | _mat() { 94 | return new this.screen.three.ShaderMaterial({ 95 | side: this.screen.three.DoubleSide, 96 | uniforms: { 97 | aspect : { type: 'f', value: this.screen.w / this.screen.h }, 98 | size : { type: 'f', value: 100 / this.screen.h }, 99 | pos : { type: 'v2', value: new this.screen.three.Vector2(0, 0) }, 100 | color : { type: 'v3', value: new this.screen.three.Vector3(0, 1, 1) }, 101 | }, 102 | vertexShader: ` 103 | varying vec3 projPos; 104 | 105 | void main() { 106 | projPos = position.xyz; 107 | 108 | gl_Position = vec4(position.xyz, 1.0); 109 | } 110 | `, 111 | fragmentShader: ` 112 | varying vec3 projPos; 113 | 114 | uniform vec2 pos; 115 | uniform float size; 116 | uniform vec3 color; 117 | uniform float aspect; 118 | 119 | void main() { 120 | vec2 diff = projPos.xy - pos; 121 | diff.x *= aspect; 122 | float dist = length(diff); 123 | 124 | float opacity = pow(1.0 - min(1.0, abs(dist - size)), 100.0); 125 | gl_FragColor = vec4(color, opacity); 126 | } 127 | `, 128 | blending : this.screen.three.AdditiveBlending, 129 | depthTest : false, 130 | transparent: true, 131 | }); 132 | } 133 | 134 | 135 | _build(opts) { 136 | return new this.screen.three.Mesh(this._geo(opts), this._mat(opts)); 137 | } 138 | } 139 | 140 | module.exports = Brush; 141 | -------------------------------------------------------------------------------- /js/objects/cloud.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Drawable = require('./drawable'); 4 | 5 | 6 | class Cloud extends Drawable { 7 | /** 8 | * Create an instance of Cloud. 9 | * 10 | * @param opts.screen {Screen} the surface to draw to 11 | * @param opts.attrs {Object} input VBO buffers 12 | * @param opts.attrs.position {VBO} position buffer 13 | * @param opts.attrs.color {VBO} color buffer 14 | * @param opts.attrs.EXTRA {VBO} your extra buffers 15 | * @param opts.count {Number} number of points to draw 16 | * 17 | */ 18 | constructor(opts) { 19 | super(opts); 20 | } 21 | 22 | 23 | get color() { return null; } 24 | set color(v) { v = null; } 25 | 26 | 27 | buildAttr(source, count) { 28 | return new this.screen.three.GLBufferAttribute( 29 | source.vbo, 30 | this.screen.context.FLOAT, 31 | source.items, 32 | 4, 33 | count 34 | ); 35 | } 36 | 37 | 38 | _geo(opts) { 39 | const geo = new this.screen.three.BufferGeometry(); 40 | 41 | Object.keys(opts.attrs).forEach((key) => { 42 | geo.setAttribute(key, this.buildAttr(opts.attrs[key], opts.count)); 43 | }); 44 | geo.boundingSphere = new this.screen.three.Sphere(new this.screen.three.Vector3(), Infinity); 45 | 46 | return geo; 47 | } 48 | 49 | 50 | _mat(opts) { 51 | const uniforms = { 52 | ...(opts.uniforms || null), 53 | winh : { type: 'f' , value: this.screen.height }, 54 | }; 55 | 56 | this.screen.on('resize', ({ height }) => uniforms.winh.value = height); 57 | 58 | return new this.screen.three.ShaderMaterial({ 59 | 60 | blending : this.screen.three.NormalBlending, 61 | depthTest : opts.depthTest === true, 62 | transparent : true, 63 | uniforms, 64 | 65 | vertexShader : this.buildVert(opts), 66 | fragmentShader : this.buildFrag(opts), 67 | 68 | }); 69 | } 70 | 71 | 72 | buildVert(opts) { 73 | return opts.vert || ` 74 | attribute vec3 color; 75 | varying vec3 varColor; 76 | 77 | ${ 78 | opts.inject && opts.inject.vert && opts.inject.vert.vars 79 | ? opts.inject.vert.vars 80 | : '' 81 | } 82 | 83 | void main() { 84 | 85 | ${ 86 | opts.inject && opts.inject.vert && opts.inject.vert.before 87 | ? opts.inject.vert.before 88 | : '' 89 | } 90 | 91 | varColor = color; 92 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); 93 | gl_Position = projectionMatrix * mvPosition; 94 | 95 | ${ 96 | opts.inject && opts.inject.vert && opts.inject.vert.after 97 | ? opts.inject.vert.after 98 | : '' 99 | } 100 | 101 | } 102 | `; 103 | } 104 | 105 | 106 | buildFrag(opts) { 107 | return opts.frag || ` 108 | varying vec3 varColor; 109 | 110 | ${ 111 | opts.inject && opts.inject.frag && opts.inject.frag.vars 112 | ? opts.inject.frag.vars 113 | : '' 114 | } 115 | 116 | void main() { 117 | 118 | ${ 119 | opts.inject && opts.inject.frag && opts.inject.frag.before 120 | ? opts.inject.frag.before 121 | : '' 122 | } 123 | 124 | // gl_FragColor = vec4(varColor, 1.0); 125 | gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 126 | 127 | ${ 128 | opts.inject && opts.inject.frag && opts.inject.frag.after 129 | ? opts.inject.frag.after 130 | : '' 131 | } 132 | 133 | } 134 | `; 135 | } 136 | 137 | 138 | _build(opts) { 139 | const points = new this.screen.three.Points(this._geo(opts), this._mat(opts)); 140 | points.frustumCulled = false; 141 | points.boundingSphere = new this.screen.three.Sphere(new this.screen.three.Vector3(), Infinity); 142 | return points; 143 | } 144 | } 145 | 146 | 147 | module.exports = Cloud; 148 | -------------------------------------------------------------------------------- /js/objects/drawable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Vec2 = require('../math/vec2'); 4 | const Color = require('../math/color'); 5 | 6 | 7 | class Drawable { 8 | 9 | constructor(opts) { 10 | 11 | this._screen = opts.screen; 12 | this._three = this._screen.three; 13 | 14 | this._pos = new Vec2(opts.pos || [0, 0]); 15 | this._z = 0; 16 | 17 | this._visible = true; 18 | 19 | this._mesh = this._build(opts); 20 | 21 | this.screen.scene.add(this._mesh); 22 | 23 | if (opts.color) { 24 | if (opts.color instanceof Color) { 25 | this.color = opts.color; 26 | } else { 27 | this.color = new Color(opts.color); 28 | } 29 | } else { 30 | this.color = new Color(0xFFFFFF); 31 | } 32 | 33 | this.pos = this._pos; 34 | this.z = opts.z || 0; 35 | 36 | } 37 | 38 | 39 | get three() { return this._three; } 40 | 41 | 42 | get screen() { return this._screen; } 43 | set screen(v) { v = null; } // dummy setter, for convinience of passing Drawable as opts 44 | 45 | 46 | get mat() { return this._mesh.material; } 47 | get geo() { return this._mesh.geometry; } 48 | get mesh() { return this._mesh; } 49 | 50 | 51 | get z() { return this._z; } 52 | set z(v) { 53 | this._z = v; 54 | this._mesh.position.z = this._z; 55 | } 56 | 57 | 58 | get visible() { return this._visible; } 59 | set visible(v) { 60 | this._visible = v; 61 | this._mesh.visible = this._visible; 62 | } 63 | 64 | 65 | get pos() { return this._pos.xy; } 66 | set pos(p) { 67 | 68 | this._pos.copy(p); 69 | 70 | this._mesh.position.x = this._pos.x; 71 | this._mesh.position.y = this._pos.y; 72 | } 73 | 74 | 75 | get color() { return this._color; } 76 | set color(v) { 77 | this._color = v; 78 | 79 | if (this.mat) { 80 | if (this.mat.color) { 81 | this.mat.color.setHex( this._color.toHex() ); 82 | } 83 | if (this.mat.opacity) { 84 | this.mat.opacity = this._color.a; 85 | } 86 | } 87 | } 88 | 89 | 90 | _build(opts) { 91 | return new this.screen.three.Mesh(this._geo(opts), this._mat(opts)); 92 | } 93 | 94 | 95 | _geo() { 96 | return new this.screen.three.PlaneGeometry(2,2); 97 | } 98 | 99 | 100 | updateGeo() { 101 | this._mesh.geometry = this._geo(this); 102 | this._mesh.geometry.needsUpdate = true; 103 | } 104 | 105 | 106 | _mat() { 107 | return new this.screen.three.MeshBasicMaterial({ 108 | transparent: true, 109 | side : this.screen.three.DoubleSide, 110 | depthWrite : true, 111 | depthTest : true, 112 | }); 113 | } 114 | 115 | 116 | remove() { 117 | this.screen.scene.remove(this._mesh); 118 | } 119 | 120 | } 121 | 122 | module.exports = Drawable; 123 | -------------------------------------------------------------------------------- /js/objects/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = { 5 | 6 | Screen : require('./screen'), 7 | Drawable : require('./drawable'), 8 | 9 | Cloud : require('./cloud'), 10 | Points : require('./points'), 11 | Lines : require('./lines'), 12 | Tris : require('./tris'), 13 | 14 | Rect : require('./rect'), 15 | Brush : require('./brush'), 16 | Surface : require('./surface'), 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /js/objects/lines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cloud = require('./cloud'); 4 | 5 | 6 | class Lines extends Cloud { 7 | 8 | 9 | constructor(opts) { 10 | 11 | super(opts); 12 | 13 | } 14 | 15 | 16 | buildFrag(opts) { 17 | return opts.frag || ` 18 | varying vec3 varColor; 19 | varying vec2 varTcoord; 20 | varying float varSize; 21 | 22 | ${ 23 | opts.inject && opts.inject.frag && opts.inject.frag.vars 24 | ? opts.inject.frag.vars 25 | : '' 26 | } 27 | 28 | void main() { 29 | 30 | ${ 31 | opts.inject && opts.inject.frag && opts.inject.frag.before 32 | ? opts.inject.frag.before 33 | : '' 34 | } 35 | 36 | gl_FragColor = vec4(varColor, 1.0); 37 | 38 | ${ 39 | opts.inject && opts.inject.frag && opts.inject.frag.after 40 | ? opts.inject.frag.after 41 | : '' 42 | } 43 | 44 | } 45 | `; 46 | } 47 | 48 | 49 | _build(opts) { 50 | const Ctor = (() => { 51 | switch (opts.mode) { 52 | case 'segments' : return this.screen.three.LineSegments; 53 | case 'loop' : return this.screen.three.LineLoop; 54 | default : return this.screen.three.Line; 55 | } 56 | })(); 57 | const lines = new Ctor(this._geo(opts), this._mat(opts)); 58 | lines.frustumCulled = false; 59 | lines.boundingSphere = new this.screen.three.Sphere(new this.screen.three.Vector3(), Infinity); 60 | return lines; 61 | } 62 | 63 | } 64 | 65 | 66 | module.exports = Lines; 67 | -------------------------------------------------------------------------------- /js/objects/points.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cloud = require('./cloud'); 4 | 5 | 6 | class Points extends Cloud { 7 | 8 | 9 | constructor(opts) { 10 | 11 | super(opts); 12 | 13 | } 14 | 15 | 16 | buildVert(opts) { 17 | return opts.vert || ` 18 | ${ 19 | opts.attrs.size 20 | ? 'attribute float size' 21 | : `float size = ${opts.size ? opts.size : '10.0'}` 22 | }; 23 | attribute vec3 color; 24 | varying vec3 varColor; 25 | varying vec2 varTcoord; 26 | varying float varSize; 27 | 28 | uniform float winh; 29 | 30 | ${ 31 | opts.inject && opts.inject.vert && opts.inject.vert.vars 32 | ? opts.inject.vert.vars 33 | : '' 34 | } 35 | 36 | void main() { 37 | 38 | ${ 39 | opts.inject && opts.inject.vert && opts.inject.vert.before 40 | ? opts.inject.vert.before 41 | : '' 42 | } 43 | 44 | varColor = color; 45 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); 46 | gl_Position = projectionMatrix * mvPosition; 47 | varSize = size; 48 | gl_PointSize = max(2.0, 2.0 * winh * varSize / length( mvPosition.xyz )); 49 | varTcoord = position.xy; 50 | 51 | ${ 52 | opts.inject && opts.inject.vert && opts.inject.vert.after 53 | ? opts.inject.vert.after 54 | : '' 55 | } 56 | 57 | } 58 | `; 59 | } 60 | 61 | 62 | buildFrag(opts) { 63 | return opts.frag || ` 64 | varying vec3 varColor; 65 | varying vec2 varTcoord; 66 | varying float varSize; 67 | 68 | ${ 69 | opts.inject && opts.inject.frag && opts.inject.frag.vars 70 | ? opts.inject.frag.vars 71 | : '' 72 | } 73 | 74 | void main() { 75 | 76 | ${ 77 | opts.inject && opts.inject.frag && opts.inject.frag.before 78 | ? opts.inject.frag.before 79 | : '' 80 | } 81 | 82 | float offs = length(gl_PointCoord.xy - vec2(0.5, 0.5)); 83 | float dist = clamp(1.0 - 2.0 * offs, 0.0, 1.0) * 0.2 * varSize; 84 | dist = pow(dist, 5.0); 85 | gl_FragColor = vec4(varColor, dist); 86 | 87 | ${ 88 | opts.inject && opts.inject.frag && opts.inject.frag.after 89 | ? opts.inject.frag.after 90 | : '' 91 | } 92 | 93 | } 94 | `; 95 | } 96 | 97 | 98 | } 99 | 100 | 101 | module.exports = Points; 102 | -------------------------------------------------------------------------------- /js/objects/rect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Drawable = require('./drawable'); 4 | const Vec2 = require('../math/vec2'); 5 | 6 | 7 | class Rect extends Drawable { 8 | 9 | 10 | constructor(opts = {}) { 11 | 12 | opts.pos = opts.pos || new Vec2(-300, -300); 13 | opts.size = (opts.size && new Vec2(opts.size)) || new Vec2(600, 600); 14 | opts.radius = opts.radius || 0; 15 | 16 | super(opts); 17 | 18 | this._size = opts.size; 19 | this._radius = opts.radius; 20 | 21 | } 22 | 23 | 24 | _build(opts) { 25 | return new this.screen.three[opts.wire ? 'Line' : 'Mesh']( 26 | this._geo(opts), 27 | this._mat(opts) 28 | ); 29 | } 30 | 31 | 32 | _mat(opts) { 33 | 34 | const matName = opts.wire ? 'LineBasicMaterial' : 'MeshBasicMaterial'; 35 | const matOpts = { 36 | transparent : true, 37 | side : this.screen.three.DoubleSide, 38 | depthWrite : false, 39 | depthTest : false, 40 | }; 41 | 42 | if (opts.wire) { 43 | matOpts.linewidth = 1; 44 | } 45 | 46 | return new this.screen.three[matName](matOpts); 47 | 48 | } 49 | 50 | 51 | get size() { return this._size.xy; } 52 | set size(p) { 53 | this._size.xy = p; 54 | this.updateGeo(); 55 | } 56 | 57 | 58 | get width() { return this._size.x; } 59 | get height() { return this._size.y; } 60 | 61 | 62 | get w() { return this._size.x; } 63 | get h() { return this._size.y; } 64 | 65 | 66 | get radius() { return this._radius; } 67 | set radius(v) { 68 | this._radius = v; 69 | this.updateGeo(); 70 | } 71 | 72 | 73 | get texture() { 74 | return this._mesh.material.map; 75 | } 76 | set texture(tex) { 77 | this._mesh.material.map = tex; 78 | this._mesh.material.needsUpdate = true; 79 | } 80 | 81 | 82 | _geo(opts) { 83 | let geometry = null; 84 | 85 | const size = opts.size || new Vec2(100, 100); 86 | 87 | const 88 | r = opts.radius || 0, 89 | w = size.x, 90 | h = size.y; 91 | 92 | if (r) { 93 | 94 | // Rounded rectangle 95 | const shape = new this.screen.three.Shape(); 96 | 97 | shape.moveTo( 0, r ); 98 | shape.lineTo( 0, h - r ); 99 | shape.quadraticCurveTo( 0, h, r, h ); 100 | 101 | shape.lineTo( w - r, h) ; 102 | shape.quadraticCurveTo( w, h, w, h - r ); 103 | 104 | shape.lineTo( w, r ); 105 | shape.quadraticCurveTo( w, 0, w - r, 0 ); 106 | 107 | shape.lineTo( r, 0 ); 108 | shape.quadraticCurveTo( 0, 0, 0, r ); 109 | 110 | geometry = new this.screen.three.ShapeGeometry(shape); 111 | 112 | geometry.translate(-w * 0.5, -h * 0.5, 0); 113 | geometry.rotateX(Math.PI); 114 | geometry.translate(w * 0.5, h * 0.5, 0); 115 | 116 | } else { 117 | geometry = new this.screen.three.PlaneGeometry(w, h); 118 | geometry.rotateX(Math.PI); 119 | geometry.translate(w * 0.5, h * 0.5, 0); 120 | } 121 | geometry.computeBoundingBox(); 122 | 123 | return geometry; 124 | } 125 | 126 | 127 | } 128 | 129 | module.exports = Rect; 130 | -------------------------------------------------------------------------------- /js/objects/screen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | 5 | 6 | class Screen extends EventEmitter { 7 | constructor(opts = {}) { 8 | super(); 9 | 10 | this._three = opts.three || opts.THREE || global.THREE; 11 | this._gl = opts.gl || global._gl; 12 | this._doc = opts.doc || opts.document || global.document; 13 | this._Image = opts.Image || global.Image; 14 | 15 | if (opts.title) { 16 | this.title = opts.title; 17 | } 18 | 19 | if (!opts.camera) { 20 | const { fov, near, far, z } = opts; 21 | if (fov === 0) { 22 | this._camera = new this._three.OrthographicCamera( 23 | -this.w * 0.5, this.w * 0.5, 24 | this.h * 0.5, -this.h * 0.5, 25 | near ?? -10, far ?? 10, 26 | ); 27 | this._camera.position.z = z ?? 5; 28 | } else { 29 | this._camera = new this._three.PerspectiveCamera( 30 | fov ?? 90, this.w / this.h, near ?? 0.1, far ?? 200, 31 | ); 32 | this._camera.position.z = z ?? 10; 33 | } 34 | } else { 35 | this._camera = opts.camera; 36 | } 37 | 38 | if (!opts.scene) { 39 | this._scene = new this._three.Scene(); 40 | } else { 41 | this._scene = opts.scene; 42 | } 43 | 44 | if (opts.renderer) { 45 | this._autoRenderer = false; 46 | this._renderer = opts.renderer; 47 | } 48 | this._reinitRenderer(); 49 | 50 | this._renderer.setSize(this._doc.width, this._doc.height, false); 51 | 52 | this._doc.on('mode', (e) => { 53 | this._reinitRenderer(); 54 | this.emit('mode', e); 55 | }); 56 | 57 | this._doc.on('resize', ({ width, height }) => { 58 | width = width || 16; 59 | height = height || 16; 60 | 61 | this._camera.aspect = width / height; 62 | this._camera.updateProjectionMatrix(); 63 | this._renderer.setSize(width, height, false); 64 | 65 | this.emit('resize', { width, height }); 66 | }); 67 | 68 | ['keydown', 'keyup', 'mousedown', 'mouseup', 'mousemove','mousewheel'].forEach( 69 | (type) => this._doc.on(type, (e) => this.emit(type, e)) 70 | ); 71 | 72 | this.draw(); 73 | } 74 | 75 | get context() { return this._gl; } 76 | get three() { return this._three; } 77 | 78 | get renderer() { return this._renderer; } 79 | get scene() { return this._scene; } 80 | get camera() { return this._camera; } 81 | 82 | get document() { return this._doc; } 83 | get canvas() { return this._doc; } 84 | 85 | get width() { return this._doc.width; } 86 | get height() { return this._doc.height; } 87 | get w() { return this._doc.width; } 88 | get h() { return this._doc.height; } 89 | get size() { return new this._three.Vector2(this.w, this.h); } 90 | 91 | get title() { return this._doc.title; } 92 | set title(v) { this._doc.title = v || 'Untitled'; } 93 | 94 | get icon() { return this._doc.icon; } 95 | set icon(v) { this._doc.icon = v || null; } 96 | 97 | get fov() { return this._camera.fov; } 98 | set fov(v) { 99 | this._camera.fov = v; 100 | this._camera.updateProjectionMatrix(); 101 | } 102 | 103 | get mode() { return this._doc.mode; } 104 | set mode(v) { this._doc.mode = v; } 105 | 106 | draw() { 107 | this._renderer.render(this._scene, this._camera); 108 | } 109 | 110 | 111 | snapshot(name = `${Date.now()}.jpg`) { 112 | const memSize = this.w * this.h * 4; // estimated number of bytes 113 | const storage = { data: Buffer.allocUnsafeSlow(memSize) }; 114 | 115 | this._gl.readPixels( 116 | 0, 0, this.w, this.h, this._gl.RGBA, this._gl.UNSIGNED_BYTE, storage, 117 | ); 118 | 119 | const img = this._Image.fromPixels(this.w, this.h, 32, storage.data); 120 | img.save(name); 121 | } 122 | 123 | static _deepAssign(src, dest) { 124 | Object.entries(src).forEach(([k, v]) => { 125 | if (v && typeof v === 'object') { 126 | Screen._deepAssign(v, dest[k]); 127 | return; 128 | } 129 | dest[k] = v; 130 | }); 131 | } 132 | 133 | // When switching from fullscreen and back, reset renderer to update VAO/FBO objects 134 | _reinitRenderer() { 135 | const old = this._renderer; 136 | 137 | // Migrate renderer props 138 | const renderProps = !old ? null : { 139 | shadowMap: { 140 | enabled: old.shadowMap.enabled, 141 | type: old.shadowMap.type, 142 | }, 143 | debug: { 144 | checkShaderErrors: old.debug_checkShaderErrors, 145 | onShaderError: old.debug_onShaderError, 146 | }, 147 | autoClear: old.autoClear, 148 | autoClearColor: old.autoClearColor, 149 | autoClearDepth: old.autoClearDepth, 150 | autoClearStencil: old.autoClearStencil, 151 | clippingPlanes: old.clippingPlanes, 152 | outputColorSpace: old.outputColorSpace, 153 | sortObjects: old.sortObjects, 154 | toneMapping: old.toneMapping, 155 | toneMappingExposure: old.toneMappingExposure, 156 | transmissionResolutionScale: old.transmissionResolutionScale, 157 | }; 158 | if (this._autoRenderer) { 159 | old.dispose(); 160 | } 161 | 162 | this._autoRenderer = true; 163 | this._renderer = new this._three.WebGLRenderer({ 164 | context: this._gl, 165 | canvas: this.canvas, 166 | }); 167 | 168 | this._camera.aspect = this.w / this.h; 169 | this._camera.updateProjectionMatrix(); 170 | this._renderer.setSize(this.w, this.h, false); 171 | 172 | if (renderProps) { 173 | Screen._deepAssign(renderProps, this._renderer); 174 | } 175 | 176 | this._gl.enable(0x8861); // GL_POINT_SPRITE 0x8861 177 | this._gl.enable(0x8642); // GL_VERTEX_PROGRAM_POINT_SIZE 178 | this._gl.enable(0x8862); // GL_COORD_REPLACE 179 | } 180 | } 181 | 182 | module.exports = Screen; 183 | -------------------------------------------------------------------------------- /js/objects/surface.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | 5 | const Rect = require('./rect'); 6 | const Vec2 = require('../math/vec2'); 7 | 8 | 9 | class Surface extends Rect { 10 | 11 | constructor(opts) { 12 | 13 | opts.pos = opts.pos || new Vec2(-300, -300); 14 | opts.size = opts.size || new Vec2(600, 600); 15 | 16 | super(opts); 17 | 18 | this._events = new EventEmitter(); 19 | 20 | 21 | // Create a different scene to hold our buffer objects 22 | if (!opts.camera ) { 23 | this._camera = new this.screen.three.PerspectiveCamera( 24 | 70, 25 | this.width / this.height, 26 | 0.1, 27 | 1000 28 | ); 29 | this._camera.position.z = 10; 30 | } else { 31 | this._camera = opts.camera; 32 | } 33 | 34 | if (!opts.scene ) { 35 | this._scene = new this.screen.three.Scene(); 36 | } else { 37 | this._scene = opts.scene; 38 | } 39 | 40 | // Init RTT 41 | this._target = this._newTarget(); 42 | this.draw(); 43 | 44 | 45 | this.mesh.material = new this.screen.three.ShaderMaterial({ 46 | 47 | side: this.screen.three.DoubleSide, 48 | 49 | uniforms : { type: 't', t: { value: this._target.texture } }, 50 | 51 | vertexShader : ` 52 | varying vec2 tc; 53 | void main() { 54 | tc = uv; 55 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); 56 | } 57 | `, 58 | 59 | fragmentShader: ` 60 | varying vec2 tc; 61 | uniform sampler2D t; 62 | void main() { 63 | // gl_FragColor = (vec4(1.0, 0.0, 1.0, 1.0) + texture2D(t, tc)) * 0.5; 64 | gl_FragColor = texture2D(t, tc); 65 | } 66 | `, 67 | 68 | depthWrite : true, 69 | depthTest : true, 70 | transparent : true, 71 | 72 | }); 73 | 74 | 75 | this.mesh.onBeforeRender = () => setTimeout(() => this.draw(), 0); 76 | 77 | 78 | this.mesh.geometry.computeBoundingSphere = () => { 79 | this.mesh.geometry.boundingSphere = new this.screen.three.Sphere(undefined, Infinity); 80 | }; 81 | this.mesh.geometry.computeBoundingSphere(); 82 | 83 | this.mesh.geometry.computeBoundingBox = () => { 84 | this.mesh.geometry.boundingBox = new this.screen.three.Box3(); 85 | }; 86 | this.mesh.geometry.computeBoundingBox(); 87 | 88 | this.mesh.material.needsUpdate = true; 89 | 90 | } 91 | 92 | on(event, cb) { 93 | this[event === 'resize' ? '_events' : 'screen'].on(event, cb); 94 | } 95 | 96 | 97 | get canvas() { return this.screen.canvas; } 98 | get camera() { return this._camera; } 99 | get scene() { return this._scene; } 100 | get renderer() { return this.screen.renderer; } 101 | get context() { return this.screen.context; } 102 | get document() { return this.screen.document; } 103 | 104 | 105 | get title() { return this.screen.title; } 106 | set title(v) { this.screen.title = v; } 107 | 108 | 109 | get fov() { return this.screen.fov; } 110 | set fov(v) { this.screen.fov = v; } 111 | 112 | 113 | get size() { return super.size; } 114 | set size(v) { 115 | super.size = v; 116 | this.reset(); 117 | this._events.emit('resize', { w: this.width, h: this.height }); 118 | } 119 | 120 | 121 | get texture() { 122 | return this._target.texture; 123 | } 124 | 125 | 126 | reset() { 127 | this._target = this._newTarget(); 128 | this.draw(); 129 | 130 | this.mesh.material.uniforms.t.value = this._target.texture; 131 | 132 | this._events.emit('reset', this._target.texture); 133 | } 134 | 135 | 136 | draw() { 137 | const rt = this.renderer.getRenderTarget(); 138 | this.renderer.setRenderTarget(this._target); 139 | this.screen.renderer.render(this._scene, this._camera); 140 | this.renderer.setRenderTarget(rt); 141 | } 142 | 143 | 144 | _newTarget() { 145 | return new this.screen.three.WebGLRenderTarget( 146 | this.w * 2, 147 | this.h * 2, 148 | { 149 | minFilter : this.screen.three.LinearFilter, 150 | magFilter : this.screen.three.NearestFilter, 151 | format : this.screen.three.RGBAFormat, 152 | } 153 | ); 154 | } 155 | 156 | } 157 | 158 | module.exports = Surface; 159 | -------------------------------------------------------------------------------- /js/objects/tris.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cloud = require('./cloud'); 4 | 5 | 6 | class Tris extends Cloud { 7 | 8 | 9 | constructor(opts) { 10 | 11 | super(opts); 12 | 13 | } 14 | 15 | 16 | buildFrag(opts) { 17 | return opts.frag || ` 18 | varying vec3 varColor; 19 | varying vec2 varTcoord; 20 | varying float varSize; 21 | 22 | ${ 23 | opts.inject && opts.inject.frag && opts.inject.frag.vars 24 | ? opts.inject.frag.vars 25 | : '' 26 | } 27 | 28 | void main() { 29 | 30 | ${ 31 | opts.inject && opts.inject.frag && opts.inject.frag.before 32 | ? opts.inject.frag.before 33 | : '' 34 | } 35 | 36 | gl_FragColor = vec4(varColor, 1.0); 37 | 38 | ${ 39 | opts.inject && opts.inject.frag && opts.inject.frag.after 40 | ? opts.inject.frag.after 41 | : '' 42 | } 43 | 44 | } 45 | `; 46 | } 47 | 48 | 49 | _build(opts) { 50 | const tris = new this.screen.three.Mesh(this._geo(opts), this._mat(opts)); 51 | tris.frustumCulled = false; 52 | tris.boundingSphere = new this.screen.three.Sphere(new this.screen.three.Vector3(), Infinity); 53 | return tris; 54 | } 55 | 56 | } 57 | 58 | 59 | module.exports = Tris; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Luis Blanco ", 3 | "name": "3d-core-raub", 4 | "version": "5.3.0", 5 | "description": "An extensible Node3D core for desktop applications", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "keywords": [ 9 | "core", 10 | "3d", 11 | "node3d", 12 | "opengl", 13 | "webgl", 14 | "glfw", 15 | "graphics", 16 | "gl", 17 | "image" 18 | ], 19 | "files": [ 20 | "js", 21 | "index.js", 22 | "index.d.ts", 23 | "LICENSE", 24 | "package.json" 25 | ], 26 | "engines": { 27 | "node": ">=22.14.0", 28 | "npm": ">=10.9.2" 29 | }, 30 | "scripts": { 31 | "eslint": "eslint .", 32 | "test": "node --test --watch", 33 | "test-ci": "node --test" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/node-3d/3d-core-raub.git" 38 | }, 39 | "dependencies": { 40 | "addon-tools-raub": "^9.3.0", 41 | "image-raub": "^5.2.0", 42 | "glfw-raub": "^6.4.0", 43 | "webgl-raub": "^5.3.0" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^22.13.4", 47 | "@eslint/js": "^9.20.0", 48 | "@types/three": "0.174.0", 49 | "eslint": "^9.20.1", 50 | "pixelmatch": "^6.0.0", 51 | "three": "0.174.0", 52 | "typescript": "^5.7.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/compare.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('node:assert').strict; 4 | const { describe, it } = require('node:test'); 5 | const three = require('three'); 6 | 7 | const { screenshot } = require('./screenshot'); 8 | const { window, document } = require('./init'); 9 | 10 | 11 | const loadBox = () => { 12 | return new Promise((res) => { 13 | let mesh = null; 14 | const texture = new three.TextureLoader().load( 15 | __dirname + '/../examples/three/textures/crate.gif', 16 | () => res(mesh), 17 | ); 18 | texture.colorSpace = three.SRGBColorSpace; 19 | const geometry = new three.BoxGeometry(); 20 | const material = new three.MeshBasicMaterial({ map: texture }); 21 | mesh = new three.Mesh(geometry, material); 22 | mesh.rotation.x = Math.PI / 7; 23 | mesh.rotation.y = Math.PI / 5; 24 | }); 25 | }; 26 | 27 | describe('Screenshots', () => { 28 | it('matches box screenshot', async () => { 29 | const camera = new three.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); 30 | camera.position.z = 2; 31 | const scene = new three.Scene(); 32 | 33 | const mesh = await loadBox(); 34 | scene.add(mesh); 35 | 36 | const renderer = new three.WebGLRenderer(); 37 | renderer.setPixelRatio(window.devicePixelRatio); 38 | renderer.setSize(window.innerWidth, window.innerHeight); 39 | document.body.appendChild(renderer.domElement); 40 | 41 | renderer.render(scene, camera); 42 | 43 | assert.ok(await screenshot('box')); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/freeimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-3d/3d-core-raub/bd35a5e6e36df2ec450d017c52dbded681d4b31c/test/freeimage.jpg -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('node:assert').strict; 4 | const { describe, it } = require('node:test'); 5 | const three = require('three'); 6 | 7 | const inited = require('./init'); 8 | const { 9 | gl, Brush, Cloud, Drawable, Lines, Points, Rect, Screen, Surface, Tris, 10 | } = inited; 11 | 12 | 13 | const staticClasses = { 14 | Brush: { 15 | create({ screen }) { 16 | return new Brush({ screen }); 17 | }, 18 | props: ['size', 'pos', 'visible', 'color'], 19 | methods: [], 20 | }, 21 | Cloud: { 22 | create({ screen }) { 23 | const vertices = []; 24 | for (let i = 30; i > 0; i--) { 25 | vertices.push( Math.random() * 2000 - 1000 ); 26 | } 27 | const pos = gl.createBuffer(); 28 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 29 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 30 | 31 | return new Cloud({ screen, count: 10, attrs: { position: { vbo: pos, items: 3 } } }); 32 | }, 33 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'visible'], 34 | methods: [], 35 | }, 36 | Drawable: { 37 | create({ screen }) { 38 | return new Drawable({ screen }); 39 | }, 40 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'z', 'visible', 'pos', 'color'], 41 | methods: [], 42 | }, 43 | Points: { 44 | create({ screen }) { 45 | const vertices = []; 46 | for (let i = 30; i > 0; i--) { 47 | vertices.push( Math.random() * 2000 - 1000 ); 48 | } 49 | const pos = gl.createBuffer(); 50 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 51 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 52 | 53 | return new Points({ screen, count: 10, attrs: { position: { vbo: pos, items: 3 } } }); 54 | }, 55 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'visible'], 56 | methods: [], 57 | }, 58 | Lines: { 59 | create({ screen }) { 60 | const vertices = []; 61 | for (let i = 30; i > 0; i--) { 62 | vertices.push( Math.random() * 2000 - 1000 ); 63 | } 64 | const pos = gl.createBuffer(); 65 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 66 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 67 | 68 | return new Lines({ screen, count: 10, attrs: { position: { vbo: pos, items: 3 } } }); 69 | }, 70 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'visible'], 71 | methods: [], 72 | }, 73 | Tris: { 74 | create({ screen }) { 75 | const vertices = []; 76 | for (let i = 30; i > 0; i--) { 77 | vertices.push( Math.random() * 2000 - 1000 ); 78 | } 79 | const pos = gl.createBuffer(); 80 | gl.bindBuffer(gl.ARRAY_BUFFER, pos); 81 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 82 | 83 | return new Tris({ screen, count: 10, attrs: { position: { vbo: pos, items: 3 } } }); 84 | }, 85 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'visible'], 86 | methods: [], 87 | }, 88 | Rect: { 89 | create({ screen }) { 90 | return new Rect({ screen }); 91 | }, 92 | props: ['three', 'screen', 'mat', 'geo', 'mesh', 'visible'], 93 | methods: [], 94 | }, 95 | Screen: { 96 | create() { 97 | return new Screen({ three }); 98 | }, 99 | props: [ 100 | 'three', 'canvas', 'camera', 'scene', 'renderer', 'context', 101 | 'document', 'width', 'height', 'w', 'h', 'size', 'title', 'fov', 102 | ], 103 | methods: [], 104 | }, 105 | Surface: { 106 | create({ screen }) { 107 | return new Surface({ screen }); 108 | }, 109 | props: [ 110 | 'canvas', 'camera', 'scene', 'renderer', 'context', 111 | 'document', 'title', 'fov', 'size', 'texture' 112 | ], 113 | methods: [], 114 | }, 115 | }; 116 | 117 | describe('Node.js 3D Core', () => { 118 | it('exports an object', () => { 119 | assert.strictEqual(typeof inited, 'object'); 120 | }); 121 | 122 | describe('Static classes', () => { 123 | Object.keys(staticClasses).forEach( 124 | (c) => { 125 | it(`${c} is exported`, () => { 126 | assert.strictEqual(typeof inited[c], 'function'); 127 | }); 128 | } 129 | ); 130 | 131 | const screen = new Screen({ three }); 132 | 133 | Object.keys(staticClasses).forEach((c) => describe(c, () => { 134 | const current = staticClasses[c]; 135 | const instance = current.create({ screen }); 136 | 137 | it('can be created', () => { 138 | assert.ok(instance instanceof inited[c]); 139 | }); 140 | 141 | current.props.forEach((prop) => { 142 | it(`#${prop} property exposed`, () => { 143 | assert.ok(instance[prop] !== undefined); 144 | }); 145 | }); 146 | 147 | current.methods.forEach((method) => { 148 | it(`#${method}() method exposed`, () => { 149 | assert.strictEqual(typeof instance[method], 'function'); 150 | }); 151 | }); 152 | })); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | const { platform } = require('node:process'); 2 | const three = require('three'); 3 | const glfw = require('glfw-raub'); 4 | 5 | const { init, addThreeHelpers } = require('..'); 6 | 7 | const initOptsLinux = { 8 | width: 400, height: 400, 9 | isGles3: true, 10 | isWebGL2: true, 11 | }; 12 | const initOpts = { 13 | width: 400, height: 400, 14 | isGles3: false, 15 | major: 2, 16 | minor: 1, 17 | }; 18 | 19 | if (platform === 'darwin') { 20 | glfw.windowHint(glfw.STENCIL_BITS, 8); 21 | // this would be nice... - https://github.com/glfw/glfw/pull/2571 22 | // glfw.windowHint(glfw.CONTEXT_RENDERER, glfw.SOFTWARE_RENDERER); 23 | } 24 | 25 | const inited = init(platform === 'linux' ? initOptsLinux : initOpts); 26 | addThreeHelpers(three, inited.gl); 27 | 28 | module.exports = inited; 29 | -------------------------------------------------------------------------------- /test/screenshot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | 5 | const { doc, Image } = require('./init'); 6 | 7 | 8 | const pixelThreshold = 0.2; // threshold error in one pixel 9 | const maxFailedPixels = 100; // total failed pixels 10 | 11 | const makePathDiff = (name) => `test/__diff__/${name}.png`; 12 | const makePathExpected = (name) => `test/__diff__/${name}__expected__.png`; 13 | const makePathActual = (name) => `test/__diff__/${name}__actual__.png`; 14 | const makePathExport = (name) => `__screenshots__/${name}.png`; 15 | 16 | 17 | const allocBuffer = () => { 18 | const memSize = doc.w * doc.h * 4; // estimated number of bytes 19 | return Buffer.allocUnsafeSlow(memSize); 20 | }; 21 | 22 | const getImage = () => { 23 | try { 24 | const storage = { data: allocBuffer() }; 25 | 26 | doc.context.readPixels( 27 | 0, 0, 28 | doc.w, doc.h, 29 | doc.context.RGBA, 30 | doc.context.UNSIGNED_BYTE, 31 | storage 32 | ); 33 | 34 | const img = Image.fromPixels(doc.w, doc.h, 32, storage.data); 35 | return img; 36 | } catch (error) { 37 | console.error(error); 38 | return null; 39 | } 40 | }; 41 | 42 | 43 | const makeScreenshot = (name) => { 44 | console.info(`Screenshot: ${name}`); 45 | const img = getImage(); 46 | if (img) { 47 | img.save(makePathExport(name)); 48 | console.info(`Screenshot: ${name} generated`); 49 | } 50 | }; 51 | 52 | const compareScreenshot = async (name) => { 53 | const path = makePathExport(name); 54 | if (!fs.existsSync(path)) { 55 | console.error(`Warning! No such screenshot: ${name}.`); 56 | return false; 57 | } 58 | 59 | const actualImage = getImage(); 60 | if (!actualImage) { 61 | return false; 62 | } 63 | 64 | const expectedImage = await Image.loadAsync(path); 65 | 66 | const diff = allocBuffer(); 67 | 68 | let numFailedPixels = 0; 69 | 70 | try { 71 | const { default: pixelmatch } = await import('pixelmatch'); 72 | 73 | numFailedPixels = pixelmatch( 74 | expectedImage.data, 75 | actualImage.data, 76 | diff, 77 | actualImage.width, 78 | actualImage.height, 79 | { 80 | threshold: pixelThreshold, 81 | alpha: 0.3, 82 | diffMask: false, 83 | diffColor: [255, 0, 0], 84 | }, 85 | ); 86 | } catch (error) { 87 | console.error(error); 88 | return false; 89 | } 90 | 91 | const pathDiff = makePathDiff(name); 92 | 93 | if (numFailedPixels) { 94 | console.warn(`Screenshot: ${name} - ${numFailedPixels}/${maxFailedPixels}.`); 95 | const pathExpected = makePathExpected(name); 96 | const pathActual = makePathActual(name); 97 | actualImage.save(pathActual); 98 | expectedImage.save(pathExpected); 99 | 100 | const diffImage = Image.fromPixels(doc.w, doc.h, 32, diff); 101 | diffImage.save(pathDiff); 102 | 103 | const isError = numFailedPixels >= maxFailedPixels; 104 | console[isError ? 'error' : 'warn']([ 105 | `Screenshot: ${name}.`, 106 | `Failed pixels: ${numFailedPixels}/${maxFailedPixels}.`, 107 | `Diff written: ${pathDiff}.`, 108 | ].join('\n')); 109 | 110 | return !isError; 111 | } 112 | 113 | return true; 114 | }; 115 | 116 | 117 | const screenshot = async (name) => { 118 | try { 119 | const path = makePathExport(name); 120 | 121 | const isCi = !!process.env['CI']; 122 | const hasFile = fs.existsSync(path); 123 | 124 | if (!hasFile && !isCi) { 125 | await makeScreenshot(name); 126 | return true; 127 | } 128 | 129 | return compareScreenshot(name); 130 | } catch (error) { 131 | console.error(error); 132 | return false; 133 | } 134 | }; 135 | 136 | module.exports = { screenshot }; --------------------------------------------------------------------------------