├── .gitignore ├── screenshots └── screenshot.png ├── src ├── material.js ├── geometry.js ├── scene.js ├── utils.js ├── index.js ├── math.js ├── renderer.js └── wgsl-mode.js ├── package.json ├── LICENSE ├── README.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takahirox/online-wgsl-editor/HEAD/screenshots/screenshot.png -------------------------------------------------------------------------------- /src/material.js: -------------------------------------------------------------------------------- 1 | import {Color} from './math.js'; 2 | 3 | export class Material { 4 | constructor() { 5 | } 6 | } 7 | 8 | export class ShaderMaterial { 9 | constructor(shaderCode) { 10 | this.shaderCode = shaderCode; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wgsl-sandbox", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "servez -p 8080", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/takahirox/wgsl-sandbox.git" 13 | }, 14 | "author": "Takahiro ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/takahirox/wgsl-sandbox/issues" 18 | }, 19 | "homepage": "https://github.com/takahirox/wgsl-sandbox#readme", 20 | "devDependencies": { 21 | "servez": "1.12.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/geometry.js: -------------------------------------------------------------------------------- 1 | export class Geometry { 2 | constructor() { 3 | this._attributes = new Map(); 4 | this._index = null; 5 | } 6 | 7 | setAttribute(key, attribute) { 8 | this._attributes.set(key, attribute); 9 | return this; 10 | } 11 | 12 | getAttribute(key) { 13 | return this._attributes.get(key); 14 | } 15 | 16 | hasAttribute(key) { 17 | return this._attributes.has(key); 18 | } 19 | 20 | setIndex(index) { 21 | this._index = index; 22 | return this; 23 | } 24 | 25 | getIndex() { 26 | return this._index; 27 | } 28 | 29 | hasIndex() { 30 | return this._index !== null; 31 | } 32 | } 33 | 34 | export class Attribute { 35 | constructor(data, itemSize) { 36 | this.data = data; 37 | this.count = this.data.length / itemSize; 38 | } 39 | } 40 | 41 | export class Index { 42 | constructor(data) { 43 | this.data = data; 44 | this.count = this.data.length; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Takahiro 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 | # online-wgsl-editor 2 | 3 | `online-wgsl-editor` is a tiny online [WGSL (WebGPU Shading language)](https://www.w3.org/TR/WGSL/) editor. 4 | 5 | You can test WGSL on your web browser. It is good especially for people who want to learn WGSL. 6 | 7 | ## Demo 8 | 9 | [Online demo](https://takahirox.github.io/online-wgsl-editor/index.html) 10 | 11 | Use Chrome for the Demo. It doesn't run on FireFox and other browsers yet. 12 | 13 | ## Presentation 14 | 15 | [WebGL + WebGPU Meetup - January 2022](https://www.khronos.org/events/webgl-webgpu-meetup-january-2022) / [Slides](https://docs.google.com/presentation/d/1WP5YGAoYvFnj2JYinV7r7SNA8sKngfW2ZNaeyIDiIy8) 16 | 17 | ## Screenshots 18 | 19 | 20 | 21 | ## How to run locally 22 | 23 | ```sh 24 | $ git clone https://github.com/takahirox/online-wgsl-editor.git 25 | $ cd online-wgsl-editor 26 | $ npm install 27 | $ npm run start 28 | # Access http://localhost:8080 on your web browser 29 | ``` 30 | 31 | Note: To run the demo locally, download [Google Chrome Canary](https://www.google.com/chrome/canary/) and enable `#enable-unsafe-webgpu` flag via `chrome://flags`. 32 | 33 | ## Thanks to 34 | 35 | I referred to [Three.js](https://threejs.org/) and [glMatrix](https://glmatrix.net/) for WebGPU and Math. 36 | -------------------------------------------------------------------------------- /src/scene.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | Euler, 4 | Matrix4, 5 | Vector3, 6 | Quaternion 7 | } from './math.js'; 8 | 9 | export class Node { 10 | // @TODO: Rename object to better one? 11 | constructor(object) { 12 | this.position = Vector3.create(); 13 | this.rotation = Euler.create(); 14 | this.scale = Vector3.set(Vector3.create(), 1.0, 1.0, 1.0); 15 | this._quaternion = Quaternion.create(); 16 | this._matrix = Matrix4.create(); 17 | this.children = []; 18 | this.parent = null; 19 | this.object = object; 20 | } 21 | 22 | add(child) { 23 | this.children.push(child); 24 | return this; 25 | } 26 | 27 | setObject(object) { 28 | this.object = object; 29 | return this; 30 | } 31 | 32 | getMatrix() { 33 | return this._matrix; 34 | } 35 | 36 | updateMatrix() { 37 | Quaternion.setFromEuler(this._quaternion, this.rotation); 38 | Matrix4.compose(this._matrix, this.position, this._quaternion, this.scale); 39 | return this; 40 | } 41 | } 42 | 43 | export class Scene { 44 | constructor() { 45 | this.backgroundColor = Color.set(Color.create(), 1.0, 1.0, 1.0); 46 | this.elapsedTime = null; 47 | this._startTime = null; 48 | } 49 | 50 | update() { 51 | const currentTime = performance.now(); 52 | if (this._startTime === null) { 53 | this._startTime = currentTime; 54 | } 55 | this.elapsedTime = currentTime - this._startTime; 56 | } 57 | } 58 | 59 | export class PerspectiveCamera { 60 | constructor(fovy = 1.0, aspect = 1.0, far = 1000.0, near = 0.001) { 61 | this.fovy = fovy; 62 | this.aspect = aspect; 63 | this.far = far; 64 | this.near = near; 65 | this.projectionMatrix = Matrix4.create(); 66 | this.projectionMatrixInverse = Matrix4.create(); 67 | this.updateProjectionMatrix(); 68 | } 69 | 70 | updateProjectionMatrix() { 71 | Matrix4.makePerspective( 72 | this.projectionMatrix, 73 | this.fovy, 74 | this.aspect, 75 | this.near, 76 | this.far 77 | ); 78 | Matrix4.invert(Matrix4.copy(this.projectionMatrixInverse, this.projectionMatrix)); 79 | return this; 80 | } 81 | } 82 | 83 | export class OrthographicCamera { 84 | constructor(left = -1, right = 1, top = 1, bottom = -1, near = 0.0, far = 2000.0) { 85 | this.left = left; 86 | this.right = right; 87 | this.top = top; 88 | this.bottom = bottom; 89 | this.near = near; 90 | this.far = far; 91 | this.projectionMatrix = Matrix4.create(); 92 | this.projectionMatrixInverse = Matrix4.create(); 93 | this.updateProjectionMatrix(); 94 | } 95 | 96 | updateProjectionMatrix() { 97 | Matrix4.makeOrthogonal( 98 | this.projectionMatrix, 99 | this.left, 100 | this.right, 101 | this.bottom, 102 | this.top, 103 | this.near, 104 | this.far 105 | ); 106 | Matrix4.invert(Matrix4.copy(this.projectionMatrixInverse, this.projectionMatrix)); 107 | return this; 108 | } 109 | } 110 | 111 | export class Mesh { 112 | constructor(geometry, material) { 113 | this.geometry = geometry; 114 | this.material = material; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WGSL sandbox 4 | 5 | 6 | 7 | 8 | 9 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 | Geometry 83 | 87 | Camera 88 | 92 | Rotation 93 | 94 |
95 | GitHub 96 | 97 |
98 | 135 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import {Attribute, Geometry, Index} from './geometry.js'; 2 | import {Vector3} from './math.js'; 3 | 4 | export const createPlaneGeometry = ( 5 | width = 1.0, 6 | height = 1.0, 7 | widthSegments = 1, 8 | heightSegments = 1 9 | ) => { 10 | const geometry = new Geometry(); 11 | 12 | // from Three.js PlaneBufferGeometry 13 | 14 | const widthHalf = width * 0.5; 15 | const heightHalf = height * 0.5; 16 | const gridX = widthSegments; 17 | const gridY = heightSegments; 18 | const gridX1 = gridX + 1; 19 | const gridY1 = gridY + 1; 20 | const segmentWidth = width / gridX; 21 | const segmentHeight = height / gridY; 22 | 23 | const positions = []; 24 | const normals = []; 25 | const uvs = []; 26 | const indices = []; 27 | 28 | for (let iy = 0; iy < gridY1; iy++) { 29 | const y = iy * segmentHeight - heightHalf; 30 | for (let ix = 0; ix < gridX1; ix++) { 31 | const x = ix * segmentWidth - widthHalf; 32 | positions.push(x, -y, 0.0); 33 | normals.push(0.0, 0.0, 1.0); 34 | uvs.push(ix / gridX, 1.0 - (iy / gridY)); 35 | } 36 | } 37 | 38 | for (let iy = 0; iy < gridY; iy++) { 39 | for (let ix = 0; ix < gridX; ix++) { 40 | const a = ix + gridX1 * iy; 41 | const b = ix + gridX1 * (iy + 1); 42 | const c = (ix + 1) + gridX1 * (iy + 1); 43 | const d = (ix + 1) + gridX1 * iy; 44 | indices.push(a, b, d); 45 | indices.push(b, c, d); 46 | } 47 | } 48 | 49 | geometry.setAttribute('position', new Attribute(new Float32Array(positions), 3)); 50 | geometry.setAttribute('normal', new Attribute(new Float32Array(normals), 3)); 51 | geometry.setAttribute('uv', new Attribute(new Float32Array(uvs), 2)); 52 | geometry.setIndex(new Index(new Uint16Array(indices))); 53 | return geometry; 54 | }; 55 | 56 | export const createBoxGeometry = ( 57 | width = 1.0, 58 | height = 1.0, 59 | depth = 1.0, 60 | widthSegments = 1.0, 61 | heightSegments = 1.0, 62 | depthSegments = 1.0 63 | ) => { 64 | const geometry = new Geometry(); 65 | 66 | // from Three.js BoxBufferGeometry 67 | 68 | const positions = []; 69 | const normals = []; 70 | const uvs = []; 71 | const indices = []; 72 | const vector = Vector3.create(); 73 | 74 | let numberOfVertices = 0; 75 | 76 | const buildPlane = (u, v, w, udir, vdir, width, height, depth, gridX, gridY) => { 77 | const segmentWidth = width / gridX; 78 | const segmentHeight = height / gridY; 79 | const widthHalf = width * 0.5; 80 | const heightHalf = height * 0.5; 81 | const depthHalf = depth * 0.5; 82 | const gridX1 = gridX + 1; 83 | const gridY1 = gridY + 1; 84 | 85 | let positionCounter = 0; 86 | 87 | for (let iy = 0; iy < gridY1; iy++) { 88 | const y = iy * segmentHeight - heightHalf; 89 | for (let ix = 0; ix < gridX1; ix++) { 90 | const x = ix * segmentWidth - widthHalf; 91 | 92 | vector[u] = x * udir; 93 | vector[v] = y * vdir; 94 | vector[w] = depthHalf; 95 | 96 | positions.push(vector[0], vector[1], vector[2]); 97 | 98 | vector[u] = 0.0; 99 | vector[v] = 0.0; 100 | vector[w] = depth > 0 ? 1.0 : -1.0; 101 | 102 | normals.push(vector[0], vector[1], vector[2]); 103 | 104 | uvs.push(ix / gridX, 1.0 - (iy / gridY)); 105 | 106 | positionCounter += 1; 107 | } 108 | } 109 | 110 | for (let iy = 0; iy < gridY; iy++) { 111 | for (let ix = 0; ix < gridX; ix++) { 112 | const a = numberOfVertices + ix + gridX1 * iy; 113 | const b = numberOfVertices + ix + gridX1 * (iy + 1); 114 | const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1); 115 | const d = numberOfVertices + (ix + 1) + gridX1 * iy; 116 | indices.push(a, b, d); 117 | indices.push(b, c, d); 118 | } 119 | } 120 | 121 | numberOfVertices += positionCounter; 122 | }; 123 | 124 | buildPlane(2, 1, 0, -1.0, -1.0, depth, height, width, depthSegments, heightSegments); 125 | buildPlane(2, 1, 0, 1.0, -1.0, depth, height, -width, depthSegments, heightSegments); 126 | buildPlane(0, 2, 1, -1.0, -1.0, width, depth, height, widthSegments, depthSegments); 127 | buildPlane(0, 2, 1, -1.0, -1.0, width, depth, -height, widthSegments, depthSegments); 128 | buildPlane(0, 1, 2, -1.0, -1.0, width, height, depth, widthSegments, heightSegments); 129 | buildPlane(0, 1, 2, -1.0, -1.0, width, height, -depth, widthSegments, heightSegments); 130 | 131 | geometry.setAttribute('position', new Attribute(new Float32Array(positions), 3)); 132 | geometry.setAttribute('normal', new Attribute(new Float32Array(normals), 3)); 133 | geometry.setAttribute('uv', new Attribute(new Float32Array(uvs), 2)); 134 | geometry.setIndex(new Index(new Uint16Array(indices))); 135 | 136 | return geometry; 137 | }; 138 | 139 | export const toRadians = degrees => { 140 | return degrees * Math.PI / 180.0; 141 | }; 142 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Material, ShaderMaterial} from './material.js'; 2 | import {Color, Euler, Vector3} from './math.js'; 3 | import WGPURenderer from './renderer.js'; 4 | import { 5 | Node, 6 | Mesh, 7 | OrthographicCamera, 8 | PerspectiveCamera, 9 | Scene 10 | } from './scene.js'; 11 | import { 12 | createBoxGeometry, 13 | createPlaneGeometry, 14 | toRadians 15 | } from './utils.js'; 16 | import './wgsl-mode.js'; 17 | 18 | const DOM_ELEMENTS_ID = { 19 | defaultShader: 'defaultShader', 20 | cameraSelect: 'cameraSelect', 21 | compiledStatus: 'compiledStatus', 22 | errorStatus: 'errorStatus', 23 | geometrySelect: 'geometrySelect', 24 | info: 'info', 25 | rotationCheckbox: 'rotationCheckbox', 26 | shaderTextarea: 'shaderTextarea' 27 | }; 28 | 29 | export const GEOMETRY_TYPE = { 30 | plane: 0, 31 | box: 1 32 | }; 33 | 34 | export const CAMERA_TYPE = { 35 | orthographic: 0, 36 | perspective: 1 37 | }; 38 | 39 | const SEGMENT_NUM = 64; 40 | const _tmpRotation = Euler.create(); 41 | 42 | export default class App { 43 | constructor(renderer, canvas) { 44 | this._renderer = renderer; 45 | this._canvas = canvas; 46 | this._autoRotation = false; 47 | 48 | const scene = new Scene(); 49 | Color.set(scene.backgroundColor, 0.0, 0.0, 0.0); 50 | this._sceneNode = new Node(scene); 51 | 52 | this._orthographicCamera = new OrthographicCamera(); 53 | this._perspectiveCamera = new PerspectiveCamera( 54 | toRadians(60), 55 | window.innerWidth / window.innerHeight, 56 | 2000.0, 57 | 0.001 58 | ); 59 | this._cameraNode = new Node(this._orthographicCamera); 60 | Vector3.set(this._cameraNode.position, 0.0, 0.0, 2.0); 61 | this._sceneNode.add(this._cameraNode); 62 | 63 | const mesh = new Mesh( 64 | createPlaneGeometry(2.0, 2.0, SEGMENT_NUM, SEGMENT_NUM), new Material()); 65 | this._meshNode = new Node(mesh); 66 | this._sceneNode.add(this._meshNode); 67 | 68 | this._errorMarks = []; 69 | this._editor = this._createEditor(); 70 | this._setupDomElements(); 71 | } 72 | 73 | static async create() { 74 | const canvas = document.createElement('canvas'); 75 | const context = canvas.getContext('webgpu'); 76 | 77 | if (!context) { 78 | throw new UnsupportedWebGPUError('Your browser does not seem to support WebGPU API'); 79 | } 80 | 81 | canvas.width = window.innerWidth; 82 | canvas.height = window.innerHeight; 83 | document.body.appendChild(canvas); 84 | 85 | const renderer = await WGPURenderer.create(context); 86 | renderer.setPixelRatio(window.devicePixelRatio); 87 | 88 | const app = new App(renderer, canvas); 89 | 90 | await app._updateShader(); 91 | app.resize(); 92 | 93 | window.addEventListener('resize', app.resize.bind(app)); 94 | 95 | return app; 96 | } 97 | 98 | resize() { 99 | const width = window.innerWidth; 100 | const height = window.innerHeight; 101 | const pixelRatio = window.devicePixelRatio; 102 | 103 | this._canvas.width = Math.floor(width * pixelRatio); 104 | this._canvas.height = Math.floor(height * pixelRatio); 105 | this._canvas.style.width = width + 'px'; 106 | this._canvas.style.height = height + 'px'; 107 | 108 | this._perspectiveCamera.aspect = width / height; 109 | this._perspectiveCamera.updateProjectionMatrix(); 110 | 111 | this._renderer.setSize(width, height); 112 | this._render(); 113 | } 114 | 115 | _createEditor() { 116 | const area = document.getElementById(DOM_ELEMENTS_ID.shaderTextarea); 117 | area.value = document.getElementById(DOM_ELEMENTS_ID.defaultShader).innerText.trim(); 118 | 119 | const inputQueue = []; 120 | const editor = CodeMirror.fromTextArea(area, { 121 | lineNumbers: true, 122 | matchBrackets: true, 123 | indentWithTabs: true, 124 | tabSize: 4, 125 | indentUnit: 4, 126 | mode: 'wgsl', 127 | theme: 'ayu-dark' 128 | }); 129 | editor.on('change', () => { 130 | inputQueue.push(0); 131 | setTimeout(() => { 132 | inputQueue.pop(); 133 | if (inputQueue.length === 0) { 134 | this._updateShader(); 135 | } 136 | }, 500); 137 | }); 138 | 139 | return editor; 140 | } 141 | 142 | _setupDomElements() { 143 | const info = document.getElementById(DOM_ELEMENTS_ID.info); 144 | document.addEventListener('mouseenter', () => { 145 | info.style.opacity = 0.8; 146 | }); 147 | document.addEventListener('mouseleave', () => { 148 | info.style.opacity = 0.0; 149 | }); 150 | 151 | const cameraSelect = document.getElementById(DOM_ELEMENTS_ID.cameraSelect); 152 | cameraSelect.addEventListener('change', e => { 153 | const option = e.target.options[e.target.selectedIndex]; 154 | this._switchCamera( 155 | option.value === 'orthographic' 156 | ? CAMERA_TYPE.orthographic : CAMERA_TYPE.perspective 157 | ); 158 | }); 159 | 160 | const geometrySelect = document.getElementById(DOM_ELEMENTS_ID.geometrySelect); 161 | geometrySelect.addEventListener('change', e => { 162 | const option = e.target.options[e.target.selectedIndex]; 163 | this._switchGeometry( 164 | option.value === 'plane' 165 | ? GEOMETRY_TYPE.plane : GEOMETRY_TYPE.box 166 | ); 167 | }); 168 | 169 | const roatationCheckbox = document.getElementById(DOM_ELEMENTS_ID.rotationCheckbox); 170 | rotationCheckbox.addEventListener('change', e => { 171 | this._enableAutoRotation(e.target.checked); 172 | }); 173 | } 174 | 175 | start() { 176 | const run = timestamp => { 177 | requestAnimationFrame(run); 178 | this._animate(); 179 | this._render(); 180 | }; 181 | run(); 182 | } 183 | 184 | _switchCamera(cameraType) { 185 | this._cameraNode.setObject( 186 | cameraType === CAMERA_TYPE.orthographic 187 | ? this._orthographicCamera 188 | : this._perspectiveCamera 189 | ); 190 | } 191 | 192 | _switchGeometry(geometryType) { 193 | const node = this._meshNode 194 | const mesh = node.object; 195 | if (geometryType === GEOMETRY_TYPE.plane) { 196 | mesh.geometry = createPlaneGeometry(2.0, 2.0, SEGMENT_NUM, SEGMENT_NUM); 197 | Vector3.set(node.position, 0.0, 0.0, 0.0); 198 | Euler.set(node.rotation, 0.0, 0.0, 0.0); 199 | } else { 200 | mesh.geometry = createBoxGeometry( 201 | 2.0, 2.0, 2.0, SEGMENT_NUM, SEGMENT_NUM, SEGMENT_NUM); 202 | Vector3.set(node.position, 0.0, 0.0, -2.0); 203 | Euler.set(node.rotation, toRadians(30.0), 0.0, 0.0); 204 | } 205 | } 206 | 207 | _enableAutoRotation(enabled) { 208 | this._autoRotation = enabled; 209 | Euler.setY(this._meshNode.rotation, 0.0); 210 | } 211 | 212 | async _updateShader() { 213 | const shaderCode = this._editor.getValue(); 214 | const material = new ShaderMaterial(shaderCode); 215 | 216 | this._removeCompileErrorHightlights(); 217 | 218 | try { 219 | await this._renderer.compile(material); 220 | } catch (error) { 221 | this._updateStatusElement(false); 222 | this._highlightCompileErrors(error.messages); 223 | throw error; 224 | } 225 | 226 | this._updateStatusElement(true); 227 | this._meshNode.object.material = material; 228 | } 229 | 230 | _updateStatusElement(succeeded) { 231 | document.getElementById(DOM_ELEMENTS_ID.compiledStatus).style.display = succeeded ? '' : 'none'; 232 | document.getElementById(DOM_ELEMENTS_ID.errorStatus).style.display = !succeeded ? '' : 'none'; 233 | } 234 | 235 | _removeCompileErrorHightlights() { 236 | this._errorMarks.forEach(mark => { 237 | mark.clear(); 238 | }); 239 | this._errorMarks.length = 0; 240 | } 241 | 242 | _highlightCompileErrors(messages) { 243 | for (let index = 0; index < messages.length; index++) { 244 | const message = messages[index]; 245 | 246 | this._errorMarks.push( 247 | this._editor.markText( 248 | { 249 | line: message.lineNum - 1, 250 | ch: message.linePos - 1 251 | }, 252 | { 253 | line: message.lineNum - 1, 254 | ch: message.linePos - 1 + message.length 255 | }, 256 | { 257 | className: 'errorMark', 258 | attributes: {title: message.message} 259 | } 260 | ) 261 | ); 262 | } 263 | } 264 | 265 | _animate() { 266 | this._sceneNode.object.update(); 267 | if (this._autoRotation) { 268 | Euler.add(this._meshNode.rotation, Euler.set(_tmpRotation, 0.0, 0.005, 0.0)); 269 | } 270 | } 271 | 272 | _render() { 273 | this._renderer.render(this._sceneNode, this._cameraNode); 274 | } 275 | } 276 | 277 | export class UnsupportedWebGPUError extends Error { 278 | constructor(...params) { 279 | super(...params); 280 | } 281 | } -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | export class Color { 2 | static create() { 3 | return new Float32Array(3); 4 | } 5 | 6 | static set(m, r, g, b) { 7 | m[0] = r; 8 | m[1] = g; 9 | m[2] = b; 10 | return m; 11 | } 12 | } 13 | 14 | export class Vector3 { 15 | static create() { 16 | return new Float32Array(3); 17 | } 18 | 19 | static set(v, x, y, z) { 20 | v[0] = x; 21 | v[1] = y; 22 | v[2] = z; 23 | return v; 24 | } 25 | 26 | static setX(v, value) { 27 | v[0] = value; 28 | return v; 29 | } 30 | 31 | static setY(v, value) { 32 | v[1] = value; 33 | return v; 34 | } 35 | 36 | static setZ(v, value) { 37 | v[2] = value; 38 | return v; 39 | } 40 | 41 | static copy(v, src) { 42 | for (let i = 0; i < 3; i++) { 43 | v[i] = src[i]; 44 | } 45 | return v; 46 | } 47 | 48 | static length(v) { 49 | return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); 50 | } 51 | 52 | static normalize(v) { 53 | const length = Vector3.length(v); 54 | 55 | // @TODO: Error handling? 56 | if (length !== 0.0) { 57 | for (let i = 0; i < 3; i++) { 58 | v[i] /= length; 59 | } 60 | } 61 | 62 | return v; 63 | } 64 | } 65 | 66 | // XYZ order 67 | export class Euler { 68 | static create() { 69 | return new Float32Array(3); 70 | } 71 | 72 | static set(e, x, y, z) { 73 | e[0] = x; 74 | e[1] = y; 75 | e[2] = z; 76 | return e; 77 | } 78 | 79 | static setX(e, value) { 80 | e[0] = value; 81 | return e; 82 | } 83 | 84 | static setY(e, value) { 85 | e[1] = value; 86 | return e; 87 | } 88 | 89 | static setZ(e, value) { 90 | e[2] = value; 91 | return e; 92 | } 93 | 94 | static add(e, e2) { 95 | for (let i = 0; i < 3; i++) { 96 | e[i] += e2[i]; 97 | } 98 | return e; 99 | } 100 | } 101 | 102 | export class Quaternion { 103 | static create() { 104 | return Quaternion.set(new Float32Array(4), 0.0, 0.0, 0.0, 1.0); 105 | } 106 | 107 | static set(q, x, y, z, w) { 108 | q[0] = x; 109 | q[1] = y; 110 | q[2] = z; 111 | q[3] = w; 112 | return q; 113 | } 114 | 115 | static setFromEuler(q, e) { 116 | // Assume XYZ order 117 | const x = e[0]; 118 | const y = e[1]; 119 | const z = e[2]; 120 | 121 | const c1 = Math.cos(x / 2.0); 122 | const c2 = Math.cos(y / 2.0); 123 | const c3 = Math.cos(z / 2.0); 124 | 125 | const s1 = Math.sin(x / 2.0); 126 | const s2 = Math.sin(y / 2.0); 127 | const s3 = Math.sin(z / 2.0); 128 | 129 | q[0] = s1 * c2 * c3 + c1 * s2 * s3; 130 | q[1] = c1 * s2 * c3 - s1 * c2 * s3; 131 | q[2] = c1 * c2 * s3 + s1 * s2 * c3; 132 | q[3] = c1 * c2 * c3 - s1 * s2 * s3; 133 | 134 | return q; 135 | } 136 | } 137 | 138 | export class Matrix3 { 139 | static create() { 140 | return Matrix3.makeIdentity(new Float32Array(9)); 141 | } 142 | 143 | static makeIdentity(m) { 144 | m[0] = 1.0; 145 | m[1] = 0.0; 146 | m[2] = 0.0; 147 | 148 | m[3] = 0.0; 149 | m[4] = 1.0; 150 | m[5] = 0.0; 151 | 152 | m[6] = 0.0; 153 | m[7] = 0.0; 154 | m[8] = 1.0; 155 | 156 | return m; 157 | } 158 | 159 | static copy(m, src) { 160 | for (let i = 0; i < 9; i++) { 161 | m[i] = src[i]; 162 | } 163 | return m; 164 | } 165 | 166 | static makeNormalFromMatrix4(m, src) { 167 | const a00 = src[0]; 168 | const a01 = src[1]; 169 | const a02 = src[2]; 170 | const a03 = src[3]; 171 | const a10 = src[4]; 172 | const a11 = src[5]; 173 | const a12 = src[6]; 174 | const a13 = src[7]; 175 | const a20 = src[8]; 176 | const a21 = src[9]; 177 | const a22 = src[10]; 178 | const a23 = src[11]; 179 | const a30 = src[12]; 180 | const a31 = src[13]; 181 | const a32 = src[14]; 182 | const a33 = src[15]; 183 | 184 | const b00 = a00 * a11 - a01 * a10; 185 | const b01 = a00 * a12 - a02 * a10; 186 | const b02 = a00 * a13 - a03 * a10; 187 | const b03 = a01 * a12 - a02 * a11; 188 | const b04 = a01 * a13 - a03 * a11; 189 | const b05 = a02 * a13 - a03 * a12; 190 | const b06 = a20 * a31 - a21 * a30; 191 | const b07 = a20 * a32 - a22 * a30; 192 | const b08 = a20 * a33 - a23 * a30; 193 | const b09 = a21 * a32 - a22 * a31; 194 | const b10 = a21 * a33 - a23 * a31; 195 | const b11 = a22 * a33 - a23 * a32; 196 | 197 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 198 | 199 | if (det === 0.0) { 200 | // @TODO: Error handling? 201 | return m; 202 | } 203 | 204 | det = 1.0 / det; 205 | m[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; 206 | m[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det; 207 | m[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det; 208 | m[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det; 209 | m[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det; 210 | m[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det; 211 | m[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det; 212 | m[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det; 213 | m[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det; 214 | 215 | return m; 216 | } 217 | } 218 | 219 | export class Matrix3GPU { 220 | static create() { 221 | return Matrix3GPU.identity(new Float32Array(12)); 222 | } 223 | 224 | static identity(m) { 225 | m[0] = 1.0; 226 | m[1] = 0.0; 227 | m[2] = 0.0; 228 | m[3] = 0.0; 229 | m[4] = 0.0; 230 | m[5] = 1.0; 231 | m[6] = 0.0; 232 | m[7] = 0.0; 233 | m[8] = 0.0; 234 | m[9] = 0.0; 235 | m[10] = 1.0; 236 | m[11] = 0.0; 237 | return m; 238 | } 239 | 240 | static copy(m, src) { 241 | for (let i = 0; i < 12; i++) { 242 | m[i] = src[i]; 243 | } 244 | return m; 245 | } 246 | 247 | static copyFromMatrix3(m, src) { 248 | // @TODO: Use loop? 249 | m[0] = src[0]; 250 | m[1] = src[1]; 251 | m[2] = src[2]; 252 | m[3] = 0.0; 253 | m[4] = src[3]; 254 | m[5] = src[4]; 255 | m[6] = src[5]; 256 | m[7] = 0.0; 257 | m[8] = src[6]; 258 | m[9] = src[7]; 259 | m[10] = src[8]; 260 | m[11] = 0.0; 261 | return m; 262 | } 263 | } 264 | 265 | export class Matrix4 { 266 | static create() { 267 | return Matrix4.makeIdentity(new Float32Array(16)); 268 | } 269 | 270 | static makeIdentity(m) { 271 | m[0] = 1.0; 272 | m[1] = 0.0; 273 | m[2] = 0.0; 274 | m[3] = 0.0; 275 | 276 | m[4] = 0.0; 277 | m[5] = 1.0; 278 | m[6] = 0.0; 279 | m[7] = 0.0; 280 | 281 | m[8] = 0.0; 282 | m[9] = 0.0; 283 | m[10] = 1.0; 284 | m[11] = 0.0; 285 | 286 | m[12] = 0.0; 287 | m[13] = 0.0; 288 | m[14] = 0.0; 289 | m[15] = 1.0; 290 | 291 | return m; 292 | } 293 | 294 | static multiply(m, m1, m2) { 295 | const a00 = m1[0]; 296 | const a01 = m1[1]; 297 | const a02 = m1[2]; 298 | const a03 = m1[3]; 299 | const a10 = m1[4]; 300 | const a11 = m1[5]; 301 | const a12 = m1[6]; 302 | const a13 = m1[7]; 303 | const a20 = m1[8]; 304 | const a21 = m1[9]; 305 | const a22 = m1[10]; 306 | const a23 = m1[11]; 307 | const a30 = m1[12]; 308 | const a31 = m1[13]; 309 | const a32 = m1[14]; 310 | const a33 = m1[15]; 311 | 312 | let b0 = m2[0]; 313 | let b1 = m2[1]; 314 | let b2 = m2[2]; 315 | let b3 = m2[3]; 316 | m[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 317 | m[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 318 | m[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 319 | m[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 320 | 321 | b0 = m2[4]; 322 | b1 = m2[5]; 323 | b2 = m2[6]; 324 | b3 = m2[7]; 325 | m[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 326 | m[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 327 | m[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 328 | m[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 329 | 330 | b0 = m2[8]; 331 | b1 = m2[9]; 332 | b2 = m2[10]; 333 | b3 = m2[11]; 334 | m[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 335 | m[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 336 | m[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 337 | m[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 338 | 339 | b0 = m2[12]; 340 | b1 = m2[13]; 341 | b2 = m2[14]; 342 | b3 = m2[15]; 343 | m[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 344 | m[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 345 | m[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 346 | m[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 347 | 348 | return m; 349 | } 350 | 351 | static copy(m, src) { 352 | for (let i = 0; i < 16; i++) { 353 | m[i] = src[i]; 354 | } 355 | return m; 356 | } 357 | 358 | static invert(m) { 359 | const a00 = m[0]; 360 | const a01 = m[1]; 361 | const a02 = m[2]; 362 | const a03 = m[3]; 363 | const a10 = m[4]; 364 | const a11 = m[5]; 365 | const a12 = m[6]; 366 | const a13 = m[7]; 367 | const a20 = m[8]; 368 | const a21 = m[9]; 369 | const a22 = m[10]; 370 | const a23 = m[11]; 371 | const a30 = m[12]; 372 | const a31 = m[13]; 373 | const a32 = m[14]; 374 | const a33 = m[15]; 375 | 376 | const b00 = a00 * a11 - a01 * a10; 377 | const b01 = a00 * a12 - a02 * a10; 378 | const b02 = a00 * a13 - a03 * a10; 379 | const b03 = a01 * a12 - a02 * a11; 380 | const b04 = a01 * a13 - a03 * a11; 381 | const b05 = a02 * a13 - a03 * a12; 382 | const b06 = a20 * a31 - a21 * a30; 383 | const b07 = a20 * a32 - a22 * a30; 384 | const b08 = a20 * a33 - a23 * a30; 385 | const b09 = a21 * a32 - a22 * a31; 386 | const b10 = a21 * a33 - a23 * a31; 387 | const b11 = a22 * a33 - a23 * a32; 388 | 389 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 390 | 391 | if (det == 0.0) { 392 | // @TODO: Through error? 393 | return m; 394 | } 395 | 396 | det = 1.0 / det; 397 | m[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; 398 | m[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; 399 | m[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; 400 | m[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; 401 | m[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; 402 | m[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; 403 | m[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; 404 | m[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; 405 | m[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; 406 | m[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; 407 | m[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; 408 | m[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; 409 | m[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; 410 | m[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; 411 | m[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; 412 | m[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; 413 | 414 | return m; 415 | } 416 | 417 | static compose(m, position, quaternion, scale) { 418 | const x = quaternion[0]; 419 | const y = quaternion[1]; 420 | const z = quaternion[2]; 421 | const w = quaternion[3]; 422 | 423 | const x2 = x + x; 424 | const y2 = y + y; 425 | const z2 = z + z; 426 | 427 | const xx = x * x2; 428 | const xy = x * y2; 429 | const xz = x * z2; 430 | 431 | const yy = y * y2; 432 | const yz = y * z2; 433 | const zz = z * z2; 434 | 435 | const wx = w * x2; 436 | const wy = w * y2; 437 | const wz = w * z2; 438 | 439 | const sx = scale[0]; 440 | const sy = scale[1]; 441 | const sz = scale[2]; 442 | 443 | m[0] = (1.0 - (yy + zz)) * sx; 444 | m[1] = (xy + wz) * sx; 445 | m[2] = (xz - wy) * sx; 446 | m[3] = 0.0; 447 | 448 | m[4] = (xy - wz) * sy; 449 | m[5] = (1.0 - (xx + zz)) * sy; 450 | m[6] = (yz + wx) * sy; 451 | m[7] = 0.0; 452 | 453 | m[8] = (xz + wy) * sz; 454 | m[9] = (yz - wx) * sz; 455 | m[10] = (1.0 - (xx + yy)) * sz; 456 | m[11] = 0.0; 457 | 458 | m[12] = position[0]; 459 | m[13] = position[1]; 460 | m[14] = position[2]; 461 | m[15] = 1.0; 462 | 463 | return m; 464 | } 465 | 466 | static makePerspective(m, fovy, aspect, near, far) { 467 | const f = 1.0 / Math.tan(fovy / 2.0); 468 | m[0] = f / aspect; 469 | m[1] = 0.0; 470 | m[2] = 0.0; 471 | m[3] = 0.0; 472 | m[4] = 0.0; 473 | m[5] = f; 474 | m[6] = 0.0; 475 | m[7] = 0.0; 476 | m[8] = 0.0; 477 | m[9] = 0.0; 478 | m[11] = -1.0; 479 | m[12] = 0.0; 480 | m[13] = 0.0; 481 | m[15] = 0.0; 482 | 483 | const nf = 1.0 / (near - far); 484 | m[10] = far * nf; 485 | m[14] = far * near * nf; 486 | 487 | return m; 488 | } 489 | 490 | static makeOrthogonal(m, left, right, bottom, top, near, far) { 491 | const lr = 1.0 / (left - right); 492 | const bt = 1.0 / (bottom - top); 493 | const nf = 1.0 / (near - far); 494 | m[0] = -2.0 * lr; 495 | m[1] = 0.0; 496 | m[2] = 0.0; 497 | m[3] = 0.0; 498 | m[4] = 0.0; 499 | m[5] = -2.0 * bt; 500 | m[6] = 0.0; 501 | m[7] = 0.0; 502 | m[8] = 0.0; 503 | m[9] = 0.0; 504 | m[10] = nf; 505 | m[11] = 0.0; 506 | m[12] = (left + right) * lr; 507 | m[13] = (top + bottom) * bt; 508 | m[14] = near * nf; 509 | m[15] = 1.0; 510 | 511 | return m; 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import {ShaderMaterial} from './material.js'; 2 | import { 3 | Color, 4 | Matrix3, 5 | Matrix3GPU, 6 | Matrix4 7 | } from './math.js'; 8 | import { 9 | Mesh, 10 | OrthographicCamera, 11 | PerspectiveCamera, 12 | Scene 13 | } from './scene.js'; 14 | 15 | const DEFAULT_WIDTH = 640.0; 16 | const DEFAULT_HEIGHT = 480.0; 17 | const DEFAULT_PIXEL_RATIO = 1.0; 18 | 19 | const ATTRIBUTE_NAMES = ['position', 'normal', 'uv']; 20 | 21 | class CompileError extends Error { 22 | constructor(messages, ...params) { 23 | super(...params); 24 | this.messages = messages; 25 | } 26 | } 27 | 28 | export default class WGPURenderer { 29 | constructor(adapter, device, context) { 30 | this._adapter = adapter; 31 | this._device = device; 32 | this._width = DEFAULT_WIDTH; 33 | this._height = DEFAULT_HEIGHT; 34 | this._pixelRatio = DEFAULT_PIXEL_RATIO; 35 | this._context = context; 36 | this._swapChain = this._context.configure({ 37 | alphaMode: 'opaque', 38 | device: this._device, 39 | format: 'bgra8unorm', 40 | usage: GPUTextureUsage.RENDER_ATTACHMENT 41 | }); 42 | this._depthBuffer = createDepthBuffer( 43 | this._device, 44 | this._width, 45 | this._height, 46 | this._pixelRatio 47 | ); 48 | this._renderPassDescriptor = { 49 | colorAttachments: [{ 50 | loadOp: 'clear', 51 | clearValue: {r: 0.0, g: 0.0, b: 0.0, a: 1.0}, 52 | storeOp: 'store', 53 | view: null // setup in render() 54 | }], 55 | depthStencilAttachment: { 56 | depthLoadOp: 'clear', 57 | depthClearValue: 1.0, 58 | depthStoreOp: 'store', 59 | stencilLoadOp: 'clear', 60 | stencilClearValue: 0.0, 61 | stencilStoreOp: 'store', 62 | view: null // setup in render() 63 | } 64 | }; 65 | this._renderPipelines = new WGPURenderPipelines(); 66 | this._attributes = new WGPUAttributes(); 67 | this._indices = new WGPUIndices(); 68 | this._bindings = new WGPUBindings(); 69 | } 70 | 71 | static async create(canvas) { 72 | const adapter = await navigator.gpu.requestAdapter(); 73 | 74 | if (adapter === null) { 75 | // @TODD: Error handling 76 | throw new Error(); 77 | } 78 | 79 | const device = await adapter.requestDevice({}); 80 | 81 | return new WGPURenderer(adapter, device, canvas); 82 | } 83 | 84 | setSize(width, height) { 85 | this._width = width; 86 | this._height = height; 87 | this._recreateDepthBuffer(); 88 | } 89 | 90 | setPixelRatio(pixelRatio) { 91 | this._pixelRatio = pixelRatio; 92 | this._recreateDepthBuffer(); 93 | } 94 | 95 | async compile(material) { 96 | if (!(material instanceof ShaderMaterial)) { 97 | // @TODO: Error handling 98 | return; 99 | } 100 | this._bindings.update(this._device, material); 101 | const binding = this._bindings.get(material); 102 | this._renderPipelines.update(this._device, material, binding.layout); 103 | const module = this._renderPipelines.get(material).module; 104 | const log = await module.getLog(); 105 | const errors = log.messages.filter(m => m.type == "error"); 106 | if (errors.length > 0) { 107 | this._bindings.remove(material); 108 | this._renderPipelines.remove(material); 109 | throw new CompileError(errors.slice(), 'Compile error'); 110 | } 111 | } 112 | 113 | render(sceneNode, cameraNode) { 114 | const scene = sceneNode.object; 115 | const camera = cameraNode.object; 116 | 117 | if (!(scene instanceof Scene) || 118 | (!(camera instanceof PerspectiveCamera) && !(camera instanceof OrthographicCamera))) { 119 | // @TODO: Error handling 120 | return; 121 | } 122 | 123 | // No graph but flat yet 124 | sceneNode.children.forEach(child => child.updateMatrix()); 125 | 126 | const encoder = this._device.createCommandEncoder({}); 127 | 128 | const colorAttachment = this._renderPassDescriptor.colorAttachments[0]; 129 | colorAttachment.view = this._context.getCurrentTexture().createView(); 130 | colorAttachment.clearValue.r = scene.backgroundColor[0]; 131 | colorAttachment.clearValue.g = scene.backgroundColor[1]; 132 | colorAttachment.clearValue.b = scene.backgroundColor[2]; 133 | 134 | this._renderPassDescriptor.depthStencilAttachment.view = 135 | this._depthBuffer.createView(); 136 | 137 | const pass = encoder.beginRenderPass(this._renderPassDescriptor); 138 | 139 | sceneNode.children.forEach(node => { 140 | const mesh = node.object; 141 | 142 | if (!(mesh instanceof Mesh)) { 143 | return; 144 | } 145 | 146 | const material = mesh.material; 147 | 148 | this._bindings.update(this._device, material); 149 | this._bindings.upload(this._device, material, node, scene, cameraNode, 150 | camera, this._width, this._height, this._pixelRatio); 151 | const binding = this._bindings.get(material); 152 | pass.setBindGroup(0, binding.group); 153 | 154 | this._renderPipelines.update(this._device, material, binding.layout); 155 | const pipeline = this._renderPipelines.get(material).pipeline.pipeline; 156 | pass.setPipeline(pipeline); 157 | 158 | const geometry = mesh.geometry; 159 | 160 | for (let i = 0; i < ATTRIBUTE_NAMES.length; i++) { 161 | const attributeName = ATTRIBUTE_NAMES[i]; 162 | if (geometry.hasAttribute(attributeName)) { 163 | const attribute = geometry.getAttribute(attributeName); 164 | this._attributes.update(this._device, attribute); 165 | pass.setVertexBuffer(i, this._attributes.get(attribute)); 166 | } 167 | } 168 | 169 | if (geometry.hasIndex()) { 170 | const index = geometry.getIndex(); 171 | this._indices.update(this._device, index); 172 | pass.setIndexBuffer(this._indices.get(index), 'uint16'); 173 | pass.drawIndexed(index.count, 1); 174 | } else { 175 | pass.draw(); 176 | } 177 | }); 178 | 179 | pass.end(); 180 | this._device.queue.submit([encoder.finish()]); 181 | } 182 | 183 | _recreateDepthBuffer() { 184 | this._depthBuffer.destroy(); 185 | this._depthBuffer = createDepthBuffer( 186 | this._device, 187 | this._width, 188 | this._height, 189 | this._pixelRatio 190 | ); 191 | } 192 | } 193 | 194 | const createDepthBuffer = (device, width, height, pixelRatio) => { 195 | return device.createTexture({ 196 | format: 'depth24plus-stencil8', 197 | sampleCount: 1, 198 | size: { 199 | width: Math.floor(width * pixelRatio), 200 | height: Math.floor(height * pixelRatio), 201 | depthOrArrayLayers: 1 202 | }, 203 | usage: GPUTextureUsage.RENDER_ATTACHMENT 204 | }); 205 | }; 206 | 207 | class WGPUShaderModule { 208 | constructor(device, shaderCode) { 209 | this.module = device.createShaderModule({ 210 | code: shaderCode 211 | }); 212 | } 213 | 214 | getLog(device) { 215 | // compilationInfo() has been renamed to getCompilationInfo() 216 | // in the WebGPU spec. Remove compilationInfo() when major 217 | // browsers support getCompilationInfo(). 218 | return ('getCompilationInfo' in this.module) 219 | ? this.module.getCompilationInfo() 220 | : this.module.compilationInfo(); 221 | } 222 | } 223 | 224 | class WGPURenderPipeline { 225 | constructor(device, shaderModule, bindGroupLayout) { 226 | this.pipeline = device.createRenderPipeline({ 227 | depthStencil: { 228 | depthCompare: 'less-equal', 229 | depthWriteEnabled: true, 230 | format: 'depth24plus-stencil8' 231 | }, 232 | fragment: { 233 | entryPoint: 'fs_main', 234 | module: shaderModule, 235 | targets: [{ 236 | // @TODO: Support alpha blend 237 | format: 'bgra8unorm' 238 | }] 239 | }, 240 | layout: device.createPipelineLayout({ 241 | bindGroupLayouts: [bindGroupLayout] 242 | }), 243 | multisample: { 244 | count: 1 245 | }, 246 | primitive: { 247 | topology: 'triangle-list' 248 | }, 249 | vertex: { 250 | buffers: [ 251 | // position 252 | { 253 | arrayStride: 3 * 4, 254 | attributes: [{ 255 | format: 'float32x3', 256 | offset: 0, 257 | shaderLocation: 0 258 | }], 259 | stepMode: 'vertex' 260 | }, 261 | // normal 262 | { 263 | arrayStride: 3 * 4, 264 | attributes: [{ 265 | format: 'float32x3', 266 | offset: 0, 267 | shaderLocation: 1 268 | }], 269 | stepMode: 'vertex' 270 | }, 271 | // uv 272 | { 273 | arrayStride: 2 * 4, 274 | attributes: [{ 275 | format: 'float32x2', 276 | offset: 0, 277 | shaderLocation: 2 278 | }], 279 | stepMode: 'vertex' 280 | } 281 | ], 282 | entryPoint: 'vs_main', 283 | module: shaderModule 284 | } 285 | }); 286 | } 287 | } 288 | 289 | // @TODO: Implement correctly 290 | class WGPURenderPipelines { 291 | constructor(device) { 292 | this._pipelines = new WeakMap(); 293 | } 294 | 295 | get(material) { 296 | return this._pipelines.get(material); 297 | } 298 | 299 | // @TODO: Dispose unused pipelines 300 | update(device, material, bindGroupLayout) { 301 | if (!(material instanceof ShaderMaterial)) { 302 | return; 303 | } 304 | if (this._pipelines.has(material)) { 305 | return; 306 | } 307 | const module = new WGPUShaderModule(device, material.shaderCode); 308 | this._pipelines.set(material, { 309 | module: module, 310 | pipeline: new WGPURenderPipeline(device, module.module, bindGroupLayout), 311 | }); 312 | } 313 | 314 | remove(material) { 315 | this._pipelines.delete(material); 316 | } 317 | } 318 | 319 | class WGPUAttributes { 320 | constructor() { 321 | this._attributes = new WeakMap(); 322 | } 323 | 324 | get(attribute) { 325 | return this._attributes.get(attribute); 326 | } 327 | 328 | update(device, attribute) { 329 | if (this._attributes.has(attribute)) { 330 | return; 331 | } 332 | 333 | this._attributes.set(attribute, createAndInitBuffer( 334 | device, 335 | attribute.data, 336 | GPUBufferUsage.VERTEX 337 | )); 338 | } 339 | } 340 | 341 | class WGPUIndices { 342 | constructor() { 343 | this._indices = new WeakMap(); 344 | } 345 | 346 | get(index) { 347 | return this._indices.get(index); 348 | } 349 | 350 | update(device, index) { 351 | if (this._indices.has(index)) { 352 | return; 353 | } 354 | 355 | this._indices.set(index, createAndInitBuffer( 356 | device, 357 | index.data, 358 | GPUBufferUsage.INDEX 359 | )); 360 | } 361 | } 362 | 363 | const _modelViewMatrix = Matrix4.create(); 364 | const _cameraMatrixInverse = Matrix4.create(); 365 | const _normalMatrix = Matrix3.create(); 366 | const _normalMatrixGPU = Matrix3GPU.create(); 367 | const _resolution = new Float32Array(2); 368 | const _elapsedTime = new Float32Array(1); 369 | 370 | // @TODO: Implement correctly 371 | class WGPUBindings { 372 | constructor() { 373 | this._bindings = new WeakMap(); 374 | } 375 | 376 | get(material) { 377 | return this._bindings.get(material); 378 | } 379 | 380 | // @TODO: Dispose unused groups 381 | update(device, material, node, scene, cameraNode, camera) { 382 | if (this._bindings.has(material)) { 383 | return; 384 | } 385 | const layout = this._createLayout(device); 386 | // model matrix mat4x4 387 | // view matrix mat4x4 388 | // projection matrix mat4x4 389 | // normal matrix: mat3x3 390 | // resolution: vec2 391 | // elapsed time float 392 | // padding 4 bytes to 256 bytes 393 | const buffer = createAndInitBuffer( 394 | device, 395 | new Float32Array(16 + 16 + 16 + 12 + 2 + 1 + 1), 396 | GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 397 | ); 398 | const group = device.createBindGroup({ 399 | entries: [ 400 | { 401 | binding: 0, 402 | resource: { 403 | buffer: buffer 404 | } 405 | } 406 | ], 407 | layout: layout 408 | }); 409 | this._bindings.set(material, { 410 | buffer: buffer, 411 | layout: layout, 412 | group: group 413 | }); 414 | } 415 | 416 | remove(material) { 417 | this._bindings.delete(material); 418 | } 419 | 420 | upload(device, material, node, scene, cameraNode, camera, width, height, pixelRatio) { 421 | Matrix4.copy(_cameraMatrixInverse, cameraNode.getMatrix()); 422 | Matrix4.invert(_cameraMatrixInverse); 423 | Matrix4.multiply(_modelViewMatrix, _cameraMatrixInverse, node.getMatrix()); 424 | Matrix3.makeNormalFromMatrix4(_normalMatrix, _modelViewMatrix); 425 | Matrix3GPU.copyFromMatrix3(_normalMatrixGPU, _normalMatrix); 426 | _resolution[0] = width * pixelRatio; 427 | _resolution[1] = height * pixelRatio; 428 | // in seconds 429 | _elapsedTime[0] = scene.elapsedTime * 0.001; 430 | 431 | // model matrix mat4x4 432 | // view matrix mat4x4 433 | // projection matrix mat4x4 434 | // normal matrix mat3x3 435 | // resolution vec2 436 | // elapsed time float 437 | const binding = this._bindings.get(material); 438 | device.queue.writeBuffer(binding.buffer, 0, node.getMatrix(), 0); 439 | device.queue.writeBuffer(binding.buffer, 64, _cameraMatrixInverse, 0); 440 | device.queue.writeBuffer(binding.buffer, 128, camera.projectionMatrix, 0); 441 | device.queue.writeBuffer(binding.buffer, 192, _normalMatrixGPU, 0); 442 | device.queue.writeBuffer(binding.buffer, 240, _resolution, 0); 443 | device.queue.writeBuffer(binding.buffer, 248, _elapsedTime, 0); 444 | } 445 | 446 | _createLayout(device) { 447 | return device.createBindGroupLayout({ 448 | entries: [ 449 | // model matrix mat4x4 450 | // view matrix mat4x4 451 | // projection matrix mat4x4 452 | // normal matrix mat3x3 453 | // resolution vec2 454 | // elapsed time float 455 | // padding 4 bytes to 256 bytes 456 | { 457 | binding: 0, 458 | buffer: { 459 | type: 'uniform', 460 | hasDynamicOffset: false, 461 | minBindingSize: (16 + 16 + 16 + 12 + 2 + 1 + 1) * 4, 462 | }, 463 | visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT 464 | } 465 | ] 466 | }); 467 | } 468 | } 469 | 470 | const createAndInitBuffer = (device, array, usage) => { 471 | const buffer = device.createBuffer( { 472 | size: array.byteLength, 473 | usage: usage, 474 | mappedAtCreation: true, 475 | }); 476 | 477 | new array.constructor(buffer.getMappedRange()).set(array); 478 | buffer.unmap(); 479 | return buffer; 480 | } 481 | -------------------------------------------------------------------------------- /src/wgsl-mode.js: -------------------------------------------------------------------------------- 1 | // Copied from 2 | // https://github.com/google/tour-of-wgsl/blob/main/assets/third_party/codemirror/wgsl-mode.js 3 | 4 | // Adapted from CodeMirror, copyright (c) by Marijn Haverbeke and others 5 | // Distributed under an MIT license: https://codemirror.net/5/LICENSE 6 | 7 | (function (mod) { 8 | if (typeof exports == 'object' && typeof module == 'object') 9 | // CommonJS 10 | mod(require('codemirror/lib/codemirror')); 11 | else if (typeof define == 'function' && define.amd) 12 | // AMD 13 | define(['codemirror/lib/codemirror'], mod); 14 | // Plain browser env 15 | else mod(CodeMirror); 16 | })(function (CodeMirror) { 17 | 'use strict'; 18 | 19 | let indentStep = 2; 20 | 21 | CodeMirror.defineMode('wgsl', function (config) { 22 | function simpleMode(states) { 23 | ensureState(states, 'start'); 24 | var states_ = {}, 25 | meta = states.languageData || {}, 26 | hasIndentation = false; 27 | for (var state in states) 28 | if (state != meta && states.hasOwnProperty(state)) { 29 | var list = (states_[state] = []), 30 | orig = states[state]; 31 | for (var i = 0; i < orig.length; i++) { 32 | var data = orig[i]; 33 | list.push(new Rule(data, states)); 34 | if (data.indent || data.dedent) hasIndentation = true; 35 | } 36 | } 37 | return { 38 | name: meta.name, 39 | startState() { 40 | return { state: 'start', pending: null, indent: hasIndentation ? [] : null }; 41 | }, 42 | copyState(state) { 43 | var s = { 44 | state: state.state, 45 | pending: state.pending, 46 | indent: state.indent && state.indent.slice(0), 47 | }; 48 | if (state.stack) s.stack = state.stack.slice(0); 49 | return s; 50 | }, 51 | token: tokenFunction(states_), 52 | indent: indentFunction(states_, meta), 53 | languageData: meta, 54 | }; 55 | } 56 | 57 | function ensureState(states, name) { 58 | if (!states.hasOwnProperty(name)) 59 | throw new Error('Undefined state ' + name + ' in simple mode'); 60 | } 61 | 62 | function toRegex(val, caret) { 63 | if (!val) return /(?:)/; 64 | var flags = ''; 65 | if (val instanceof RegExp) { 66 | if (val.ignoreCase) flags += 'i'; 67 | if (val.unicode) flags += 'u'; 68 | val = val.source; 69 | } else { 70 | val = String(val); 71 | } 72 | return new RegExp((caret === false ? '' : '^') + '(?:' + val + ')', flags); 73 | } 74 | 75 | function asToken(val) { 76 | // val is either a string or list of strings. 77 | if (!val) return null; 78 | if (val.apply) return val; 79 | if (typeof val == 'string') return val.replace(/\./g, ' '); 80 | var result = []; 81 | for (var i = 0; i < val.length; i++) result.push(val[i] && val[i].replace(/\./g, ' ')); 82 | return result; 83 | } 84 | 85 | function Rule(data, states) { 86 | if (data.next || data.push) ensureState(states, data.next || data.push); 87 | this.regex = toRegex(data.regex); 88 | this.token = asToken(data.token); 89 | this.data = data; 90 | } 91 | 92 | function tokenFunction(states) { 93 | return function (stream, state) { 94 | if (state.pending) { 95 | var pend = state.pending.shift(); 96 | if (state.pending.length == 0) state.pending = null; 97 | stream.pos += pend.text.length; 98 | return pend.token; 99 | } 100 | 101 | // Work like a pygments state matcher. 102 | // Do the work described by the first rule that matches. 103 | var curState = states[state.state]; 104 | for (var i = 0; i < curState.length; i++) { 105 | var rule = curState[i]; 106 | var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); 107 | if (matches) { 108 | if (rule.data.unreachable) { 109 | throw new Error('Reached unreachable rule: ' + rule.data.unreachable); 110 | } 111 | if (rule.data.next) { 112 | // Transition to the named state. 113 | state.state = rule.data.next; 114 | } else if (rule.data.push) { 115 | // Push current state onto the stack, and transition to the next 116 | // state. 117 | (state.stack || (state.stack = [])).push(state.state); 118 | state.state = rule.data.push; 119 | } else if (rule.data.pop && state.stack && state.stack.length) { 120 | // Pop. 121 | state.state = state.stack.pop(); 122 | } 123 | 124 | if (rule.data.indent) state.indent.push(stream.indentation() + indentStep); 125 | if (rule.data.dedent) state.indent.pop(); 126 | var token = rule.token; 127 | if (token && token.apply) token = token(matches); 128 | if (matches.length > 2 && rule.token && typeof rule.token != 'string') { 129 | state.pending = []; 130 | for (var j = 2; j < matches.length; j++) 131 | if (matches[j]) state.pending.push({ text: matches[j], token: rule.token[j - 1] }); 132 | stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); 133 | return token[0]; 134 | } else if (token && token.join) { 135 | return token[0]; 136 | } else { 137 | return token; 138 | } 139 | } 140 | } 141 | stream.next(); 142 | return null; 143 | }; 144 | } 145 | 146 | function indentFunction(states, meta) { 147 | return function (state, textAfter) { 148 | if ( 149 | state.indent == null || 150 | (meta.dontIndentStates && meta.dontIndentStates.indexOf(state.state) > -1) 151 | ) 152 | return null; 153 | 154 | var pos = state.indent.length - 1, 155 | rules = states[state.state]; 156 | scan: for (;;) { 157 | for (var i = 0; i < rules.length; i++) { 158 | var rule = rules[i]; 159 | if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { 160 | var m = rule.regex.exec(textAfter); 161 | if (m && m[0]) { 162 | pos--; 163 | if (rule.next || rule.push) rules = states[rule.next || rule.push]; 164 | textAfter = textAfter.slice(m[0].length); 165 | continue scan; 166 | } 167 | } 168 | } 169 | break; 170 | } 171 | return pos < 0 ? 0 : state.indent[pos]; 172 | }; 173 | } 174 | 175 | // Returns a list of empty strings, from a possibly multi-line string, 176 | // stripping blanks and line endings. Emulates Perls 'qw' function. 177 | function qw(str) { 178 | var result = []; 179 | var words = str.split(/\r+|\n+| +/); 180 | for (var i = 0; i < words.length; ++i) { 181 | if (words[i].length > 0) { 182 | result.push(words[i]); 183 | } 184 | } 185 | return result; 186 | } 187 | 188 | var keywords = qw(` 189 | alias 190 | break 191 | case 192 | const 193 | const_assert 194 | continue 195 | continuing 196 | default 197 | diagnostic 198 | discard 199 | else 200 | enable 201 | false 202 | fn 203 | for 204 | if 205 | let 206 | loop 207 | override 208 | requires 209 | return 210 | struct 211 | switch 212 | true 213 | var 214 | while 215 | `); 216 | 217 | var reserved = qw(` 218 | NULL 219 | Self 220 | abstract 221 | active 222 | alignas 223 | alignof 224 | as 225 | asm 226 | asm_fragment 227 | async 228 | attribute 229 | auto 230 | await 231 | become 232 | binding_array 233 | cast 234 | catch 235 | class 236 | co_await 237 | co_return 238 | co_yield 239 | coherent 240 | column_major 241 | common 242 | compile 243 | compile_fragment 244 | concept 245 | const_cast 246 | consteval 247 | constexpr 248 | constinit 249 | crate 250 | debugger 251 | decltype 252 | delete 253 | demote 254 | demote_to_helper 255 | do 256 | dynamic_cast 257 | enum 258 | explicit 259 | export 260 | extends 261 | extern 262 | external 263 | fallthrough 264 | filter 265 | final 266 | finally 267 | friend 268 | from 269 | fxgroup 270 | get 271 | goto 272 | groupshared 273 | highp 274 | impl 275 | implements 276 | import 277 | inline 278 | instanceof 279 | interface 280 | layout 281 | lowp 282 | macro 283 | macro_rules 284 | match 285 | mediump 286 | meta 287 | mod 288 | module 289 | move 290 | mut 291 | mutable 292 | namespace 293 | new 294 | nil 295 | noexcept 296 | noinline 297 | nointerpolation 298 | noperspective 299 | null 300 | nullptr 301 | of 302 | operator 303 | package 304 | packoffset 305 | partition 306 | pass 307 | patch 308 | pixelfragment 309 | precise 310 | precision 311 | premerge 312 | priv 313 | protected 314 | pub 315 | public 316 | readonly 317 | ref 318 | regardless 319 | register 320 | reinterpret_cast 321 | require 322 | resource 323 | restrict 324 | self 325 | set 326 | shared 327 | sizeof 328 | smooth 329 | snorm 330 | static 331 | static_assert 332 | static_cast 333 | std 334 | subroutine 335 | super 336 | target 337 | template 338 | this 339 | thread_local 340 | throw 341 | trait 342 | try 343 | type 344 | typedef 345 | typeid 346 | typename 347 | typeof 348 | union 349 | unless 350 | unorm 351 | unsafe 352 | unsized 353 | use 354 | using 355 | varying 356 | virtual 357 | volatile 358 | wgsl 359 | where 360 | with 361 | writeonly 362 | yield 363 | `); 364 | 365 | var predeclared_enums = qw(` 366 | read write read_write 367 | function private workgroup uniform storage 368 | perspective linear flat 369 | center centroid sample 370 | vertex_index instance_index position front_facing frag_depth 371 | local_invocation_id local_invocation_index 372 | global_invocation_id workgroup_id num_workgroups 373 | sample_index sample_mask 374 | rgba8unorm 375 | rgba8snorm 376 | rgba8uint 377 | rgba8sint 378 | rgba16uint 379 | rgba16sint 380 | rgba16float 381 | r32uint 382 | r32sint 383 | r32float 384 | rg32uint 385 | rg32sint 386 | rg32float 387 | rgba32uint 388 | rgba32sint 389 | rgba32float 390 | bgra8unorm 391 | `); 392 | 393 | var predeclared_types = qw(` 394 | bool 395 | f16 396 | f32 397 | i32 398 | sampler sampler_comparison 399 | texture_depth_2d 400 | texture_depth_2d_array 401 | texture_depth_cube 402 | texture_depth_cube_array 403 | texture_depth_multisampled_2d 404 | texture_external 405 | texture_external 406 | u32 407 | `); 408 | 409 | var predeclared_type_generators = qw(` 410 | array 411 | atomic 412 | mat2x2 413 | mat2x3 414 | mat2x4 415 | mat3x2 416 | mat3x3 417 | mat3x4 418 | mat4x2 419 | mat4x3 420 | mat4x4 421 | ptr 422 | texture_1d 423 | texture_2d 424 | texture_2d_array 425 | texture_3d 426 | texture_cube 427 | texture_cube_array 428 | texture_multisampled_2d 429 | texture_storage_1d 430 | texture_storage_2d 431 | texture_storage_2d_array 432 | texture_storage_3d 433 | vec2 434 | vec3 435 | vec4 436 | `); 437 | 438 | var predeclared_type_aliases = qw(` 439 | vec2i vec3i vec4i 440 | vec2u vec3u vec4u 441 | vec2f vec3f vec4f 442 | vec2h vec3h vec4h 443 | mat2x2f mat2x3f mat2x4f 444 | mat3x2f mat3x3f mat3x4f 445 | mat4x2f mat4x3f mat4x4f 446 | mat2x2h mat2x3h mat2x4h 447 | mat3x2h mat3x3h mat3x4h 448 | mat4x2h mat4x3h mat4x4h 449 | `); 450 | 451 | var predeclared_intrinsics = qw(` 452 | bitcast all any select arrayLength abs acos acosh asin asinh atan atanh atan2 453 | ceil clamp cos cosh countLeadingZeros countOneBits countTrailingZeros cross 454 | degrees determinant distance dot exp exp2 extractBits faceForward firstLeadingBit 455 | firstTrailingBit floor fma fract frexp inverseBits inverseSqrt ldexp length 456 | log log2 max min mix modf normalize pow quantizeToF16 radians reflect refract 457 | reverseBits round saturate sign sin sinh smoothstep sqrt step tan tanh transpose 458 | trunc dpdx dpdxCoarse dpdxFine dpdy dpdyCoarse dpdyFine fwidth fwidthCoarse fwidthFine 459 | textureDimensions textureGather textureGatherCompare textureLoad textureNumLayers 460 | textureNumLevels textureNumSamples textureSample textureSampleBias textureSampleCompare 461 | textureSampleCompareLevel textureSampleGrad textureSampleLevel textureSampleBaseClampToEdge 462 | textureStore atomicLoad atomicStore atomicAdd atomicSub atomicMax atomicMin 463 | atomicAnd atomicOr atomicXor atomicExchange atomicCompareExchangeWeak pack4x8snorm 464 | pack4x8unorm pack2x16snorm pack2x16unorm pack2x16float unpack4x8snorm unpack4x8unorm 465 | unpack2x16snorm unpack2x16unorm unpack2x16float storageBarrier workgroupBarrier 466 | workgroupUniformLoad 467 | `); 468 | 469 | // Treat '_' and regular identifiers the same. 470 | var ident_like = /^[_\p{XID_Start}]\p{XID_Continue}*/u; 471 | 472 | // Converts a list of strings to a regexp to match any word in that list. 473 | function list2re(strings) { 474 | return new RegExp('^(?:' + strings.join('|') + ')\\b'); 475 | } 476 | 477 | var comment_rules = [ 478 | { regex: /^\s+/, token: 'space' }, 479 | { regex: /^\/\/.*/, token: 'comment' }, 480 | { regex: /^\/\*/, token: 'comment', push: 'blockComment' }, 481 | ]; 482 | 483 | const wgsl_states = { 484 | languageData: { 485 | name: 'wgsl', 486 | dontIndentStates: ['blockComment'], 487 | }, 488 | start: [ 489 | ...comment_rules, 490 | { regex: /^@/, token: 'meta', push: 'attribute' }, 491 | 492 | // Keywords 493 | { regex: /^(true|false)\b/, token: 'atom' }, // Should this be builtin? 494 | { regex: list2re(keywords), token: 'keyword' }, 495 | { regex: list2re(reserved), token: 'keyword' }, 496 | 497 | // Intrinsic functions 498 | { regex: list2re(predeclared_intrinsics), token: 'intrinsic' }, 499 | 500 | // Predeclared names. 501 | { regex: list2re(predeclared_enums), token: 'variable-3' }, 502 | { regex: list2re(predeclared_types), token: 'variable-2' }, 503 | { regex: list2re(predeclared_type_generators), token: 'variable-2' }, 504 | { regex: list2re(predeclared_type_aliases), token: 'variable-2' }, 505 | 506 | // Decimal float literals 507 | // https://www.w3.org/TR/WGSL/#syntax-decimal_float_literal 508 | // 0, with type-specifying suffix. 509 | { regex: /^0[fh]/, token: 'number' }, 510 | // Other decimal integer, with type-specifying suffix. 511 | { regex: /^[1-9][0-9]*[fh]/, token: 'number' }, 512 | // Has decimal point, at least one digit after decimal. 513 | { regex: /^[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?[fh]?/, token: 'number' }, 514 | // Has decimal point, at least one digit before decimal. 515 | { regex: /^[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?[fh]?/, token: 'number' }, 516 | // Has at least one digit, and has an exponent. 517 | { regex: /^[0-9]+[eE][+-]?[0-9]+[fh]?/, token: 'number' }, 518 | 519 | // Hex float literals 520 | // https://www.w3.org/TR/WGSL/#syntax-hex_float_literal 521 | { regex: /^0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+(?:[pP][+-]?[0-9]+[fh]?)?/, token: 'number' }, 522 | { regex: /^0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*(?:[pP][+-]?[0-9]+[fh]?)?/, token: 'number' }, 523 | { regex: /^0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+[fh]?/, token: 'number' }, 524 | 525 | // Hexadecimal integer literals 526 | // https://www.w3.org/TR/WGSL/#syntax-hex_int_literal 527 | { regex: /^0[xX][0-9a-fA-F]+[iu]?/, token: 'number' }, 528 | 529 | // Decimal integer literals 530 | // https://www.w3.org/TR/WGSL/#syntax-decimal_int_literal 531 | // We need two rules here because 01 is not valid. 532 | { regex: /^[1-9][0-9]*[iu]?\b/, token: 'number' }, 533 | { regex: /^0[iu]?\b/, token: 'number' }, // Must match last 534 | 535 | // Indentation 536 | { regex: /^[\{\[\(]/, token: 'operator', indent: true }, 537 | { regex: /^[\}\]\)]/, token: 'operator', dedent: true }, 538 | 539 | // Operators and Punctuation 540 | { regex: /^[,\.;:]/, token: 'operator' }, 541 | { regex: /^[+\-*/%&|<>^!~=]+/, token: 'operator' }, 542 | 543 | { regex: ident_like, token: 'variable' }, 544 | 545 | // Uncomment the following to help debug problems recognizing tokens 546 | // { regex: /()/, token: 'unreachable', unreachable: 'end of root list' }, 547 | ], 548 | blockComment: [ 549 | // Soak up uninteresting text 550 | { regex: RegExp('^[^*/]+'), token: 'comment' }, 551 | // Recognize the start of a block comment 552 | { regex: RegExp('^/\\*'), token: 'comment', push: 'blockComment' }, 553 | // Recognize the end of a block comment 554 | { regex: RegExp('^\\*/'), token: 'comment', pop: true }, 555 | // Soak up stray * and / 556 | { regex: RegExp('^[*/]'), token: 'comment' }, 557 | ], 558 | attribute: [ 559 | ...comment_rules, 560 | { regex: /^\w+/, token: 'meta', pop: true }, 561 | // Empty match. to pop the stack. 562 | { regex: /^()/, token: 'punctuation', pop: true }, 563 | ], 564 | }; 565 | 566 | return simpleMode(wgsl_states); 567 | }); 568 | 569 | CodeMirror.defineMIME('text/wgsl', 'wgsl'); 570 | }); 571 | --------------------------------------------------------------------------------