├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.ts ├── debug │ └── appDebug.ts ├── engineExtensions │ └── engine.bufferSubData.ts ├── inkCanvas.ts ├── materials │ ├── debugMaterial.ts │ ├── debugShader.ts │ ├── rainbowMaterial.ts │ ├── rainbowShader.ts │ ├── simpleMaterial.ts │ └── simpleShader.ts └── path │ ├── pathBufferData.ts │ └── pathMesh.ts ├── tsconfig.json ├── webpack.config.js └── www ├── assets ├── favicon.ico ├── logo.svg └── particle.png └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | # And OS or Editor folders 3 | [Bb]in/ 4 | *.tmp 5 | *.log 6 | *.DS_Store 7 | ._* 8 | Thumbs.db 9 | .cache 10 | .tmproj 11 | nbproject 12 | *.sublime-project 13 | *.sublime-workspace 14 | .idea 15 | .directory 16 | build 17 | .history 18 | .tempChromeProfileForDebug 19 | 20 | # Node Modules 21 | node_modules/* 22 | 23 | # Local dist 24 | dist/* 25 | www/scripts/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Index (Chrome)", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:8080", 9 | "webRoot": "${workspaceRoot}", 10 | "sourceMaps": true, 11 | "preLaunchTask": "devServer", 12 | "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug", 13 | "runtimeArgs": [ 14 | "--enable-unsafe-es3-apis" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/.DS_Store": true, 9 | "**/.vs": true, 10 | "**/.tempChromeProfileForDebug": true, 11 | "**/node_modules": true, 12 | "**/.temp": true 13 | }, 14 | "search.exclude": { 15 | "**/.tempChromeProfileForDebug": true, 16 | "**/node_modules": true, 17 | "**/.temp": true, 18 | "**/.dist": true, 19 | "**/*.map": true, 20 | "**/scripts": true 21 | }, 22 | "editor.tabSize": 4, 23 | "typescript.tsdk": "node_modules\\typescript\\lib" 24 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "command": "npm", 6 | "type": "shell", 7 | "presentation": { 8 | "echo": true, 9 | "reveal": "always", 10 | "focus": false, 11 | "panel": "shared" 12 | }, 13 | "tasks": [ 14 | { 15 | "label": "start", 16 | "args": [ "start" ], 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | }, 21 | "isBackground": true, 22 | "problemMatcher": { 23 | "owner": "typescript", 24 | "fileLocation": "relative", 25 | "pattern": { 26 | "regexp": "^([^\\s].*)\\((\\d+|\\,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 27 | "file": 1, 28 | "location": 2, 29 | "severity": 3, 30 | "code": 4, 31 | "message": 5 32 | }, 33 | "background": { 34 | "activeOnStart": true, 35 | "beginsPattern": "start", 36 | "endsPattern": "Compiled successfully" 37 | } 38 | } 39 | }, 40 | { 41 | "label": "devServer", 42 | "args": [ "run", "devServer" ], 43 | "group": { 44 | "kind": "build", 45 | "isDefault": true 46 | }, 47 | "isBackground": true, 48 | "problemMatcher": { 49 | "owner": "typescript", 50 | "fileLocation": "relative", 51 | "pattern": { 52 | "regexp": "^([^\\s].*)\\((\\d+|\\,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 53 | "file": 1, 54 | "location": 2, 55 | "severity": 3, 56 | "code": 4, 57 | "message": 5 58 | }, 59 | "background": { 60 | "activeOnStart": true, 61 | "beginsPattern": "devServer", 62 | "endsPattern": "Compiled successfully" 63 | } 64 | } 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 sebavan 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 | # Babylon.js Inking DEMO 2 | 3 | [![Twitter](https://img.shields.io/twitter/follow/babylonjs.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=babylonjs) 4 | 5 | **Any questions?** Here is our official [forum](https://forum.babylonjs.com/). 6 | 7 | ## Running locally 8 | 9 | After cloning the repo, running locally during development is all the simplest: 10 | ``` 11 | npm install 12 | npm start 13 | ``` 14 | 15 | For VSCode users, if you have installed the Chrome Debugging extension, you can start debugging within VSCode by using the appropriate launch menu. 16 | 17 | ## Live Demo 18 | 19 | All available in the famous [Babylon.js website](https://www.babylonjs.com/demos/ink). 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babylonjs-ink-sample", 3 | "version": "1.0.0", 4 | "description": "Quick Demo of using the Babylon.js to simulate inking.", 5 | "author": { 6 | "name": "Sebastien Vandenberghe" 7 | }, 8 | "contributors": [ 9 | "Sebastien Vandenberghe" 10 | ], 11 | "keywords": [ 12 | "controls", 13 | "ink", 14 | "video", 15 | "acceleration", 16 | "3D", 17 | "2D", 18 | "javascript", 19 | "html5", 20 | "webgl", 21 | "webgl2", 22 | "webgpu", 23 | "babylon" 24 | ], 25 | "license": "MIT", 26 | "readme": "README.md", 27 | "typings": "dist/src/index.d.ts", 28 | "main": "dist/src/index.js", 29 | "files": [ 30 | "dist/src/**", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "build": "webpack --env=prod", 35 | "start": "npx webpack-dev-server --open", 36 | "devServer": "npx webpack-dev-server" 37 | }, 38 | "devDependencies": { 39 | "@babylonjs/core": "^5.0.0-alpha.54", 40 | "@babylonjs/inspector": "^5.0.0-alpha.54", 41 | "ts-loader": "^9.2.6", 42 | "typescript": "^4.4.4", 43 | "webpack": "^5.94.0", 44 | "webpack-cli": "^4.9.0", 45 | "webpack-dev-server": "^4.12.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Color3 } from "@babylonjs/core/Maths/math.color"; 2 | import { PointerEventTypes } from "@babylonjs/core/Events/pointerEvents"; 3 | import { KeyboardEventTypes } from "@babylonjs/core/Events/keyboardEvents"; 4 | 5 | import { InkCanvas } from "./inkCanvas"; 6 | 7 | // Find our elements 8 | const mainCanvas = document.getElementById("renderCanvas") as HTMLCanvasElement; 9 | const fpsDiv = document.getElementById("fps") as HTMLCanvasElement; 10 | const undoBtn = document.getElementById("undo") as HTMLElement; 11 | const redoBtn = document.getElementById("redo") as HTMLElement; 12 | const clearBtn = document.getElementById("clear") as HTMLElement; 13 | const size1Btn = document.getElementById("size1") as HTMLElement; 14 | const size5Btn = document.getElementById("size5") as HTMLElement; 15 | const size15Btn = document.getElementById("size15") as HTMLElement; 16 | const blackBtn = document.getElementById("black") as HTMLElement; 17 | const whiteBtn = document.getElementById("white") as HTMLElement; 18 | const rainbowBtn = document.getElementById("rainbow") as HTMLElement; 19 | 20 | /** 21 | * Can be set to enter in debug mode. 22 | * Materials will be wireframed and inputs will be debounced 23 | */ 24 | const debug = false; 25 | 26 | // Create our inking surface 27 | const inkCanvas = new InkCanvas(mainCanvas, "./assets/particle.png", debug); 28 | 29 | // Timer Events 30 | setInterval(() => { 31 | fpsDiv.innerText = "FPS: " + inkCanvas.getFps().toFixed(2); 32 | }, 1000); 33 | 34 | // Keyboard events 35 | inkCanvas.onKeyboardObservable.add((e) => { 36 | if (e.type === KeyboardEventTypes.KEYDOWN) { 37 | if (e.event.ctrlKey) { 38 | // Undo 39 | if (e.event.key === 'z') { 40 | inkCanvas.undo(); 41 | } 42 | // Redo 43 | else if (e.event.key === 'y') { 44 | inkCanvas.redo(); 45 | } 46 | // Clear 47 | else if (e.event.key === 'c') { 48 | inkCanvas.clear(); 49 | } 50 | // Debug 51 | else if (e.event.key === 'i') { 52 | inkCanvas.toggleDebugLayer(); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | // Pointer events 59 | inkCanvas.onPointerObservable.add((e) => { 60 | // Create 61 | if(e.type == PointerEventTypes.POINTERDOWN){ 62 | inkCanvas.startPath(); 63 | } 64 | // Trace 65 | else if(e.type == PointerEventTypes.POINTERMOVE){ 66 | inkCanvas.extendPath(); 67 | } 68 | // Release 69 | else if(e.type == PointerEventTypes.POINTERUP){ 70 | inkCanvas.endPath(); 71 | } 72 | }); 73 | 74 | // Buttons Events 75 | undoBtn.onclick = () => { 76 | inkCanvas.undo(); 77 | }; 78 | redoBtn.onclick = () => { 79 | inkCanvas.redo(); 80 | }; 81 | clearBtn.onclick = () => { 82 | inkCanvas.clear(); 83 | }; 84 | size1Btn.onclick = () => { 85 | inkCanvas.changeSize(1); 86 | }; 87 | size5Btn.onclick = () => { 88 | inkCanvas.changeSize(5); 89 | }; 90 | size15Btn.onclick = () => { 91 | inkCanvas.changeSize(15); 92 | }; 93 | blackBtn.onclick = () => { 94 | inkCanvas.changeColor(Color3.Black()); 95 | }; 96 | whiteBtn.onclick = () => { 97 | inkCanvas.changeColor(Color3.White()); 98 | }; 99 | rainbowBtn.onclick = () => { 100 | inkCanvas.useRainbow(); 101 | }; -------------------------------------------------------------------------------- /src/debug/appDebug.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from "@babylonjs/core/scene"; 2 | 3 | // Go big or go... 4 | import "@babylonjs/core/Legacy/legacy"; 5 | import "@babylonjs/core/Debug/debugLayer"; 6 | import "@babylonjs/inspector"; 7 | 8 | /** 9 | * Toggles on/off the inspector in the scene. 10 | * Keeping this as a separate file helps with code splitting and let us 11 | * Only download the full bjs code if debug mode has been requested. 12 | * @param scene defines the sence to inspect 13 | */ 14 | export function toggleDebugMode(scene: Scene): void { 15 | if (scene.debugLayer.isVisible()) { 16 | scene.debugLayer.hide(); 17 | } 18 | else { 19 | scene.debugLayer.show({ 20 | embedMode: true 21 | }); 22 | } 23 | } -------------------------------------------------------------------------------- /src/engineExtensions/engine.bufferSubData.ts: -------------------------------------------------------------------------------- 1 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine"; 2 | import { DataBuffer } from "@babylonjs/core/Buffers/dataBuffer"; 3 | 4 | declare module "@babylonjs/core/Engines/thinEngine" { 5 | export interface ThinEngine { 6 | /** 7 | * Update a gpu index buffer 8 | * @param indexBuffer defines the buffer to update 9 | * @param dstByteOffset defines where to start updating the data in bytes in the gpu buffer 10 | * @param data defines the data to upload to the gpu 11 | * @param srcOffset defines the index where to start the copy of data in the source buffer 12 | * @param srcLength defines the number of UInt32 elements to copy 13 | */ 14 | indexBufferSubData(indexBuffer: DataBuffer, dstByteOffset: number, data: Uint32Array, srcOffset: number, srcLength: number): void; 15 | /** 16 | * Update a gpu vertex buffer 17 | * @param dstByteOffset defines where to start updating the data in bytes in the gpu buffer 18 | * @param data defines the data to upload to the gpu 19 | * @param srcOffset defines the index where to start the copy of data in the source buffer 20 | * @param srcLength defines the number of Float32 elements to copy 21 | */ 22 | vertexBufferSubData(vertexBuffer: DataBuffer, dstByteOffset: number, data: Float32Array, srcOffset: number, srcLength: number): void; 23 | } 24 | } 25 | 26 | ThinEngine.prototype.indexBufferSubData = function(this: ThinEngine, indexBuffer: DataBuffer, dstByteOffset: number, data: Uint32Array, srcOffset: number, srcLength: number): void { 27 | // Force cache update 28 | this._currentBoundBuffer[this._gl.ELEMENT_ARRAY_BUFFER] = null; 29 | this.bindIndexBuffer(indexBuffer); 30 | 31 | if (this.webGLVersion === 2) { 32 | (this._gl as unknown as WebGL2RenderingContext).bufferSubData(this._gl.ELEMENT_ARRAY_BUFFER, dstByteOffset, data, srcOffset, srcLength); 33 | } 34 | else { 35 | const dataView = new Uint32Array(data.buffer, srcOffset * 4, srcLength); 36 | this._gl.bufferSubData(this._gl.ELEMENT_ARRAY_BUFFER, dstByteOffset, dataView); 37 | } 38 | 39 | this._resetIndexBufferBinding(); 40 | } 41 | 42 | ThinEngine.prototype.vertexBufferSubData = function(this: ThinEngine, vertexBuffer: DataBuffer, dstByteOffset: number, data: Float32Array, srcOffset: number, srcLength: number): void { 43 | this.bindArrayBuffer(vertexBuffer); 44 | 45 | if (this.webGLVersion === 2) { 46 | (this._gl as unknown as WebGL2RenderingContext).bufferSubData(this._gl.ARRAY_BUFFER, dstByteOffset, data, srcOffset, srcLength); 47 | } 48 | else { 49 | const dataView = new Float32Array(data.buffer, srcOffset * 4, srcLength); 50 | this._gl.bufferSubData(this._gl.ARRAY_BUFFER, dstByteOffset, dataView); 51 | } 52 | 53 | this._resetVertexBufferBinding(); 54 | }; -------------------------------------------------------------------------------- /src/inkCanvas.ts: -------------------------------------------------------------------------------- 1 | import { Engine } from "@babylonjs/core/Engines/engine"; 2 | import { Scene } from "@babylonjs/core/scene"; 3 | import { Color4, Color3 } from "@babylonjs/core/Maths/math.color"; 4 | import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera"; 5 | import { Vector3 } from "@babylonjs/core/Maths/math.vector"; 6 | import { Camera } from "@babylonjs/core/Cameras/camera"; 7 | import { Material } from "@babylonjs/core/Materials/material"; 8 | import { PointerInfo } from "@babylonjs/core/Events/pointerEvents"; 9 | import { Nullable } from "@babylonjs/core/types"; 10 | import { KeyboardInfo } from "@babylonjs/core/Events/keyboardEvents"; 11 | import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; 12 | import { Texture } from "@babylonjs/core/Materials/Textures"; 13 | import { Observable } from "@babylonjs/core/Misc/observable"; 14 | 15 | import { PathBufferDataOptions } from "./path/pathBufferData"; 16 | import { PathMesh } from "./path/pathMesh"; 17 | 18 | import { createDebugMaterial } from "./materials/debugMaterial"; 19 | import { createSimpleMaterial } from "./materials/simpleMaterial"; 20 | import { createRainbowMaterial, getColorAtToRef } from "./materials/rainbowMaterial"; 21 | 22 | /** 23 | * Various brush modes 24 | */ 25 | const enum Brush { 26 | /** 27 | * Normal pen mode 28 | */ 29 | pen, 30 | /** 31 | * Rainbow mode 32 | */ 33 | rainbow, 34 | } 35 | 36 | /** 37 | * The canvas is responsible to create and orchestrate all the resources 38 | * the ink platform would need (scene, camera...) 39 | */ 40 | export class InkCanvas { 41 | private readonly _debug: boolean; 42 | private readonly _particleTextureURL: string; 43 | private readonly _scene: Scene; 44 | private readonly _pointerNode: Vector3; 45 | private readonly _particleSystem: ParticleSystem; 46 | 47 | private readonly _paths: PathMesh[]; 48 | private readonly _redoPaths: PathMesh[]; 49 | 50 | private _currentPath: Nullable = null; 51 | private _currentSize: number; 52 | private _currentColor: Color3; 53 | private _currentMode: Brush; 54 | 55 | /** 56 | * Creates an instance of an ink canvas associated to a html canvas element 57 | * @param canvas defines the html element to transform into and ink surface 58 | * @param particleTextureURL defines the URL of the texture used for the rainbow particle effects 59 | * @param debug defines wheter the ink canvas is in debug mode or not (wireframe, input debounced...) 60 | */ 61 | constructor(canvas: HTMLCanvasElement, particleTextureURL: string, debug = false) { 62 | this._debug = debug; 63 | this._particleTextureURL = particleTextureURL; 64 | this._paths = []; 65 | this._redoPaths = []; 66 | this._currentPath = null; 67 | this._currentSize = 5; 68 | this._currentColor = Color3.White(); 69 | this._currentMode = Brush.rainbow; 70 | 71 | this._scene = this._createScene(canvas); 72 | this._pointerNode = new Vector3(0, 0, 0); 73 | this._particleSystem = this._createParticleSystem(); 74 | } 75 | 76 | /** 77 | * Gets the keyboard observable for the current canvas 78 | */ 79 | public get onKeyboardObservable(): Observable { 80 | return this._scene.onKeyboardObservable; 81 | } 82 | 83 | /** 84 | * Gets the pointer observable for the current canvas 85 | */ 86 | public get onPointerObservable(): Observable { 87 | return this._scene.onPointerObservable; 88 | } 89 | 90 | /** 91 | * Starts creating a new path at the location of the pointer 92 | */ 93 | public startPath(): void { 94 | if (this._currentPath) { 95 | return; 96 | } 97 | 98 | // Cleanup the redo list 99 | this._redoPaths.length = 0; 100 | 101 | // Create the new path mesh and assigns its material 102 | this._currentPath = this._createPath(this._scene.pointerX, this._scene.pointerY); 103 | this._currentPath.material = this._createPathMaterial(); 104 | 105 | // Quick Optim 106 | this._currentPath.isPickable = false; 107 | this._currentPath.material.freeze(); 108 | this._currentPath.alwaysSelectAsActiveMesh = true; 109 | this._currentPath.freezeWorldMatrix(); 110 | 111 | // Starts the particles in rainbow mode 112 | if (this._currentMode === Brush.rainbow) { 113 | this._updateParticleSystem(); 114 | this._particleSystem.start(); 115 | } 116 | } 117 | 118 | /** 119 | * Extends the path to the new pointer location 120 | */ 121 | public extendPath(): void { 122 | if (!this._currentPath) { 123 | return; 124 | } 125 | 126 | // Add a new point to the path 127 | this._currentPath.addPointToPath(this._scene.pointerX, this._scene.pointerY); 128 | 129 | // Updates the particles in rainbow mode 130 | if (this._currentMode === Brush.rainbow) { 131 | this._updateParticleSystem(); 132 | } 133 | } 134 | 135 | /** 136 | * Ends the current path 137 | */ 138 | public endPath(): void { 139 | if (!this._currentPath) { 140 | return; 141 | } 142 | 143 | // Adds the path to our undo list 144 | this._paths.push(this._currentPath); 145 | 146 | // Clear the current path 147 | this._currentPath = null; 148 | 149 | // Stops the particle system 150 | this._particleSystem.stop(); 151 | } 152 | 153 | /** 154 | * Undo the latest created path 155 | */ 156 | public undo(): void { 157 | if (!this._currentPath && this._paths.length > 0) { 158 | const path = this._paths.pop(); 159 | this._redoPaths.push(path); 160 | this._scene.removeMesh(path); 161 | } 162 | } 163 | 164 | /** 165 | * Redo the latest undone path 166 | */ 167 | public redo(): void { 168 | if (!this._currentPath && this._redoPaths.length > 0) { 169 | const path = this._redoPaths.pop(); 170 | this._paths.push(path); 171 | this._scene.addMesh(path); 172 | } 173 | } 174 | 175 | /** 176 | * Clear all the created path 177 | */ 178 | public clear(): void { 179 | if (!this._currentPath && this._paths.length > 0) { 180 | let path: PathMesh; 181 | while (path = this._paths.pop()) { 182 | path.dispose(); 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Change the size of the current brush 189 | */ 190 | public changeSize(size: number): void { 191 | this._currentSize = size; 192 | this._particleSystem.createSphereEmitter(this._currentSize, 0.5); 193 | } 194 | 195 | /** 196 | * Change the color of the current pen 197 | */ 198 | public changeColor(color: Color3): void { 199 | this._currentColor = color; 200 | this.usePen(); 201 | } 202 | 203 | /** 204 | * Switch to pen mode 205 | */ 206 | public usePen(): void { 207 | this._currentMode = Brush.pen; 208 | } 209 | 210 | /** 211 | * Switch to rainbow mode 212 | */ 213 | public useRainbow(): void { 214 | this._currentMode = Brush.rainbow; 215 | } 216 | 217 | /** 218 | * Get the current framerate 219 | */ 220 | public getFps(): number { 221 | return this._scene.getEngine().getFps(); 222 | } 223 | 224 | /** 225 | * Toggle the Babylon almighty inspector 226 | */ 227 | public toggleDebugLayer(): Promise { 228 | // Rely on code splitting to prevent all of babylon 229 | // + loaders, serializers... to be downloaded if not necessary 230 | return import(/* webpackChunkName: "debug" */ "./debug/appDebug").then((debugModule) => { 231 | debugModule.toggleDebugMode(this._scene); 232 | }); 233 | } 234 | 235 | private _updateParticleSystem(): void { 236 | // Update the current particle emitter 237 | this._pointerNode.x = this._scene.pointerX; 238 | this._pointerNode.y = this._scene.pointerY; 239 | 240 | // Gets the interpolated color for the rainbow particle 241 | getColorAtToRef(this._currentPath.totalLength, this._particleSystem.color2) 242 | getColorAtToRef(this._currentPath.totalLength, this._particleSystem.color1) 243 | 244 | } 245 | 246 | private _createScene(canvas: HTMLCanvasElement): Scene { 247 | // Create our engine to hold on the canvas 248 | const engine = new Engine(canvas, true, { 249 | preserveDrawingBuffer: false, 250 | alpha: false, 251 | }); 252 | engine.preventCacheWipeBetweenFrames = true; 253 | 254 | // Create a scene to ink with 255 | const scene = new Scene(engine); 256 | 257 | // no need to clear here as we do not preserve buffers 258 | scene.autoClearDepthAndStencil = false; 259 | 260 | // Ensures default is part of our supported use cases. 261 | scene.defaultMaterial = createSimpleMaterial("default", scene, Color3.White()); 262 | 263 | // A nice and fancy background color 264 | const clearColor = new Color4(77 / 255, 86 / 255, 92 / 255, 1); 265 | scene.clearColor = clearColor; 266 | 267 | // Add a camera to the scene 268 | const camera = new FreeCamera("orthoCamera", new Vector3(0, 0, -3), scene); 269 | this._setupCamera(camera, engine.getRenderWidth(), engine.getRenderHeight()); 270 | 271 | // Rely on the underlying engine render loop to update the filter result every frame. 272 | engine.runRenderLoop(() => { 273 | scene.render(); 274 | }); 275 | 276 | // OnResize 277 | engine.onResizeObservable.add(() => { 278 | this._setupCamera(camera, engine.getRenderWidth(), engine.getRenderHeight()); 279 | }); 280 | 281 | return scene; 282 | } 283 | 284 | private _setupCamera(camera: Camera, width: number, height: number): void { 285 | // We chose an orthographic view to simplify at most our mesh creation 286 | camera.mode = Camera.ORTHOGRAPHIC_CAMERA; 287 | 288 | // Setup the camera to fit with our gl coordinates in the canvas 289 | camera.unfreezeProjectionMatrix(); 290 | camera.orthoTop = 0; 291 | camera.orthoLeft = 0; 292 | camera.orthoBottom = height; 293 | camera.orthoRight = width; 294 | camera.getProjectionMatrix(true); 295 | camera.freezeProjectionMatrix(); 296 | } 297 | 298 | private _createParticleSystem(): ParticleSystem { 299 | // Create a particle system 300 | const particleSystem = new ParticleSystem("particles", 1500, this._scene); 301 | 302 | // Texture of each particle 303 | particleSystem.particleTexture = new Texture(this._particleTextureURL, this._scene); 304 | 305 | // Where the particles come from 306 | particleSystem.emitter = this._pointerNode; // the starting location 307 | 308 | // Colors of all particles 309 | particleSystem.color1 = new Color4(0.99, 0.99, 0.99); 310 | particleSystem.color2 = new Color4(1, 0.98, 0); 311 | particleSystem.colorDead = new Color4(0.1, 0.1, 0.1, 0.1); 312 | 313 | // Size of each particle; random between... 314 | particleSystem.minSize = 1; 315 | particleSystem.maxSize = 8; 316 | 317 | // Life time of each particle; random between... 318 | particleSystem.minLifeTime = 0.1; 319 | particleSystem.maxLifeTime = 0.2; 320 | 321 | // Emission rate 322 | particleSystem.emitRate = 5000; 323 | 324 | // Emission Space 325 | particleSystem.createSphereEmitter(this._currentSize, 0.3); 326 | 327 | // Speed 328 | particleSystem.minEmitPower = 70; 329 | particleSystem.maxEmitPower = 100; 330 | particleSystem.updateSpeed = 0.005; 331 | 332 | return particleSystem; 333 | } 334 | 335 | private _createPathMaterial(): Material { 336 | // Creates a material for the path according to our current inking 337 | // setup. 338 | 339 | if (this._debug) { 340 | const pathMaterial = createDebugMaterial("debugMaterial", this._scene); 341 | return pathMaterial; 342 | } 343 | 344 | if (this._currentMode === Brush.pen) { 345 | const pathMaterial = createSimpleMaterial("pathMaterial", this._scene, this._currentColor); 346 | return pathMaterial; 347 | } 348 | 349 | const pathMaterial = createRainbowMaterial("pathMaterial", this._scene); 350 | return pathMaterial; 351 | } 352 | 353 | private _createPath(x: number, y: number): PathMesh { 354 | // Creates a path mesh according to our current inking setup 355 | 356 | let options: Partial = { 357 | radius: this._currentSize 358 | } 359 | 360 | if (this._debug) { 361 | options.debounce = 1; 362 | options.roundness = 8; 363 | } 364 | 365 | const path = new PathMesh('path', this._scene, x, y, options); 366 | return path; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/materials/debugMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from "@babylonjs/core/scene"; 2 | import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial"; 3 | 4 | import { DebugShaderConfiguration } from "./debugShader"; 5 | 6 | /** 7 | * Creates a new instance of a debug material. 8 | * (A basic wireframe material with rolling over distance to under the setup and topology) 9 | * @param name defines the name of the material 10 | * @param scene defines the scene the material belongs to 11 | * @returns the created material 12 | */ 13 | export function createDebugMaterial(name: string, scene: Scene): ShaderMaterial { 14 | // We simply use a shader material for this. 15 | const shaderMaterial = new ShaderMaterial(name, scene, DebugShaderConfiguration, { 16 | attributes: DebugShaderConfiguration.attributes, 17 | uniforms: DebugShaderConfiguration.uniformNames 18 | }); 19 | 20 | // In wireframe mode. 21 | shaderMaterial.wireframe = true; 22 | 23 | return shaderMaterial; 24 | } -------------------------------------------------------------------------------- /src/materials/debugShader.ts: -------------------------------------------------------------------------------- 1 | 2 | // This shader is used to debug our path data. 3 | 4 | const vertexShader = ` 5 | // Attributes 6 | attribute vec3 position; 7 | attribute float distance; 8 | 9 | // Transform main transform for local space to clip 10 | uniform mat4 worldViewProjection; 11 | 12 | // Output 13 | varying float vDistance; 14 | 15 | void main(void) { 16 | // Position 17 | vec4 p = vec4(position.xyz, 1.); 18 | 19 | vDistance = distance / 1000.; 20 | 21 | gl_Position = worldViewProjection * p; 22 | }`; 23 | 24 | const fragmentShader = ` 25 | // Inputs from vertex 26 | varying float vDistance; 27 | 28 | // Main function 29 | void main(void) { 30 | vec3 debugColor = vec3(0., cos(vDistance) * 0.5 + 0.5, 1.); 31 | gl_FragColor = vec4(debugColor, 1.0); 32 | }`; 33 | 34 | /** 35 | * Defines all the data required for our effect 36 | */ 37 | export const DebugShaderConfiguration = { 38 | name: "Debug", 39 | fragment: "Debug", 40 | vertexSource: vertexShader, 41 | fragmentSource: fragmentShader, 42 | attributes: ["position", "distance"], 43 | uniformNames: ["worldViewProjection"], 44 | } -------------------------------------------------------------------------------- /src/materials/rainbowMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from "@babylonjs/core/scene"; 2 | import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial"; 3 | import { PrecisionDate } from "@babylonjs/core/Misc/precisionDate"; 4 | import { RawTexture } from "@babylonjs/core/Materials/Textures/rawTexture"; 5 | import { Constants } from "@babylonjs/core/Engines/constants"; 6 | import { Vector2 } from "@babylonjs/core/Maths/math.vector"; 7 | import { Color4 } from "@babylonjs/core/Maths/math.color"; 8 | import { Scalar } from "@babylonjs/core/Maths/math.scalar"; 9 | 10 | import { RainbowShaderConfiguration } from "./rainbowShader"; 11 | 12 | /** 13 | * The Rainbow colors 14 | */ 15 | const colorLookup = new Uint8Array([ 16 | 148, 0, 211, 255, 17 | 75, 0, 130, 255, 18 | 0, 0, 255, 255, 19 | 0, 255, 0, 255, 20 | 255, 255, 0, 255, 21 | 255, 187, 0, 255, 22 | 255, 88, 0, 255, 23 | 255, 0, 0, 255, 24 | ]); 25 | 26 | /** 27 | * The number of colors in our rainbow 28 | */ 29 | const colorsCount = colorLookup.length / 4; 30 | 31 | /** 32 | * Get the color we find at a certain distance from the begining of the stroke 33 | * This interpolates as the GPU would do to ensure a matching color scheme 34 | * @param distance defines the distance from the begining of the stroke we want to know the color for 35 | * @param result the color we want to update with the result to prevent GC 36 | */ 37 | export function getColorAtToRef(distance: number, result: Color4): void { 38 | // copied setup from the rainbow shader. (one full color loop on 1000px) 39 | distance = distance % 1000; 40 | // go back between 0 and 1 41 | distance = distance / 1000; 42 | 43 | // let's compute the colors index in the array (the one right before and right after) 44 | distance = distance * (colorsCount - 1); 45 | const index1 = Math.floor(distance) * 4; 46 | const index2 = Math.ceil(distance) * 4; 47 | 48 | // Keep only the floating part to lerp between both color 49 | distance = distance - Math.floor(distance); 50 | 51 | // Lerp Lerp Lerp 52 | result.r = Scalar.Lerp(colorLookup[index1 + 0], colorLookup[index2 + 0], distance) / 255; 53 | result.g = Scalar.Lerp(colorLookup[index1 + 1], colorLookup[index2 + 1], distance) / 255; 54 | result.b = Scalar.Lerp(colorLookup[index1 + 2], colorLookup[index2 + 2], distance) / 255; 55 | } 56 | 57 | /** 58 | * Creates a new instance of a rainbow material. 59 | * (this material change colors along the distance attribute in a wrapped way) 60 | * @param name defines the name of the material 61 | * @param scene defines the scene the material belongs to 62 | * @returns the created material 63 | */ 64 | export function createRainbowMaterial(name: string, scene: Scene): ShaderMaterial { 65 | // Create a lookup texture from the rainbow colors 66 | const lookup = RawTexture.CreateRGBATexture(colorLookup, 8, 1, scene); 67 | lookup.wrapU = Constants.TEXTURE_WRAP_ADDRESSMODE; 68 | lookup.wrapV = Constants.TEXTURE_WRAP_ADDRESSMODE; 69 | 70 | // A simple shader material is enought for the rainbow 71 | const shaderMaterial = new ShaderMaterial(name, scene, RainbowShaderConfiguration, { 72 | attributes: RainbowShaderConfiguration.attributes, 73 | uniforms: RainbowShaderConfiguration.uniformNames, 74 | samplers: RainbowShaderConfiguration.samplerNames 75 | }); 76 | 77 | // Sets our required values for the shader 78 | const screenSize = new Vector2(scene.getEngine().getRenderWidth(), scene.getEngine().getRenderHeight()); 79 | shaderMaterial.setVector2("screenSize", screenSize); 80 | shaderMaterial.setFloat("offset", 10); 81 | shaderMaterial.setTexture("rainbowLookup", lookup); 82 | 83 | // On every 6 frames... because it looks ok, update our offset to provide 84 | // a glittery look 85 | let debounceShimmerValue = 0; 86 | scene.onBeforeRenderObservable.add(() => { 87 | debounceShimmerValue++; 88 | debounceShimmerValue = debounceShimmerValue % 6; 89 | if (debounceShimmerValue === 0) { 90 | const time = PrecisionDate.Now / 10000 % 400; 91 | shaderMaterial.setFloat("offset", time); 92 | } 93 | }); 94 | 95 | return shaderMaterial; 96 | } -------------------------------------------------------------------------------- /src/materials/rainbowShader.ts: -------------------------------------------------------------------------------- 1 | 2 | // This shader is used to simulate a rainbow effect along a path. 3 | // Fake particles should simmer over time. 4 | 5 | const vertexShader = ` 6 | // Attributes 7 | attribute vec3 position; 8 | attribute float distance; 9 | 10 | // Transform main transform for local space to clip 11 | uniform mat4 worldViewProjection; 12 | 13 | // Output 14 | varying vec3 vPosition; 15 | varying float vDistance; 16 | 17 | void main(void) { 18 | // Position 19 | vec4 p = vec4(position.xyz, 1.); 20 | 21 | vPosition = position.xyz; 22 | vDistance = distance / 1000.; 23 | 24 | gl_Position = worldViewProjection * p; 25 | }`; 26 | 27 | const fragmentShader = ` 28 | // Inputs from vertex 29 | varying vec3 vPosition; 30 | varying float vDistance; 31 | 32 | // Rainbow Color Lookup 33 | uniform sampler2D rainbowLookup; 34 | 35 | // Screen Size 36 | uniform vec2 screenSize; 37 | 38 | // Time offset 39 | uniform float offset; 40 | 41 | // Helper functions 42 | float getRand(vec2 seed) { 43 | return fract(sin(dot(seed.xy ,vec2(12.9898,78.233))) * 43758.5453); 44 | } 45 | float dither(vec2 seed, float varianceAmount) { 46 | float rand = getRand(seed); 47 | float value = mix(-varianceAmount/255.0, varianceAmount/255.0, rand); 48 | 49 | return value; 50 | } 51 | 52 | // Main function 53 | void main(void) { 54 | vec2 halfScreen = screenSize / 2.; 55 | float staticGlitters = dither((vPosition.xy + halfScreen) / (screenSize + halfScreen), 230.); 56 | staticGlitters = clamp(staticGlitters, 0.0, 1.0); 57 | 58 | vec2 xyOffset = vec2(offset, offset); 59 | float dynamicGlitters = dither((vPosition.xy + xyOffset) / (screenSize + xyOffset), 200.); 60 | dynamicGlitters = clamp(dynamicGlitters, -0.2, 1.0); 61 | 62 | float totalGlitters = mix(staticGlitters, dynamicGlitters, 0.3); 63 | 64 | vec3 rainbowColor = texture2D(rainbowLookup, vec2(vDistance, 0.5)).rgb; 65 | vec3 finalColor = rainbowColor + totalGlitters; 66 | 67 | gl_FragColor = vec4(finalColor, 1.0); 68 | }`; 69 | 70 | /** 71 | * Defines all the data required for our effect 72 | */ 73 | export const RainbowShaderConfiguration = { 74 | name: "Rainbow", 75 | fragment: "Rainbow", 76 | vertexSource: vertexShader, 77 | fragmentSource: fragmentShader, 78 | attributes: ["position", "distance"], 79 | uniformNames: ["worldViewProjection", "screenSize", "offset"], 80 | samplerNames: ["rainbowLookup"], 81 | } -------------------------------------------------------------------------------- /src/materials/simpleMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from "@babylonjs/core/scene"; 2 | import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial"; 3 | import { Color3 } from "@babylonjs/core/Maths/math.color"; 4 | 5 | import { SimpleShaderConfiguration } from "./simpleShader"; 6 | 7 | /** 8 | * Creates a new instance of a simple material. 9 | * (A basic monochrome material well fitted for lines) 10 | * @param name defines the name of the material 11 | * @param scene defines the scene the material belongs to 12 | * @param color defines the... color of the material 13 | * @returns the created material 14 | */ 15 | export function createSimpleMaterial(name: string, scene: Scene, color: Color3): ShaderMaterial { 16 | // We simply use a shader material for this. 17 | const shaderMaterial = new ShaderMaterial(name, scene, SimpleShaderConfiguration, { 18 | attributes: SimpleShaderConfiguration.attributes, 19 | uniforms: SimpleShaderConfiguration.uniformNames 20 | }); 21 | 22 | // Sets the requested color on our shader 23 | shaderMaterial.setColor3("color", color); 24 | 25 | return shaderMaterial; 26 | } -------------------------------------------------------------------------------- /src/materials/simpleShader.ts: -------------------------------------------------------------------------------- 1 | 2 | // This shader is used to simply display a static color at the requested 3 | // position. 4 | 5 | const vertexShader = ` 6 | // Attributes 7 | attribute vec3 position; 8 | 9 | // Transform main transform for local space to clip 10 | uniform mat4 worldViewProjection; 11 | 12 | void main(void) { 13 | // Position 14 | vec4 p = vec4(position.xyz, 1.); 15 | gl_Position = worldViewProjection * p; 16 | }`; 17 | 18 | const fragmentShader = ` 19 | // Inputs 20 | uniform vec3 color; 21 | 22 | // Main function 23 | void main(void) { 24 | gl_FragColor = vec4(color, 1.0); 25 | }`; 26 | 27 | /** 28 | * Defines all the data required for our effect 29 | */ 30 | export const SimpleShaderConfiguration = { 31 | name: "Simple", 32 | fragment: "Simple", 33 | vertexSource: vertexShader, 34 | fragmentSource: fragmentShader, 35 | attributes: ["position"], 36 | uniformNames: ["worldViewProjection", "color"], 37 | } -------------------------------------------------------------------------------- /src/path/pathBufferData.ts: -------------------------------------------------------------------------------- 1 | // Import our Shader Config 2 | import { Vector2 } from "@babylonjs/core/Maths/math.vector"; 3 | 4 | /** 5 | * Defines the set of options available to create path buffer data 6 | */ 7 | export interface PathBufferDataOptions { 8 | /** 9 | * Defines what is the min distance between 2 added points 10 | * to start smoothing. 11 | */ 12 | smoothingDistance: number; 13 | /** 14 | * Defines the end caps roundness (how many subdivs the points would have). 15 | */ 16 | roundness: number; 17 | /** 18 | * Defines the radius of the path. 19 | */ 20 | radius: number; 21 | /** 22 | * Defines how many added points do we debounce. 23 | * (can be a great help while debugging to simulate slowing down pointer events) 24 | */ 25 | debounce: number; 26 | } 27 | 28 | /** 29 | * Defines the set of data changed while adding data 30 | */ 31 | export interface PathBufferDataChanges { 32 | /** 33 | * Defines the minimum index that changes during the path changes 34 | */ 35 | indexStart: number; 36 | /** 37 | * Defines the maximum index that changes during the path changes 38 | */ 39 | indexEnd: number; 40 | /** 41 | * Defines the minimum position that changes during the path changes 42 | */ 43 | vertexPositionStart: number; 44 | /** 45 | * Defines the maximum position that changes during the path changes 46 | */ 47 | vertexPositionEnd: number; 48 | /** 49 | * Defines the minimum distance that changes during the path changes 50 | */ 51 | vertexDistanceStart: number; 52 | /** 53 | * Defines the maximum distance that changes during the path changes 54 | */ 55 | vertexDistanceEnd: number; 56 | } 57 | 58 | /** 59 | * The default options setup 60 | */ 61 | const DefaultOptions: PathBufferDataOptions = { 62 | smoothingDistance: 20, 63 | roundness: 16, 64 | radius: 5, 65 | debounce: 1, 66 | } 67 | 68 | /** 69 | * This class helps creating a mesh according to a list of points being 70 | * added to it. 71 | * 72 | * It will expose all the required buffer data to be wrappable in any gl contexts. 73 | * 74 | * It will try to limit GC and over allocation. 75 | */ 76 | export class PathBufferData { 77 | 78 | private static readonly _VerticesStartSize = 10000; 79 | private static readonly _VerticesExpansionRate = 2; 80 | 81 | /** 82 | * The positions vertex buffer raw data as float (vec3) 83 | */ 84 | public positions: Float32Array; 85 | /** 86 | * The distances vertex buffer raw data as float (float) 87 | */ 88 | public distances: Float32Array; 89 | /** 90 | * The indices buffer raw data as UInt32 (TRIANGLE) 91 | */ 92 | public indices: Uint32Array; 93 | /** 94 | * The number of meaningfull data in the indices buffer (to help drawing only 95 | * the relevant information as the buffers might be bigger) 96 | */ 97 | public indicesCount: number; 98 | 99 | private readonly _options: PathBufferDataOptions; 100 | private readonly _smoothingDistance: number; 101 | private readonly _roundness: number; 102 | private readonly _radius: number; 103 | private readonly _debounce: number; 104 | private readonly _roundnessSliceAlpha: number; 105 | private readonly _maxAddedVerticesPerPoint: number; 106 | private readonly _currentChanges: PathBufferDataChanges; 107 | 108 | private _maxVerticesCount: number; 109 | 110 | private _nextVertexIndex: number; 111 | private _nextTriangleIndex: number; 112 | private _previousPoint: Vector2; 113 | private _currentPoint: Vector2; 114 | 115 | private _points = []; 116 | 117 | // Update temp data preventing GC 118 | private _previous_translation: Vector2 = new Vector2(); 119 | private _previous_thicknessDirection: Vector2 = new Vector2(); 120 | private _previous_thicknessDirectionScaled: Vector2 = new Vector2(); 121 | private _previous_p1: Vector2 = new Vector2(); 122 | private _previous_p2: Vector2 = new Vector2(); 123 | private _previous_p3: Vector2 = new Vector2(); 124 | private _previous_p4: Vector2 = new Vector2(); 125 | private _previous_p2Index = 0; 126 | private _previous_p3Index = 0; 127 | private _previous_length = 0; 128 | private _translation: Vector2 = new Vector2(); 129 | private _thicknessDirection: Vector2 = new Vector2(); 130 | private _thicknessDirectionScaled: Vector2 = new Vector2(); 131 | private _p1: Vector2 = new Vector2(); 132 | private _p2: Vector2 = new Vector2(); 133 | private _p3: Vector2 = new Vector2(); 134 | private _p4: Vector2 = new Vector2(); 135 | private _p1p2: Vector2 = new Vector2(); 136 | private _p1previous_p2: Vector2 = new Vector2(); 137 | private _currentDebounce = 0; 138 | private _smoothing_previousPoint = new Vector2(); 139 | private _smoothing_newPoint = new Vector2(); 140 | private _smoothing_temp = new Vector2(); 141 | 142 | /** 143 | * Creates a new instance of the path buffer data. 144 | * @param options defines the various options impacting how we generate the path 145 | */ 146 | constructor(options: Partial = DefaultOptions) { 147 | this._options = { 148 | ...DefaultOptions, 149 | ...options, 150 | }; 151 | 152 | this._smoothingDistance = Math.max(5, this._options.smoothingDistance); 153 | this._debounce = Math.max(1, this._options.debounce); 154 | 155 | // Compute default slice rotation angle according to the roundness. 156 | this._roundness = Math.max(4, this._options.roundness); 157 | this._radius = Math.max(1, this._options.radius); 158 | this._roundnessSliceAlpha = 2 * Math.PI / this._roundness; 159 | 160 | // We can at max add as many vertices than half the roundness 161 | // plus the mid point and the new quad for the current point. 162 | this._maxAddedVerticesPerPoint = (this._roundness / 2 + 1 + 4) + 20; 163 | 164 | this._nextVertexIndex = 0; 165 | this._nextTriangleIndex = 0; 166 | this._previousPoint = new Vector2(0, 0); 167 | this._currentPoint = new Vector2(0, 0); 168 | 169 | this._maxVerticesCount = PathBufferData._VerticesStartSize; 170 | this._createBuffers(this._maxVerticesCount); 171 | 172 | // No data so far 173 | this.indicesCount = 0; 174 | this._currentChanges = { 175 | indexStart: 0, 176 | indexEnd: 0, 177 | vertexPositionStart: 0, 178 | vertexPositionEnd: 0, 179 | vertexDistanceStart: 0, 180 | vertexDistanceEnd: 0, 181 | } 182 | } 183 | 184 | /** 185 | * Get the total length of the path (accumulated distance between each points) 186 | */ 187 | public get totalLength(): number { 188 | return this._previous_length; 189 | } 190 | 191 | /** 192 | * Adds a new point to the path. 193 | * @param x defines the x coordinates of the path 194 | * @param y defines the x coordinates of the path 195 | */ 196 | public addPointToPath(x: number, y: number): PathBufferDataChanges { 197 | // Reset the changes 198 | this._currentChanges.indexStart = Number.MAX_VALUE; 199 | this._currentChanges.indexEnd = 0; 200 | this._currentChanges.vertexPositionStart = Number.MAX_VALUE; 201 | this._currentChanges.vertexPositionEnd = 0; 202 | this._currentChanges.vertexDistanceStart = Number.MAX_VALUE; 203 | this._currentChanges.vertexDistanceEnd = 0; 204 | 205 | const pointsLength = this._points.length; 206 | if (pointsLength === 0) { 207 | // The first point is directly added 208 | this._addPoint(x, y); 209 | } 210 | // The subsequent ones needs to be different 211 | else if (this._previousPoint.x !== x || this._previousPoint.y !== y) { 212 | 213 | // Also we debounce our inputs here for debug purpose 214 | this._currentDebounce++; 215 | this._currentDebounce = this._currentDebounce % this._debounce; 216 | if (this._currentDebounce !== 0) { 217 | return this._currentChanges; 218 | } 219 | 220 | // We compute the distance from the previous point 221 | this._currentPoint.set(x, y); 222 | this._currentPoint.subtractInPlace(this._previousPoint); 223 | const dist = this._currentPoint.length(); 224 | 225 | // Only if we are superior to half the radius 226 | if (dist > this._radius / 2) { 227 | if (pointsLength <= 2) { 228 | // Under 2 points we can not smooth the lines. 229 | this._addMidPoint(x, y); 230 | } 231 | else { 232 | // As soon as we have at least 2 previous points we can start smoothing 233 | this._addPointsSmoothly(x, y); 234 | } 235 | } 236 | } 237 | 238 | this._points.push(x, y); 239 | return this._currentChanges; 240 | } 241 | 242 | //_______________ SMOOTHING ______________ 243 | 244 | private _addMidPoint(x: number, y: number): void { 245 | const length = this._points.length; 246 | 247 | // Compute the Mid Point between our last inputs and the new one 248 | const x1 = this._points[length - 2]; 249 | const y1 = this._points[length - 1]; 250 | this._smoothing_previousPoint.set(x1, y1); 251 | this._smoothing_newPoint.set(x, y); 252 | this._smoothing_newPoint.subtractToRef(this._smoothing_previousPoint, this._smoothing_temp); 253 | this._smoothing_temp.scaleInPlace(0.5); 254 | 255 | // Temp now holds the half vector (previous -> current) 256 | // We add back to the previous point 257 | this._smoothing_temp.addInPlace(this._smoothing_previousPoint); 258 | 259 | // To find our mid point 260 | const midPointX = this._smoothing_temp.x; 261 | const midPointY = this._smoothing_temp.y; 262 | 263 | // Which is the one we visually add 264 | this._addPoint(midPointX, midPointY); 265 | } 266 | 267 | private _addPointsSmoothly(x: number, y: number): void { 268 | const length = this._points.length; 269 | 270 | // Compute the Mid Point between our last inputs and the new one 271 | const x1 = this._points[length - 2]; 272 | const y1 = this._points[length - 1]; 273 | this._smoothing_previousPoint.set(x1, y1); 274 | this._smoothing_newPoint.set(x, y); 275 | this._smoothing_newPoint.subtractToRef(this._smoothing_previousPoint, this._smoothing_temp); 276 | // We extract the distance the pointer did since the previous addition 277 | const distanceFromPreviousPoint = this._smoothing_temp.length(); 278 | this._smoothing_temp.scaleInPlace(0.5); 279 | 280 | // Temp now holds the half vector (previous -> current) 281 | // We add back to the previous point 282 | this._smoothing_temp.addInPlace(this._smoothing_previousPoint); 283 | 284 | // To find our mid point 285 | const midPointX = this._smoothing_temp.x; 286 | const midPointY = this._smoothing_temp.y; 287 | 288 | // We compute how many steps we should introduce depending on our 289 | // smoothing distance. 290 | // An angle would be more relevant than a distance here but it looks ok so... 291 | const steps = Math.ceil(distanceFromPreviousPoint / this._smoothingDistance); 292 | if (steps > 1) { 293 | const previousX = this._previousPoint.x; 294 | const previousY = this._previousPoint.y; 295 | for (let step = 1; step < steps; step++) { 296 | const howFar = step / steps; 297 | const smoothX = this._quadraticBezierEquation(howFar, previousX, x1, midPointX); 298 | const smoothY = this._quadraticBezierEquation(howFar, previousY, y1, midPointY); 299 | 300 | // We use a quadratic bezier between the previous point and the new coordinates 301 | // with a control points being the mid vector 302 | this._addPoint(smoothX, smoothY); 303 | } 304 | } 305 | 306 | // Finally we record the point 307 | this._addPoint(midPointX, midPointY); 308 | } 309 | 310 | //_______________ SMOOTHING END _____________ 311 | //_______________ GEOMETRY ______________ 312 | 313 | private _addPoint(x: number, y: number): void { 314 | // Checks and expands buffer accordingly. 315 | if (this._shouldExpand(1)) { 316 | this._expandBuffers(); 317 | } 318 | 319 | // Current point setup 320 | this._currentPoint.x = x; 321 | this._currentPoint.y = y; 322 | 323 | // We are just starting 324 | if (this._nextVertexIndex === 0) { 325 | // Draw a circle 326 | this._startPath(); 327 | } 328 | else if (this._nextVertexIndex === 1) { 329 | // Draw a double caped segment 330 | this._firstSegment(); 331 | } 332 | else { 333 | // Draw a single caped segment linked to the previous 334 | // Segment 335 | this._addSegment(); 336 | } 337 | 338 | // Record the points we are adding. 339 | this._previousPoint.x = x; 340 | this._previousPoint.y = y; 341 | } 342 | 343 | ////// Start /////// 344 | 345 | private _startPath(): void { 346 | const { x, y } = this._currentPoint; 347 | const distance = 0; 348 | 349 | // Add Center. 350 | const centerIndex = this._pushVertexData(x, y, 0, distance); 351 | 352 | // Add Contour for a full circle. 353 | for (let i: number = 0; i < this._roundness; i++) { 354 | const alpha = i * this._roundnessSliceAlpha; 355 | const xSlice = x + Math.cos(alpha) * this._radius; 356 | const ySlice = y + Math.sin(alpha) * this._radius; 357 | 358 | // Add each point on the contour. 359 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, distance); 360 | 361 | if (i == this._roundness - 1) { 362 | this._pushTriangleData(centerIndex, centerIndex + 1, contourIndex); 363 | } 364 | else { 365 | this._pushTriangleData(centerIndex, contourIndex + 1, contourIndex); 366 | } 367 | } 368 | 369 | // Reset to first point only as we need to recreate only half a cap 370 | // Oriented in the next direction 371 | this._nextTriangleIndex = 0; 372 | this._nextVertexIndex = 1; 373 | } 374 | 375 | ////// First Segment /////// 376 | 377 | // The first segment is a bit different as it requires caps on both ends 378 | private _firstSegment(): void { 379 | this._currentPoint.subtractToRef(this._previousPoint, this._translation); 380 | const currentSegmentDistance = this._translation.length(); 381 | this._translation.normalize(); 382 | this._thicknessDirection.set(-this._translation.y, this._translation.x); 383 | this._thicknessDirection.scaleToRef(this._radius, this._thicknessDirectionScaled); 384 | 385 | this._previousPoint.subtractToRef(this._thicknessDirectionScaled, this._p1); 386 | this._currentPoint.subtractToRef(this._thicknessDirectionScaled, this._p2); 387 | this._currentPoint.addToRef(this._thicknessDirectionScaled, this._p3); 388 | this._previousPoint.addToRef(this._thicknessDirectionScaled, this._p4); 389 | 390 | // Create the quad for the segment. 391 | const totalDistance = this._previous_length + currentSegmentDistance; 392 | const p1Index = this._pushVertexData(this._p1.x, this._p1.y, 0, this._previous_length); 393 | const p2Index = this._pushVertexData(this._p2.x, this._p2.y, 0, totalDistance); 394 | const p3Index = this._pushVertexData(this._p3.x, this._p3.y, 0, totalDistance); 395 | const p4Index = this._pushVertexData(this._p4.x, this._p4.y, 0, this._previous_length); 396 | 397 | this._pushTriangleData(p1Index, p3Index, p2Index); 398 | this._pushTriangleData(p3Index, p1Index, p4Index); 399 | 400 | let nextSegmentTriangleIndex = this._nextTriangleIndex; 401 | 402 | // Add Start Cap. 403 | for (let i: number = 1; i < this._roundness; i++) { 404 | // Only half of a circle. 405 | const alpha = i * this._roundnessSliceAlpha; 406 | if (alpha >= Math.PI) { 407 | nextSegmentTriangleIndex = this._nextTriangleIndex + 1; 408 | this._pushTriangleData(0, p1Index, this._nextVertexIndex - 1); 409 | break; 410 | } 411 | 412 | // 2D rotation 413 | const xSlice = this._previousPoint.x + (this._thicknessDirectionScaled.x * Math.cos(alpha) - this._thicknessDirectionScaled.y * Math.sin(alpha)); 414 | const ySlice = this._previousPoint.y + (this._thicknessDirectionScaled.x * Math.sin(alpha) + this._thicknessDirectionScaled.y * Math.cos(alpha)); 415 | 416 | // Add each point on the contour. 417 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, this._previous_length); 418 | 419 | this._pushTriangleData(0, contourIndex, contourIndex - 1); 420 | } 421 | 422 | const centerIndex = this._pushVertexData(this._currentPoint.x, this._currentPoint.y, 0, totalDistance); 423 | 424 | // Add End Cap. 425 | for (let i: number = 1; i < this._roundness; i++) { 426 | // Only half of a circle. 427 | const alpha = i * this._roundnessSliceAlpha; 428 | if (alpha >= Math.PI) { 429 | this._pushTriangleData(centerIndex, p3Index, this._nextVertexIndex - 1); 430 | break; 431 | } 432 | 433 | // 2D rotation 434 | const xSlice = this._currentPoint.x + (-this._thicknessDirectionScaled.x * Math.cos(alpha) + this._thicknessDirectionScaled.y * Math.sin(alpha)); 435 | const ySlice = this._currentPoint.y + (-this._thicknessDirectionScaled.x * Math.sin(alpha) - this._thicknessDirectionScaled.y * Math.cos(alpha)); 436 | 437 | // Add each point on the contour. 438 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, totalDistance); 439 | if (i == 1) { 440 | this._pushTriangleData(centerIndex, contourIndex, p2Index); 441 | } 442 | else { 443 | this._pushTriangleData(centerIndex, contourIndex, contourIndex - 1); 444 | } 445 | } 446 | 447 | // Reset to first point only as we need to recreate only half a cap 448 | this._nextTriangleIndex = nextSegmentTriangleIndex; 449 | this._nextVertexIndex = centerIndex + 1; 450 | this._previous_length = totalDistance; 451 | 452 | this._previous_translation.copyFrom(this._translation); 453 | this._previous_thicknessDirection.copyFrom(this._thicknessDirection); 454 | this._previous_thicknessDirectionScaled.copyFrom(this._thicknessDirectionScaled); 455 | this._previous_p1.copyFrom(this._p1); 456 | this._previous_p2.copyFrom(this._p2); 457 | this._previous_p3.copyFrom(this._p3); 458 | this._previous_p4.copyFrom(this._p4); 459 | this._previous_p2Index = p2Index; 460 | this._previous_p3Index = p3Index; 461 | } 462 | 463 | ////// Other Segments /////// 464 | 465 | // Add A new segment with end cap and link to the previous segment 466 | private _addSegment(): void { 467 | const previousCenterIndex = this._nextVertexIndex - 1; 468 | 469 | this._currentPoint.subtractToRef(this._previousPoint, this._translation); 470 | const currentSegmentDistance = this._translation.length(); 471 | this._translation.normalize(); 472 | this._thicknessDirection.set(-this._translation.y, this._translation.x); 473 | this._thicknessDirection.scaleToRef(this._radius, this._thicknessDirectionScaled); 474 | 475 | this._previousPoint.subtractToRef(this._thicknessDirectionScaled, this._p1); 476 | this._currentPoint.subtractToRef(this._thicknessDirectionScaled, this._p2); 477 | this._currentPoint.addToRef(this._thicknessDirectionScaled, this._p3); 478 | this._previousPoint.addToRef(this._thicknessDirectionScaled, this._p4); 479 | 480 | const totalDistance = this._previous_length + currentSegmentDistance; 481 | const p1Index = this._pushVertexData(this._p1.x, this._p1.y, 0, this._previous_length); 482 | const p2Index = this._pushVertexData(this._p2.x, this._p2.y, 0, totalDistance); 483 | const p3Index = this._pushVertexData(this._p3.x, this._p3.y, 0, totalDistance); 484 | const p4Index = this._pushVertexData(this._p4.x, this._p4.y, 0, this._previous_length); 485 | 486 | this._pushTriangleData(p1Index, p3Index, p2Index); 487 | this._pushTriangleData(p3Index, p1Index, p4Index); 488 | 489 | this._previous_p2.subtractToRef(this._p1, this._p1previous_p2); 490 | this._p1previous_p2.normalize(); 491 | this._p2.subtractToRef(this._p1, this._p1p2); 492 | this._p1p2.normalize(); 493 | 494 | // Cos angle compute to determing which quadrant the link should be in 495 | const dot_p1previous_p2_p1p2 = Vector2.Dot(this._p1previous_p2, this._p1p2); 496 | const dot_previous_translation_translation = Vector2.Dot(this._previous_translation, this._translation); 497 | 498 | // We are Aligned so either we go forward or backward 499 | if (dot_p1previous_p2_p1p2 == 0) { 500 | // if we go backward we need a full Hemisphere as a link 501 | if (dot_previous_translation_translation < 0) { 502 | // Add Mid Cap. 503 | for (let i: number = 1; i < this._roundness; i++) { 504 | // Only half of a circle. 505 | const alpha = i * this._roundnessSliceAlpha; 506 | if (alpha >= Math.PI) { 507 | this._pushTriangleData(previousCenterIndex, p1Index, this._nextVertexIndex - 1); 508 | break; 509 | } 510 | 511 | // 2D rotation 512 | const xSlice = this._previousPoint.x + (-this._thicknessDirectionScaled.x * Math.cos(alpha + Math.PI) + this._thicknessDirectionScaled.y * Math.sin(alpha + Math.PI)); 513 | const ySlice = this._previousPoint.y + (-this._thicknessDirectionScaled.x * Math.sin(alpha + Math.PI) - this._thicknessDirectionScaled.y * Math.cos(alpha + Math.PI)); 514 | 515 | // Add each point on the contour. 516 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, this._previous_length); 517 | if (i == 1) { 518 | this._pushTriangleData(previousCenterIndex, contourIndex, p4Index); 519 | } 520 | else { 521 | this._pushTriangleData(previousCenterIndex, contourIndex, contourIndex - 1); 522 | } 523 | } 524 | } 525 | // if we go forward that is the easiest 526 | else { 527 | // Extend or do nothing 528 | // Here we simply do nothing 529 | } 530 | } 531 | // We need to add a top link between p4 an the previous p3 point 532 | else if (dot_p1previous_p2_p1p2 > 0) { 533 | // Add Mid Cap. 534 | for (let i: number = 1; i < this._roundness; i++) { 535 | // Only half of a circle. 536 | const alpha = i * this._roundnessSliceAlpha; 537 | if (Math.cos(alpha) <= dot_previous_translation_translation) { 538 | // We are lucky cause if the first angle is enough, this._nextVertexIndex - 1 is actually equal to p4 :-) 539 | // We do not need another if. 540 | this._pushTriangleData(previousCenterIndex, this._previous_p3Index, this._nextVertexIndex - 1); 541 | break; 542 | } 543 | 544 | // 2D rotation 545 | const xSlice = this._previousPoint.x + (-this._thicknessDirectionScaled.x * Math.cos(alpha + Math.PI) + this._thicknessDirectionScaled.y * Math.sin(alpha + Math.PI)); 546 | const ySlice = this._previousPoint.y + (-this._thicknessDirectionScaled.x * Math.sin(alpha + Math.PI) - this._thicknessDirectionScaled.y * Math.cos(alpha + Math.PI)); 547 | 548 | // Add each point on the contour. 549 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, this._previous_length); 550 | 551 | // We are lucky cause if the first angle is enough, contourIndex - 1 is actually equal to p4 :-) 552 | // We do not need another if. 553 | this._pushTriangleData(previousCenterIndex, contourIndex, contourIndex - 1); 554 | } 555 | } 556 | // We need to add a bottom link between p1 an the previous p2 point 557 | else if (dot_p1previous_p2_p1p2 < 0) { 558 | // Add Mid Cap. 559 | for (let i: number = 1; i < this._roundness; i++) { 560 | // Only half of a circle. 561 | const alpha = i * this._roundnessSliceAlpha; 562 | if (Math.cos(alpha) <= dot_previous_translation_translation) { 563 | if (i == 1) { 564 | this._pushTriangleData(previousCenterIndex, p1Index, this._previous_p2Index); 565 | } 566 | else { 567 | this._pushTriangleData(previousCenterIndex, p1Index, this._nextVertexIndex - 1); 568 | } 569 | break; 570 | } 571 | 572 | // 2D rotation 573 | const xSlice = this._previousPoint.x + (-this._previous_thicknessDirectionScaled.x * Math.cos(alpha) + this._previous_thicknessDirectionScaled.y * Math.sin(alpha)); 574 | const ySlice = this._previousPoint.y + (-this._previous_thicknessDirectionScaled.x * Math.sin(alpha) - this._previous_thicknessDirectionScaled.y * Math.cos(alpha)); 575 | 576 | // Add each point on the contour. 577 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, this._previous_length); 578 | if (i == 1) { 579 | this._pushTriangleData(previousCenterIndex, contourIndex, this._previous_p2Index); 580 | } 581 | else { 582 | this._pushTriangleData(previousCenterIndex, contourIndex, contourIndex - 1); 583 | } 584 | } 585 | } 586 | 587 | // At this point we can record where the next segment should start. 588 | let nextSegmentTriangleIndex = this._nextTriangleIndex; 589 | 590 | const centerIndex = this._pushVertexData(this._currentPoint.x, this._currentPoint.y, 0, totalDistance); 591 | 592 | // Add End Cap. 593 | for (let i: number = 1; i < this._roundness; i++) { 594 | // Only half of a circle. 595 | const alpha = i * this._roundnessSliceAlpha; 596 | if (alpha >= Math.PI) { 597 | this._pushTriangleData(centerIndex, p3Index, this._nextVertexIndex - 1); 598 | break; 599 | } 600 | 601 | // 2D rotation 602 | const xSlice = this._currentPoint.x + (-this._thicknessDirectionScaled.x * Math.cos(alpha) + this._thicknessDirectionScaled.y * Math.sin(alpha)); 603 | const ySlice = this._currentPoint.y + (-this._thicknessDirectionScaled.x * Math.sin(alpha) - this._thicknessDirectionScaled.y * Math.cos(alpha)); 604 | 605 | // Add each point on the contour. 606 | const contourIndex = this._pushVertexData(xSlice, ySlice, 0, totalDistance); 607 | if (i == 1) { 608 | this._pushTriangleData(centerIndex, contourIndex, p2Index); 609 | } 610 | else { 611 | this._pushTriangleData(centerIndex, contourIndex, contourIndex - 1); 612 | } 613 | } 614 | 615 | // Reset to first point only as we need to recreate only half a cap 616 | this._nextTriangleIndex = nextSegmentTriangleIndex; 617 | this._nextVertexIndex = centerIndex + 1; 618 | this._previous_length = totalDistance; 619 | 620 | this._previous_translation.copyFrom(this._translation); 621 | this._previous_thicknessDirection.copyFrom(this._thicknessDirection); 622 | this._previous_thicknessDirectionScaled.copyFrom(this._thicknessDirectionScaled); 623 | this._previous_p1.copyFrom(this._p1); 624 | this._previous_p2.copyFrom(this._p2); 625 | this._previous_p3.copyFrom(this._p3); 626 | this._previous_p4.copyFrom(this._p4); 627 | this._previous_p2Index = p2Index; 628 | this._previous_p3Index = p3Index; 629 | } 630 | 631 | ////// Utils /////// 632 | 633 | private _pushVertexData(x: number, y: number, z: number, dist: number): number { 634 | const vertexIndex = this._nextVertexIndex; 635 | this._updateVertexData(vertexIndex, x, y, z, dist); 636 | 637 | this._nextVertexIndex++; 638 | 639 | return vertexIndex; 640 | } 641 | 642 | private _updateVertexData(vertexIndex: number, x: number, y: number, z: number, dist: number): void { 643 | const vertexIndexInPositionBuffer = vertexIndex * 3; 644 | this.positions[vertexIndexInPositionBuffer + 0] = x; 645 | this.positions[vertexIndexInPositionBuffer + 1] = y; 646 | this.positions[vertexIndexInPositionBuffer + 2] = z; 647 | this._currentChanges.vertexPositionStart = Math.min(this._currentChanges.vertexPositionStart, vertexIndexInPositionBuffer); 648 | this._currentChanges.vertexPositionEnd = Math.max(this._currentChanges.vertexPositionEnd, vertexIndexInPositionBuffer); 649 | 650 | const vertexIndexInDistanceBuffer = vertexIndex * 1; 651 | this.distances[vertexIndexInDistanceBuffer + 0] = dist; 652 | this._currentChanges.vertexDistanceStart = Math.min(this._currentChanges.vertexDistanceStart, vertexIndexInDistanceBuffer); 653 | this._currentChanges.vertexDistanceEnd = Math.max(this._currentChanges.vertexDistanceEnd, vertexIndexInDistanceBuffer); 654 | } 655 | 656 | private _pushTriangleData(indexA: number, indexB: number, indexC: number): void { 657 | this._updateTriangleData(this._nextTriangleIndex, indexA, indexB, indexC); 658 | this._nextTriangleIndex++; 659 | this.indicesCount = this._nextTriangleIndex * 3; 660 | } 661 | 662 | private _updateTriangleData(triangleIndex: number, indexA: number, indexB: number, indexC: number): void { 663 | const triangleIndexInIndicesBuffer = triangleIndex * 3; 664 | this.indices[triangleIndexInIndicesBuffer + 0] = indexA; 665 | this.indices[triangleIndexInIndicesBuffer + 1] = indexB; 666 | this.indices[triangleIndexInIndicesBuffer + 2] = indexC; 667 | this._currentChanges.indexStart = Math.min(this._currentChanges.indexStart, triangleIndexInIndicesBuffer); 668 | this._currentChanges.indexEnd = Math.max(this._currentChanges.indexEnd, triangleIndexInIndicesBuffer); 669 | } 670 | 671 | private _createBuffers(size: number) { 672 | // 3 floats per position x y z 673 | this.positions = new Float32Array(size * 3); 674 | // 1 float for the distance 675 | this.distances = new Float32Array(size * 1); 676 | // double number of indices for floats 677 | this.indices = new Uint32Array(size * 3 * 2); 678 | } 679 | 680 | private _shouldExpand(newPointsCount): boolean { 681 | const shouldStopAt = this._maxVerticesCount - (this._maxAddedVerticesPerPoint * newPointsCount); 682 | 683 | if (this._nextVertexIndex > shouldStopAt) { 684 | return true; 685 | } 686 | return false; 687 | } 688 | 689 | private _expandBuffers(): void { 690 | const oldPositions = this.positions; 691 | const oldDistances = this.distances; 692 | const oldIndices = this.indices; 693 | 694 | this._maxVerticesCount = this._maxVerticesCount * PathBufferData._VerticesExpansionRate; 695 | this._createBuffers(this._maxVerticesCount); 696 | 697 | this.positions.set(oldPositions); 698 | this.distances.set(oldDistances); 699 | this.indices.set(oldIndices); 700 | } 701 | 702 | private _quadraticBezierEquation(t: number, val0: number, val1: number, val2: number): number { 703 | const result = (1.0 - t) * (1.0 - t) * val0 + 2.0 * t * (1.0 - t) * val1 + t * t * val2; 704 | return result; 705 | }; 706 | } 707 | -------------------------------------------------------------------------------- /src/path/pathMesh.ts: -------------------------------------------------------------------------------- 1 | // Import our Shader Config 2 | import { Mesh } from "@babylonjs/core/Meshes/mesh"; 3 | import { Scene } from "@babylonjs/core/scene"; 4 | import { VertexBuffer } from "@babylonjs/core/Buffers/buffer"; 5 | import { DataBuffer } from "@babylonjs/core/Buffers/dataBuffer"; 6 | import { Observer } from "@babylonjs/core/Misc/observable"; 7 | import { ThinEngine } from "@babylonjs/core/Engines/thinEngine"; 8 | 9 | import "../engineExtensions/engine.bufferSubData"; 10 | 11 | import { PathBufferData, PathBufferDataOptions, PathBufferDataChanges } from "./pathBufferData"; 12 | 13 | 14 | /** 15 | * A path mesh is representing a line of a certain thickness that can be 16 | * constructed gradually. 17 | */ 18 | export class PathMesh extends Mesh { 19 | private readonly _onBeforeRenderObserver: Observer; 20 | private readonly _pathData: PathBufferData; 21 | private readonly _currentChanges: PathBufferDataChanges; 22 | private readonly _engine: ThinEngine; 23 | 24 | private _currentPositionsBufferLength: number; 25 | 26 | private _indicesBuffer: DataBuffer; 27 | private _positionsBuffer: DataBuffer; 28 | private _distancesBuffer: DataBuffer; 29 | 30 | /** 31 | * Instantiates a new path from its starting location. 32 | * @param name The value used by scene.getMeshByName() to do a lookup. 33 | * @param scene The scene to add this mesh to. 34 | * @param startPointX Defines where does the path starts horizontally. 35 | * @param startPointY Defines where does the path starts vertically. 36 | * @param options Defines the path mesh construction options. 37 | */ 38 | constructor(name: string, scene: Scene, startPointX: number, startPointY: number, options?: Partial) { 39 | super(name, scene); 40 | 41 | this._engine = scene.getEngine(); 42 | 43 | this._currentChanges = { 44 | indexStart: 0, 45 | indexEnd: 0, 46 | vertexPositionStart: 0, 47 | vertexPositionEnd: 0, 48 | vertexDistanceStart: 0, 49 | vertexDistanceEnd: 0, 50 | } 51 | this._resetCurrentChanges(); 52 | 53 | this._pathData = new PathBufferData(options); 54 | this._pathData.addPointToPath(startPointX, startPointY); 55 | 56 | this._createGeometry(); 57 | 58 | this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { 59 | this._updateGeometry(); 60 | }); 61 | } 62 | 63 | /** 64 | * Gets the total length of the path by summing all the 65 | * distance between the points that have been added to the path 66 | */ 67 | public get totalLength(): number { 68 | return this._pathData.totalLength; 69 | } 70 | 71 | /** 72 | * Adds a new point to the path 73 | * @param x defines the x coordinates of the point 74 | * @param y defines the y coordinates of the point 75 | */ 76 | public addPointToPath(x: number, y: number): void { 77 | const changes = this._pathData.addPointToPath(x, y); 78 | this._currentChanges.indexStart = Math.min(this._currentChanges.indexStart, changes.indexStart); 79 | this._currentChanges.indexEnd = Math.max(this._currentChanges.indexEnd, changes.indexEnd); 80 | this._currentChanges.vertexPositionStart = Math.min(this._currentChanges.vertexPositionStart, changes.vertexPositionStart); 81 | this._currentChanges.vertexPositionEnd = Math.max(this._currentChanges.vertexPositionEnd, changes.vertexPositionEnd); 82 | this._currentChanges.vertexDistanceStart = Math.min(this._currentChanges.vertexDistanceStart, changes.vertexDistanceStart); 83 | this._currentChanges.vertexDistanceEnd = Math.max(this._currentChanges.vertexDistanceEnd, changes.vertexDistanceEnd); 84 | } 85 | 86 | /** 87 | * Release the resources associated with the mesh 88 | * @param doNotRecurse defines whether or not to recurse and dispose childrens 89 | * @param disposeMaterialAndTextures defines whether or not to dispose associated material and textures 90 | */ 91 | public dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean) { 92 | this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); 93 | 94 | super.dispose(doNotRecurse, disposeMaterialAndTextures); 95 | } 96 | 97 | private _createGeometry(): void { 98 | const indices = this._pathData.indices; 99 | const positions = this._pathData.positions; 100 | const distances = this._pathData.distances; 101 | 102 | // Sets the indices buffer 103 | this.setIndices(indices, null, true); 104 | 105 | // Sets the vertices buffers 106 | this.setVerticesData(VertexBuffer.PositionKind, positions, true, 3); 107 | this.setVerticesData("distance", distances, true, 1); 108 | 109 | this._indicesBuffer = this.geometry.getIndexBuffer(); 110 | this._positionsBuffer = this.geometry.getVertexBuffer(VertexBuffer.PositionKind).getBuffer(); 111 | this._distancesBuffer = this.geometry.getVertexBuffer("distance").getBuffer(); 112 | 113 | // Reset the meaningfull index count 114 | // The buffer are not resized every frame to save GC 115 | // and prevent over allocation on every frame 116 | this.subMeshes[0].indexCount = this._pathData.indicesCount; 117 | 118 | // hold on to the current buffer size 119 | this._currentPositionsBufferLength = positions.length; 120 | } 121 | 122 | private _updateGeometry(): void { 123 | // TODO. Should update bounding boxes. 124 | 125 | const indices = this._pathData.indices; 126 | const positions = this._pathData.positions; 127 | const distances = this._pathData.distances; 128 | 129 | const geometry = this.geometry; 130 | 131 | // Prevent extra copy with the use of gpu only on the next line 132 | geometry._indices = indices; 133 | 134 | if (this._currentPositionsBufferLength !== positions.length) { 135 | // if the buffers have been recreated upload the new buffer to the gpu 136 | geometry.updateIndices(indices, 0, true); 137 | this.setVerticesData(VertexBuffer.PositionKind, positions, true, 3); 138 | this.setVerticesData("distance", distances, true, 1); 139 | 140 | this._indicesBuffer = this.geometry.getIndexBuffer(); 141 | this._positionsBuffer = this.geometry.getVertexBuffer(VertexBuffer.PositionKind).getBuffer(); 142 | this._distancesBuffer = this.geometry.getVertexBuffer("distance").getBuffer(); 143 | } 144 | else { 145 | // if the buffers haven t been recreated update the gpu data only 146 | if (this._currentChanges.indexStart !== Number.MAX_VALUE) { 147 | // Add 3 to handle the end data point 148 | const length = this._currentChanges.indexEnd - this._currentChanges.indexStart + 3; 149 | const offset = this._currentChanges.indexStart; 150 | this._engine.indexBufferSubData(this._indicesBuffer, offset * 4, indices, offset, length); 151 | } 152 | if (this._currentChanges.vertexPositionStart !== Number.MAX_VALUE) { 153 | // Add 3 to handle the end data point 154 | const positionsLength = this._currentChanges.vertexPositionEnd - this._currentChanges.vertexPositionStart + 3; 155 | const positionsOffset = this._currentChanges.vertexPositionStart; 156 | this._engine.vertexBufferSubData(this._positionsBuffer, positionsOffset * 4, positions, positionsOffset, positionsLength); 157 | 158 | // Add 1 to handle the end data point 159 | const distancesLength = this._currentChanges.vertexDistanceEnd - this._currentChanges.vertexDistanceStart + 1; 160 | const distancesOffset = this._currentChanges.vertexDistanceStart; 161 | this._engine.vertexBufferSubData(this._distancesBuffer, distancesOffset * 4, distances, distancesOffset, distancesLength); 162 | } 163 | } 164 | 165 | // Reset the meaningfull index count 166 | // The buffer are not resized every frame to save GC 167 | // and prevent over allocation on every frame 168 | this.subMeshes[0].indexCount = this._pathData.indicesCount; 169 | 170 | // Reset the line index cache to help with wireframe as all the operations 171 | // are intented to be almost gpu exclusive 172 | (this.subMeshes[0])._linesIndexBuffer = null; 173 | 174 | // hold on to the current buffer size 175 | this._currentPositionsBufferLength = positions.length; 176 | 177 | // Reset the changes 178 | this._resetCurrentChanges(); 179 | } 180 | 181 | private _resetCurrentChanges(): void { 182 | this._currentChanges.indexStart = Number.MAX_VALUE; 183 | this._currentChanges.indexEnd = 0; 184 | this._currentChanges.vertexPositionStart = Number.MAX_VALUE; 185 | this._currentChanges.vertexPositionEnd = 0; 186 | this._currentChanges.vertexDistanceStart = Number.MAX_VALUE; 187 | this._currentChanges.vertexDistanceEnd = 0; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esNext", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "types" : [], 8 | "lib": ["dom", "es2015"] 9 | } 10 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | var SRC_DIR = path.resolve(__dirname, "./src"); 4 | var DIST_DIR = path.resolve(__dirname, "./www"); 5 | var DEV_DIR = path.resolve(__dirname, "./.temp"); 6 | 7 | var buildConfig = function(env) { 8 | var isProd = env.prod; 9 | return { 10 | context: __dirname, 11 | entry: { 12 | index: SRC_DIR + "/app.ts" 13 | }, 14 | output: { 15 | path: (isProd ? DIST_DIR : DEV_DIR) + "/scripts/", 16 | filename: "[name].js", 17 | publicPath: "/scripts/", 18 | }, 19 | devtool: isProd ? false : "source-map", 20 | devServer: { 21 | static: ['www'] 22 | }, 23 | resolve: { 24 | extensions: [".ts", ".js"] 25 | }, 26 | module: { 27 | rules: [{ 28 | test: /\.tsx?$/, 29 | loader: "ts-loader", 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: ["style-loader", "css-loader"], 34 | }] 35 | }, 36 | mode: isProd ? "production" : "development" 37 | }; 38 | } 39 | 40 | module.exports = buildConfig; -------------------------------------------------------------------------------- /www/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebavan/BabylonjsInkSample/9ebe01cd8930683b6ca396f6b1305dcd18c353fd/www/assets/favicon.ico -------------------------------------------------------------------------------- /www/assets/logo.svg: -------------------------------------------------------------------------------- 1 | babylonjs_identity_color_dark -------------------------------------------------------------------------------- /www/assets/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebavan/BabylonjsInkSample/9ebe01cd8930683b6ca396f6b1305dcd18c353fd/www/assets/particle.png -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Babylon.js Ink Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 123 | 124 | 125 | 126 |
127 | 132 | 137 |
138 | Loading... 139 |
140 |
141 | 142 |
143 | 157 |
158 | 159 | 160 | 161 | 162 | --------------------------------------------------------------------------------