├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── engine │ ├── cloth │ │ └── cloth.ts │ ├── core │ │ ├── engine.ts │ │ └── transform.ts │ ├── input │ │ └── input.ts │ ├── math │ │ └── util.ts │ ├── modules.d.ts │ ├── particles │ │ └── particles.ts │ ├── primitives │ │ └── cube.ts │ ├── render │ │ ├── base.ts │ │ ├── camera.ts │ │ ├── deferred-pass.ts │ │ ├── interfaces.ts │ │ ├── pointlight.ts │ │ ├── scene.ts │ │ └── staticmesh.ts │ ├── shaders │ │ ├── cloth.wgsl │ │ ├── deferred.frag.wgsl │ │ ├── gbuffer.frag.wgsl │ │ ├── gbuffer.vert.wgsl │ │ ├── particles.wgsl │ │ └── quad.vert.wgsl │ ├── types.d.ts │ └── ui │ │ └── style │ │ └── index.css ├── index.tsx └── template │ └── index.html ├── tsconfig.json └── webpack.config.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and GH-Page Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Install 19 | run: npm i 20 | 21 | - name: Build 22 | run: npm run build 23 | 24 | - name: Deploy to GH Page 25 | uses: JamesIves/github-pages-deploy-action@4.1.1 26 | with: 27 | branch: gh-pages 28 | folder: build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build/* 3 | src/native/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "jsxBracketSameLine": false, 6 | "jsxSingleQuote": false, 7 | "printWidth": 90, 8 | "quoteProps": "as-needed", 9 | "semi": true, 10 | "singleQuote": true, 11 | "tabWidth": 4, 12 | "trailingComma": "all", 13 | "useTabs": false 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGPU [Demo](https://yuu6883.github.io/WebGPUDemo/) 2 | 3 | https://user-images.githubusercontent.com/38842891/170631244-2958a061-4219-4a2f-945c-0dbb4b614d6f.mp4 4 | 5 | ## Features 6 | * Deferred rendering 7 | * Cloth simulation (spring damper, compute shader) 8 | * Particles (compute shader) 9 | 10 | ## Local Build 11 | This project is built with [Typescript](https://www.typescriptlang.org/) 12 | and compiled using [webpack](https://webpack.js.org/) and [swc](https://swc.rs/). Building the project 13 | requires an installation of [Node.js](https://nodejs.org/en/). 14 | 15 | - Install dependencies: `npm install`. 16 | - Compile the project: `npm run build`. 17 | - Run the project: `npm start` which will start an electron.js window 18 | 19 | **Check out [webgpu-samples](https://github.com/austinEng/webgpu-samples) and the official [documentation/draft](https://gpuweb.github.io/gpuweb/) for basic WebGPU concepts and WGSL syntax.** 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgpu", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack serve --mode=development", 8 | "build": "webpack build --mode=production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/dat.gui": "^0.7.7", 15 | "@types/node": "^17.0.15", 16 | "@types/offscreencanvas": "^2019.6.4", 17 | "@types/react": "^18.0.15", 18 | "@types/react-dom": "^18.0.6", 19 | "@webgpu/types": "^0.1.21", 20 | "css-loader": "^6.6.0", 21 | "dat.gui": "^0.7.9", 22 | "file-loader": "^6.2.0", 23 | "gl-matrix": "^3.4.3", 24 | "html-webpack-plugin": "^5.5.0", 25 | "mobx-react-lite": "^3.4.0", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-tooltip": "^4.2.21", 29 | "stats-js": "^1.0.1", 30 | "style-loader": "^3.3.1", 31 | "swc-loader": "^0.2.3", 32 | "typescript": "^4.7.4", 33 | "url-loader": "^4.1.1", 34 | "webpack": "^5.82.0", 35 | "webpack-cli": "^5.0.2", 36 | "webpack-dev-server": "^4.13.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/engine/cloth/cloth.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyVec3 } from 'gl-matrix'; 2 | import Transform from '../core/transform'; 3 | import { checkDevice, GDevice } from '../render/base'; 4 | import { Renderable, RenderPass } from '../render/interfaces'; 5 | import { GBuffer } from '../render/deferred-pass'; 6 | import ClothWGSL from '../shaders/cloth.wgsl'; 7 | 8 | export interface ClothConstants { 9 | mass: number; 10 | rest_length: number; 11 | springConstant: number; 12 | dampingConstant: number; 13 | floor: number; 14 | gravity: ReadonlyVec3; 15 | wind: ReadonlyVec3; 16 | } 17 | 18 | export const GCloth: { 19 | ready: boolean; 20 | renderPipeline: GPURenderPipeline; 21 | normalDebugPipeline: GPURenderPipeline; 22 | simPipeline: GPUComputePipeline; 23 | triangleGenPipeline: GPUComputePipeline; 24 | normalCalcPipeline: GPUComputePipeline; 25 | updatePipeline: GPUComputePipeline; 26 | dtGroup: GPUBindGroup; 27 | viewGroup: GPUBindGroup; 28 | debugViewGroup: GPUBindGroup; 29 | } = { 30 | ready: false, 31 | renderPipeline: null, 32 | normalDebugPipeline: null, 33 | simPipeline: null, 34 | normalCalcPipeline: null, 35 | triangleGenPipeline: null, 36 | updatePipeline: null, 37 | dtGroup: null, 38 | viewGroup: null, 39 | debugViewGroup: null, 40 | }; 41 | 42 | export default class Cloth implements Renderable { 43 | // vec3 x4 NOTE: vec3 is padded to 16 bytes 44 | static readonly STRIDE = Float32Array.BYTES_PER_ELEMENT * 4 * 4; 45 | 46 | public readonly transform: Transform; 47 | 48 | // s_ for simulation 49 | private readonly s_computeGroup: GPUBindGroup; 50 | private readonly s_uniformGroup: GPUBindGroup; 51 | 52 | // n_ for normal 53 | private readonly n_computeGroup: GPUBindGroup; 54 | private readonly n_uniformGroup: GPUBindGroup; 55 | 56 | // u_ for update 57 | private readonly u_computeGroup: GPUBindGroup; 58 | private readonly u_uniformGroup: GPUBindGroup; 59 | 60 | private readonly particleBuf: GPUBuffer; 61 | private readonly constUB: GPUBuffer; 62 | private readonly dimUB: GPUBuffer; 63 | private readonly vectorUB: GPUBuffer; 64 | 65 | private readonly constants = new Float32Array(5); 66 | private readonly dimension = new Uint32Array(2); 67 | private readonly vectors = new Float32Array(8); 68 | 69 | private readonly ibo: GPUBuffer; 70 | 71 | private readonly uniformIndex: number; 72 | private readonly modelGroup: GPUBindGroup; 73 | private readonly debuModelGroup: GPUBindGroup; 74 | private readonly pass: RenderPass; 75 | 76 | private readonly debugBuf: GPUBuffer; 77 | 78 | private readonly totalIndices: number; 79 | 80 | public static debug = false; 81 | 82 | public static sampleRate = 1; 83 | public static sampleTime = performance.now(); 84 | 85 | private static readonly VertexLayout: GPUVertexBufferLayout = { 86 | arrayStride: Cloth.STRIDE, 87 | attributes: [ 88 | { 89 | // position 90 | shaderLocation: 0, 91 | offset: 0, 92 | format: 'float32x3', 93 | }, 94 | { 95 | // normal 96 | shaderLocation: 1, 97 | offset: Float32Array.BYTES_PER_ELEMENT * 4, // Padded 98 | format: 'float32x3', 99 | }, 100 | { 101 | // uv ((unused)) 102 | shaderLocation: 2, 103 | offset: Float32Array.BYTES_PER_ELEMENT * 8, 104 | format: 'float32x2', 105 | }, 106 | ], 107 | }; 108 | 109 | private static readonly NormalDebugVertexLayout: GPUVertexBufferLayout = { 110 | arrayStride: Cloth.STRIDE / 2, 111 | attributes: [ 112 | { 113 | // position 114 | shaderLocation: 0, 115 | offset: 0, 116 | format: 'float32x3', 117 | }, 118 | { 119 | // normal 120 | shaderLocation: 1, 121 | offset: Float32Array.BYTES_PER_ELEMENT * 4, // Padded 122 | format: 'float32x3', 123 | }, 124 | { 125 | // uv ((unused)) 126 | shaderLocation: 2, 127 | offset: Float32Array.BYTES_PER_ELEMENT * 4, 128 | format: 'float32x2', 129 | }, 130 | ], 131 | }; 132 | 133 | public readonly fixedPoints: { row: number; col: number; x: number; y: number }[] = 134 | []; 135 | 136 | static async initPipeline(camUB: GPUBuffer, dtUB: GPUBuffer) { 137 | checkDevice(); 138 | const device = GDevice.device; 139 | 140 | const shader = device.createShaderModule({ 141 | code: ClothWGSL, 142 | }); 143 | 144 | const tasks: Promise[] = []; 145 | 146 | tasks.push( 147 | device 148 | .createComputePipelineAsync({ 149 | layout: 'auto', 150 | compute: { 151 | module: shader, 152 | entryPoint: 'calc_forces', 153 | }, 154 | }) 155 | .then(p => (GCloth.simPipeline = p)), 156 | ); 157 | 158 | // TODO: move the shader modules 159 | tasks.push( 160 | device 161 | .createRenderPipelineAsync({ 162 | layout: 'auto', 163 | vertex: { 164 | module: GBuffer.basePassVertShader, 165 | entryPoint: 'main', 166 | buffers: [Cloth.VertexLayout], 167 | }, 168 | fragment: { 169 | module: GBuffer.basePassFragShader, 170 | entryPoint: 'main', 171 | targets: [ 172 | // position 173 | { format: 'rgba16float' }, 174 | // normal 175 | { format: 'rgba16float' }, 176 | // albedo 177 | { format: 'bgra8unorm' }, 178 | ], 179 | }, 180 | depthStencil: { 181 | depthWriteEnabled: true, 182 | depthCompare: 'less', 183 | format: 'depth24plus', 184 | }, 185 | primitive: { 186 | // topology: 'line-list', 187 | topology: 'triangle-list', 188 | cullMode: 'none', 189 | }, 190 | }) 191 | .then(p => (GCloth.renderPipeline = p)), 192 | ); 193 | 194 | tasks.push( 195 | device 196 | .createRenderPipelineAsync({ 197 | layout: 'auto', 198 | vertex: { 199 | module: GBuffer.basePassVertShader, 200 | entryPoint: 'main', 201 | buffers: [Cloth.NormalDebugVertexLayout], 202 | }, 203 | fragment: { 204 | module: GBuffer.basePassFragShader, 205 | entryPoint: 'main', 206 | targets: [ 207 | // position 208 | { format: 'rgba16float' }, 209 | // normal 210 | { format: 'rgba16float' }, 211 | // albedo 212 | { format: 'bgra8unorm' }, 213 | ], 214 | }, 215 | depthStencil: { 216 | depthWriteEnabled: true, 217 | depthCompare: 'less', 218 | format: 'depth24plus', 219 | }, 220 | primitive: { 221 | topology: 'line-list', 222 | cullMode: 'none', 223 | }, 224 | }) 225 | .then(p => (GCloth.normalDebugPipeline = p)), 226 | ); 227 | 228 | tasks.push( 229 | device 230 | .createComputePipelineAsync({ 231 | layout: 'auto', 232 | compute: { 233 | module: shader, 234 | entryPoint: 'init_indices', 235 | }, 236 | }) 237 | .then(p => (GCloth.triangleGenPipeline = p)), 238 | ); 239 | 240 | tasks.push( 241 | device 242 | .createComputePipelineAsync({ 243 | layout: 'auto', 244 | compute: { 245 | module: shader, 246 | entryPoint: 'calc_normal', 247 | }, 248 | }) 249 | .then(p => (GCloth.normalCalcPipeline = p)), 250 | ); 251 | 252 | tasks.push( 253 | device 254 | .createComputePipelineAsync({ 255 | layout: 'auto', 256 | compute: { 257 | module: shader, 258 | entryPoint: 'update', 259 | }, 260 | }) 261 | .then(p => (GCloth.updatePipeline = p)), 262 | ); 263 | 264 | await Promise.all(tasks); 265 | 266 | GCloth.viewGroup = device.createBindGroup({ 267 | layout: GCloth.renderPipeline.getBindGroupLayout(1), 268 | entries: [ 269 | { 270 | binding: 0, 271 | resource: { 272 | buffer: camUB, 273 | }, 274 | }, 275 | ], 276 | }); 277 | 278 | GCloth.dtGroup = device.createBindGroup({ 279 | layout: GCloth.updatePipeline.getBindGroupLayout(2), 280 | entries: [ 281 | { 282 | binding: 0, 283 | resource: { 284 | buffer: dtUB, 285 | }, 286 | }, 287 | ], 288 | }); 289 | 290 | GCloth.debugViewGroup = device.createBindGroup({ 291 | layout: GCloth.normalDebugPipeline.getBindGroupLayout(1), 292 | entries: [ 293 | { 294 | binding: 0, 295 | resource: { 296 | buffer: camUB, 297 | }, 298 | }, 299 | ], 300 | }); 301 | 302 | GCloth.ready = true; 303 | Cloth.sampleTime = performance.now(); 304 | } 305 | 306 | constructor( 307 | pass: RenderPass, 308 | width: number, 309 | height: number, 310 | constants: ClothConstants, 311 | ) { 312 | checkDevice(); 313 | this.pass = pass; 314 | const device = GDevice.device; 315 | 316 | this.dimension.set([width, height]); 317 | this.constants.set([ 318 | constants.mass, 319 | constants.rest_length, 320 | constants.springConstant, 321 | constants.dampingConstant, 322 | constants.floor, 323 | ]); 324 | 325 | this.vectors.set(constants.wind, 0); 326 | this.vectors.set(constants.gravity, 4); 327 | 328 | this.particleBuf = device.createBuffer({ 329 | size: Cloth.STRIDE * this.dimension[0] * this.dimension[1], 330 | usage: 331 | GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, 332 | // GPUBufferUsage.COPY_SRC, 333 | mappedAtCreation: true, 334 | }); 335 | 336 | this.debugBuf = device.createBuffer({ 337 | size: Cloth.STRIDE * this.dimension[0] * this.dimension[1], 338 | usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, 339 | }); 340 | 341 | const RLEN = constants.rest_length; 342 | const mapped = this.particleBuf.getMappedRange(); 343 | for (let x = 0; x < this.dimension[0]; x++) { 344 | for (let y = 0; y < this.dimension[1]; y++) { 345 | const offset = Cloth.STRIDE * (y * this.dimension[0] + x); 346 | // Position (set 2 corners to fixed points by setting their position.w to -1) 347 | const fixed = 348 | y === this.dimension[1] - 1 && (!x || x === this.dimension[0] - 1); 349 | 350 | new Float32Array(mapped, offset, 4).set([ 351 | x * RLEN, 352 | y * RLEN, 353 | 0, 354 | fixed ? -1 : 0, 355 | ]); 356 | // Normal 357 | new Float32Array(mapped, offset + 16, 3).set([0, 0, 1]); 358 | 359 | if (fixed) 360 | this.fixedPoints.push({ row: y, col: x, x: x * RLEN, y: y * RLEN }); 361 | } 362 | } 363 | this.particleBuf.unmap(); 364 | 365 | this.constUB = device.createBuffer({ 366 | size: this.constants.byteLength, 367 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 368 | mappedAtCreation: true, 369 | }); 370 | 371 | // Upload constants buffer to gpu uniform 372 | new Float32Array(this.constUB.getMappedRange()).set(this.constants); 373 | this.constUB.unmap(); 374 | 375 | this.dimUB = device.createBuffer({ 376 | size: this.dimension.byteLength, 377 | usage: GPUBufferUsage.UNIFORM, 378 | mappedAtCreation: true, 379 | }); 380 | 381 | // Upload constants buffer to gpu uniform 382 | new Uint32Array(this.dimUB.getMappedRange()).set(this.dimension); 383 | this.dimUB.unmap(); 384 | 385 | this.vectorUB = device.createBuffer({ 386 | size: this.vectors.byteLength, 387 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 388 | mappedAtCreation: true, 389 | }); 390 | 391 | // Upload constants buffer to gpu uniform 392 | new Float32Array(this.vectorUB.getMappedRange()).set(this.vectors); 393 | this.vectorUB.unmap(); 394 | 395 | this.s_computeGroup = device.createBindGroup({ 396 | layout: GCloth.simPipeline.getBindGroupLayout(0), 397 | entries: [ 398 | { 399 | binding: 0, 400 | resource: { buffer: this.particleBuf }, 401 | }, 402 | ], 403 | }); 404 | 405 | this.s_uniformGroup = device.createBindGroup({ 406 | layout: GCloth.simPipeline.getBindGroupLayout(1), 407 | entries: [this.constUB, this.dimUB, this.vectorUB].map((buffer, binding) => ({ 408 | binding, 409 | resource: { buffer }, 410 | })), 411 | }); 412 | 413 | this.n_computeGroup = device.createBindGroup({ 414 | layout: GCloth.normalCalcPipeline.getBindGroupLayout(0), 415 | entries: [ 416 | { 417 | binding: 0, 418 | resource: { buffer: this.particleBuf }, 419 | }, 420 | ], 421 | }); 422 | 423 | this.n_uniformGroup = device.createBindGroup({ 424 | layout: GCloth.normalCalcPipeline.getBindGroupLayout(1), 425 | entries: [ 426 | { 427 | binding: 1, 428 | resource: { buffer: this.dimUB }, 429 | }, 430 | ], 431 | }); 432 | 433 | this.u_computeGroup = device.createBindGroup({ 434 | layout: GCloth.updatePipeline.getBindGroupLayout(0), 435 | entries: [ 436 | { 437 | binding: 0, 438 | resource: { buffer: this.particleBuf }, 439 | }, 440 | ], 441 | }); 442 | 443 | this.u_uniformGroup = device.createBindGroup({ 444 | layout: GCloth.updatePipeline.getBindGroupLayout(1), 445 | entries: [this.constUB, this.dimUB].map((buffer, binding) => ({ 446 | binding, 447 | resource: { buffer }, 448 | })), 449 | }); 450 | 451 | // Generate IBO on gpu because why not 452 | { 453 | // 2 tri per quad, 3 indices per triangle 454 | this.totalIndices = (this.dimension[0] - 1) * (this.dimension[1] - 1) * 2 * 3; 455 | 456 | const idxBufSize = Uint32Array.BYTES_PER_ELEMENT * this.totalIndices; 457 | this.ibo = device.createBuffer({ 458 | size: idxBufSize, 459 | usage: 460 | GPUBufferUsage.INDEX | 461 | GPUBufferUsage.STORAGE | 462 | GPUBufferUsage.COPY_SRC, 463 | }); 464 | 465 | console.log(`Total cloth triangles: ${this.totalIndices / 3}`); 466 | 467 | const indicesGroup0 = device.createBindGroup({ 468 | layout: GCloth.triangleGenPipeline.getBindGroupLayout(0), 469 | entries: [ 470 | { 471 | binding: 0, 472 | resource: { buffer: this.ibo }, 473 | }, 474 | ], 475 | }); 476 | 477 | const indicesGroup1 = device.createBindGroup({ 478 | layout: GCloth.triangleGenPipeline.getBindGroupLayout(1), 479 | entries: [ 480 | { 481 | binding: 1, 482 | resource: { 483 | buffer: this.dimUB, 484 | }, 485 | }, 486 | ], 487 | }); 488 | 489 | const GridX = Math.ceil((this.dimension[0] - 1) / 16); 490 | const GridY = Math.ceil((this.dimension[1] - 1) / 16); 491 | 492 | const cmd = device.createCommandEncoder(); 493 | const indicesPass = cmd.beginComputePass(); 494 | indicesPass.setPipeline(GCloth.triangleGenPipeline); 495 | indicesPass.setBindGroup(0, indicesGroup0); 496 | indicesPass.setBindGroup(1, indicesGroup1); 497 | indicesPass.dispatchWorkgroups(GridX, GridY); 498 | indicesPass.end(); 499 | 500 | // const testBuf = device.createBuffer({ 501 | // size: idxBufSize, 502 | // usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, 503 | // }); 504 | 505 | // cmd.copyBufferToBuffer(this.ibo, 0, testBuf, 0, idxBufSize); 506 | 507 | device.queue.submit([cmd.finish()]); 508 | 509 | // testBuf.mapAsync(GPUBufferUsage.MAP_READ).then(() => { 510 | // const data = new Uint32Array(testBuf.getMappedRange().slice(0)); 511 | // console.log(data); 512 | // testBuf.destroy(); 513 | // }); 514 | } 515 | 516 | const { index, offset, buffer, model } = pass.allocUniform(); 517 | 518 | this.uniformIndex = index; 519 | this.modelGroup = device.createBindGroup({ 520 | layout: GCloth.renderPipeline.getBindGroupLayout(0), 521 | entries: [ 522 | { 523 | binding: 0, 524 | resource: { 525 | buffer, 526 | offset, 527 | size: Float32Array.BYTES_PER_ELEMENT * 16 * 2, 528 | }, 529 | }, 530 | ], 531 | }); 532 | 533 | this.debuModelGroup = device.createBindGroup({ 534 | layout: GCloth.normalDebugPipeline.getBindGroupLayout(0), 535 | entries: [ 536 | { 537 | binding: 0, 538 | resource: { 539 | buffer, 540 | offset, 541 | size: Float32Array.BYTES_PER_ELEMENT * 16 * 2, 542 | }, 543 | }, 544 | ], 545 | }); 546 | 547 | this.transform = new Transform(model); 548 | this.transform.update(); 549 | this.transform.updateInverse(); 550 | } 551 | 552 | setWindSpeed(v: ReadonlyVec3) { 553 | this.vectors.set(v, 0); 554 | GDevice.device.queue.writeBuffer(this.vectorUB, 0, this.vectors, 0, 3); 555 | } 556 | 557 | setFloor(y: number) { 558 | this.constants[4] = y; 559 | GDevice.device.queue.writeBuffer(this.constUB, 0, this.constants); 560 | } 561 | 562 | setFixedPointPosition(row: number, col: number, x: number, y: number) { 563 | const offset = Cloth.STRIDE * (row * this.dimension[0] + col); 564 | GDevice.device.queue.writeBuffer( 565 | this.particleBuf, 566 | offset, 567 | new Float32Array([x, y]), 568 | ); 569 | } 570 | 571 | free() { 572 | this.pass.freeUniformIndex(this.uniformIndex); 573 | this.ibo?.destroy(); 574 | 575 | this.particleBuf.destroy(); 576 | this.constUB.destroy(); 577 | this.dimUB.destroy(); 578 | this.vectorUB.destroy(); 579 | } 580 | 581 | simulate(pass: GPUComputePassEncoder) { 582 | // Shrinked grid 583 | const GridX = Math.ceil(this.dimension[0] / 14); 584 | const GridY = Math.ceil(this.dimension[1] / 14); 585 | 586 | pass.setBindGroup(0, this.s_computeGroup); 587 | pass.setBindGroup(1, this.s_uniformGroup); 588 | 589 | pass.dispatchWorkgroups(GridX, GridY); 590 | } 591 | 592 | update(pass: GPUComputePassEncoder) { 593 | pass.setBindGroup(0, this.u_computeGroup); 594 | pass.setBindGroup(1, this.u_uniformGroup); 595 | // Workgroup size is 256 596 | pass.dispatchWorkgroups(Math.ceil((this.dimension[0] * this.dimension[1]) / 256)); 597 | } 598 | 599 | recalcNormals(pass: GPUComputePassEncoder) { 600 | // Shrinked grid 601 | const GridX = Math.ceil(this.dimension[0] / 14); 602 | const GridY = Math.ceil(this.dimension[1] / 14); 603 | 604 | pass.setBindGroup(0, this.n_computeGroup); 605 | pass.setBindGroup(1, this.n_uniformGroup); 606 | 607 | pass.dispatchWorkgroups(GridX, GridY); 608 | } 609 | 610 | draw(pass: GPURenderPassEncoder) { 611 | pass.setBindGroup(0, this.modelGroup); 612 | pass.setVertexBuffer(0, this.particleBuf); 613 | pass.setIndexBuffer(this.ibo, 'uint32'); 614 | pass.drawIndexed(this.totalIndices); 615 | } 616 | 617 | debug(pass: GPURenderPassEncoder) { 618 | pass.setBindGroup(0, this.debuModelGroup); 619 | pass.setVertexBuffer(0, this.particleBuf); 620 | pass.setIndexBuffer(this.ibo, 'uint32'); 621 | pass.draw(this.dimension[0] * this.dimension[1] * 2); 622 | } 623 | 624 | async postUpdate() { 625 | // const size = this.dimension[0] * this.dimension[1] * Cloth.STRIDE; 626 | // const device = GDevice.device; 627 | // const cmd = device.createCommandEncoder(); 628 | // cmd.copyBufferToBuffer(this.pointBuffer, 0, this.debugBuf, 0, size); 629 | // device.queue.submit([cmd.finish()]); 630 | // const p = (n: Float32Array, d = 2) => [...n].map(_ => _.toFixed(d)).join(', '); 631 | // await this.debugBuf.mapAsync(GPUBufferUsage.MAP_READ); 632 | // const mapped = this.debugBuf.getMappedRange(); 633 | // for (let x = 0; x < this.dimension[0]; x++) { 634 | // for (let y = 0; y < this.dimension[1]; y++) { 635 | // const offset = Cloth.STRIDE * (y * this.dimension[0] + x); 636 | // // Position 637 | // const pos = new Float32Array(mapped, offset, 4); 638 | // // Normal 639 | // const norm = new Float32Array(mapped, offset + 16, 3); 640 | // // Velocity 641 | // const velo = new Float32Array(mapped, offset + 32, 3); 642 | // // Force 643 | // const force = new Float32Array(mapped, offset + 48, 3); 644 | // console.log(`[${x},${y}]: v: ${p(velo)}, f: ${p(force, 8)}`); 645 | // } 646 | // } 647 | // this.debugBuf.unmap(); 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /src/engine/core/engine.ts: -------------------------------------------------------------------------------- 1 | import Renderer from '../render/base'; 2 | import { EngineParam } from '../types'; 3 | 4 | export default class Engine { 5 | public readonly renderer: Renderer; 6 | public readonly params: EngineParam; 7 | 8 | constructor(params: EngineParam) { 9 | this.params = params; 10 | this.renderer = new Renderer(this); 11 | } 12 | 13 | async init() { 14 | await this.renderer.init(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/engine/core/transform.ts: -------------------------------------------------------------------------------- 1 | import { mat4, quat, ReadonlyVec3 } from 'gl-matrix'; 2 | 3 | const inverted = new Float32Array(16); 4 | 5 | export default class Transform { 6 | // prettier-ignore 7 | private readonly buffer = new Float32Array([ 8 | 0, 0, 0, 0, 9 | 0, 0, 0, 1, 10 | 1, 1, 1, 0 11 | ]); 12 | 13 | public readonly model: Float32Array; 14 | 15 | constructor(model: Float32Array) { 16 | this.model = model; 17 | } 18 | 19 | get position() { 20 | return this.buffer.slice(0, 3); 21 | } 22 | 23 | set position(pos: ReadonlyVec3) { 24 | this.buffer[0] = pos[0]; 25 | this.buffer[1] = pos[1]; 26 | this.buffer[2] = pos[2]; 27 | } 28 | 29 | get rotation() { 30 | return this.buffer.slice(4, 8); 31 | } 32 | 33 | set rotation(rot: quat) { 34 | quat.normalize(this.buffer.subarray(4, 8), rot); 35 | } 36 | 37 | get scale() { 38 | return this.buffer.slice(8, 11); 39 | } 40 | 41 | set scale(value: ReadonlyVec3) { 42 | this.buffer[8] = value[0]; 43 | this.buffer[9] = value[1]; 44 | this.buffer[10] = value[2]; 45 | } 46 | 47 | update() { 48 | mat4.fromRotationTranslationScale( 49 | this.model.subarray(0, 16), 50 | this.buffer.subarray(4, 8), 51 | this.buffer.subarray(0, 3), 52 | this.buffer.subarray(8, 11), 53 | ); 54 | } 55 | 56 | updateInverse() { 57 | mat4.invert(inverted, this.model.subarray(0, 16)); 58 | mat4.transpose(this.model.subarray(16, 32), inverted); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/engine/input/input.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../math/util'; 2 | import Camera from '../render/camera'; 3 | 4 | enum InputState { 5 | MOUSE_UP, 6 | MOUSE_DOWN, 7 | } 8 | 9 | enum InputButtons { 10 | MOUSE_LEFT = 0, 11 | MOUSE_MIDDLE = 1, 12 | MOUSE_RIGHT = 2, 13 | } 14 | 15 | export default class CameraRotator { 16 | private leftDown: boolean; 17 | private middleDown: boolean; 18 | private rightDown: boolean; 19 | 20 | private mouseX: number; 21 | private mouseY: number; 22 | 23 | private readonly cam: Camera; 24 | 25 | constructor(camera: Camera) { 26 | this.cam = camera; 27 | 28 | window.addEventListener('contextmenu', e => e.preventDefault()); 29 | window.addEventListener('mousemove', e => this.onMouseMove(e.clientX, e.clientY)); 30 | window.addEventListener('mousedown', e => { 31 | if (e.target != document.body) return; 32 | this.onClick(e.button, InputState.MOUSE_DOWN, e.clientX, e.clientY); 33 | }); 34 | window.addEventListener('mouseup', e => { 35 | if (e.target != document.body) return; 36 | this.onClick(e.button, InputState.MOUSE_UP, e.clientX, e.clientY); 37 | }); 38 | window.addEventListener('wheel', e => this.onMouseWheel(e.deltaY)); 39 | } 40 | 41 | onMouseWheel(delta: number) { 42 | this.cam.targetDistance *= 1 + delta / 1000; 43 | } 44 | 45 | onMouseMove(nx: number, ny: number) { 46 | const MaxDelta = 100; 47 | const dx = clamp(nx - this.mouseX, -MaxDelta, MaxDelta); 48 | const dy = clamp(-(ny - this.mouseY), -MaxDelta, MaxDelta); 49 | 50 | this.mouseX = nx; 51 | this.mouseY = ny; 52 | 53 | if (this.leftDown) { 54 | const rate = 1; 55 | this.cam.azimuth = this.cam.azimuth + dx * rate; 56 | this.cam.incline = clamp(this.cam.incline - dy * rate, -90, 90); 57 | } 58 | 59 | if (this.rightDown) { 60 | const rate = 0.005; 61 | this.cam.distance = clamp(this.cam.distance * (1 - dx * rate), 0.01, 1000); 62 | } 63 | } 64 | 65 | public onClick(button: number, mode: InputState, x: number, y: number) { 66 | if (button == InputButtons.MOUSE_LEFT) { 67 | this.leftDown = mode == InputState.MOUSE_DOWN; 68 | } else if (button == InputButtons.MOUSE_MIDDLE) { 69 | this.middleDown = mode == InputState.MOUSE_DOWN; 70 | } else if (button == InputButtons.MOUSE_RIGHT) { 71 | this.rightDown = mode == InputState.MOUSE_DOWN; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/engine/math/util.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (n: number, min: number, max: number) => 2 | n < min ? min : n > max ? max : n; 3 | 4 | export const normalize_nd = (vec: Float32Array) => { 5 | let v = 0; 6 | for (let i = 0; i < vec.length; i++) { 7 | v += vec[i] * vec[i]; 8 | } 9 | v = Math.sqrt(v); 10 | return vec.map(n => n / v); 11 | }; 12 | -------------------------------------------------------------------------------- /src/engine/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.wgsl' { 2 | const shader: string; 3 | export default shader; 4 | } 5 | -------------------------------------------------------------------------------- /src/engine/particles/particles.ts: -------------------------------------------------------------------------------- 1 | import Transform from '../core/transform'; 2 | import { checkDevice, GDevice } from '../render/base'; 3 | import { Renderable, RenderPass } from '../render/interfaces'; 4 | 5 | import ParticleWGSL from '../shaders/particles.wgsl'; 6 | 7 | export interface ParticleConstants { 8 | max_num: number; 9 | max_spawn_per_frame: number; 10 | } 11 | 12 | export const GParticle: { 13 | ready: boolean; 14 | renderPipeline: GPURenderPipeline; 15 | stagePipeline: GPUComputePipeline; 16 | updatePipeline: GPUComputePipeline; 17 | viewGroup: GPUBindGroup; 18 | dtGroup: GPUBindGroup; 19 | } = { 20 | ready: false, 21 | renderPipeline: null, 22 | stagePipeline: null, 23 | updatePipeline: null, 24 | viewGroup: null, 25 | dtGroup: null, 26 | }; 27 | 28 | const rng = (min: number, max: number) => Math.random() * (max - min) + min; 29 | 30 | export default class Particles implements Renderable { 31 | private readonly pass: RenderPass; 32 | 33 | private readonly uniformIndex: number; 34 | 35 | private readonly modelGroup: GPUBindGroup; 36 | private readonly renderGroup: GPUBindGroup; 37 | 38 | private readonly stageGroup: GPUBindGroup; 39 | private readonly updateGroup: GPUBindGroup; 40 | 41 | private readonly stageUniformGroup: GPUBindGroup; 42 | private readonly updateUniformGroup: GPUBindGroup; 43 | 44 | private readonly stageCameraGroup: GPUBindGroup; 45 | private readonly updateCameraGroup: GPUBindGroup; 46 | 47 | public readonly transform: Transform; 48 | 49 | private readonly particleBuf: GPUBuffer; 50 | private readonly indicesBuf: GPUBuffer; 51 | private readonly stageBuf: GPUBuffer; 52 | 53 | private readonly cpuStageBuf: Float32Array; 54 | 55 | private readonly paramsUB: GPUBuffer; 56 | private readonly sphereBuffer = new ArrayBuffer(8 * 4 + 4 * 4); 57 | 58 | public readonly initPos = new Float32Array([0, 25, 0]); 59 | public readonly initVel = new Float32Array([0, 75, 0]); 60 | public readonly variPos = new Float32Array([25, 25, 25]); 61 | public readonly variVel = new Float32Array([20, 20, 20]); 62 | public readonly lifeSpan = new Float32Array([15000, 1000]); 63 | // Air density, drag, elasticity, friction, 64 | public readonly coeffients = new Float32Array([1, 0.01, 0.5, 0.5]); 65 | public readonly wind = new Float32Array([0, 0, 0]); 66 | 67 | private usedListTop = 0; 68 | private readonly usedIndices: Uint32Array; 69 | private readonly freeIndices: number[] = []; 70 | private readonly particleLifeSpan: Float32Array; 71 | 72 | // vec3 x4 NOTE: vec3 is padded to 16 bytes 73 | static readonly STRIDE = Float32Array.BYTES_PER_ELEMENT * 4 * 2; 74 | 75 | private count = 0; 76 | public radius = 1; 77 | public spawn_rate = 1000; 78 | 79 | private readonly max_num: number; 80 | private readonly max_spawn_per_frame: number; 81 | 82 | public pause = false; 83 | 84 | static async initPipeline(camUB: GPUBuffer, dtUB: GPUBuffer) { 85 | checkDevice(); 86 | const device = GDevice.device; 87 | const shader = device.createShaderModule({ 88 | code: ParticleWGSL, 89 | }); 90 | 91 | GParticle.renderPipeline = await device.createRenderPipelineAsync({ 92 | layout: 'auto', 93 | vertex: { 94 | module: shader, 95 | entryPoint: 'vert_main', 96 | buffers: [], 97 | }, 98 | fragment: { 99 | module: shader, 100 | entryPoint: 'frag_main', 101 | targets: [ 102 | // position 103 | { format: 'rgba16float' }, 104 | // normal 105 | { format: 'rgba16float' }, 106 | // albedo 107 | { format: 'bgra8unorm' }, 108 | ], 109 | }, 110 | depthStencil: { 111 | depthWriteEnabled: true, 112 | depthCompare: 'less', 113 | format: 'depth24plus', 114 | }, 115 | primitive: { 116 | topology: 'triangle-list', 117 | cullMode: 'back', 118 | }, 119 | }); 120 | 121 | GParticle.stagePipeline = await device.createComputePipelineAsync({ 122 | layout: 'auto', 123 | compute: { 124 | module: shader, 125 | entryPoint: 'spawn', 126 | }, 127 | }); 128 | 129 | GParticle.updatePipeline = await device.createComputePipelineAsync({ 130 | layout: 'auto', 131 | compute: { 132 | module: shader, 133 | entryPoint: 'update', 134 | }, 135 | }); 136 | 137 | GParticle.viewGroup = device.createBindGroup({ 138 | layout: GParticle.renderPipeline.getBindGroupLayout(1), 139 | entries: [ 140 | { 141 | binding: 0, 142 | resource: { 143 | buffer: camUB, 144 | }, 145 | }, 146 | ], 147 | }); 148 | 149 | GParticle.dtGroup = device.createBindGroup({ 150 | layout: GParticle.updatePipeline.getBindGroupLayout(3), 151 | entries: [ 152 | { 153 | binding: 0, 154 | resource: { 155 | buffer: dtUB, 156 | }, 157 | }, 158 | ], 159 | }); 160 | 161 | GParticle.ready = true; 162 | } 163 | 164 | constructor(pass: RenderPass, constants: ParticleConstants) { 165 | checkDevice(); 166 | this.pass = pass; 167 | const device = GDevice.device; 168 | 169 | constants.max_spawn_per_frame = Math.min( 170 | constants.max_num, 171 | constants.max_spawn_per_frame, 172 | ); 173 | 174 | this.max_num = constants.max_num; 175 | this.max_spawn_per_frame = constants.max_spawn_per_frame; 176 | this.cpuStageBuf = new Float32Array(this.max_spawn_per_frame * 8); 177 | 178 | for (let i = 0; i < this.max_num; i++) { 179 | this.freeIndices.push(i); 180 | } 181 | 182 | this.paramsUB = device.createBuffer({ 183 | size: 4 * 8 + 4 * 4, 184 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 185 | mappedAtCreation: true, 186 | }); 187 | 188 | const view = new DataView(this.paramsUB.getMappedRange()); 189 | view.setFloat32(0, this.radius, true); 190 | this.paramsUB.unmap(); 191 | 192 | this.usedIndices = new Uint32Array(constants.max_num); 193 | this.particleBuf = device.createBuffer({ 194 | size: constants.max_num * Particles.STRIDE, 195 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 196 | }); 197 | 198 | this.indicesBuf = device.createBuffer({ 199 | size: constants.max_num * Uint32Array.BYTES_PER_ELEMENT, 200 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 201 | }); 202 | 203 | this.stageBuf = device.createBuffer({ 204 | size: constants.max_spawn_per_frame * Particles.STRIDE, 205 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 206 | }); 207 | 208 | this.particleLifeSpan = new Float32Array(constants.max_num); 209 | 210 | const { index, offset, buffer, model } = pass.allocUniform(); 211 | 212 | this.uniformIndex = index; 213 | this.modelGroup = device.createBindGroup({ 214 | layout: GParticle.renderPipeline.getBindGroupLayout(0), 215 | entries: [ 216 | { 217 | binding: 0, 218 | resource: { 219 | buffer, 220 | offset, 221 | size: Float32Array.BYTES_PER_ELEMENT * 16 * 2, 222 | }, 223 | }, 224 | ], 225 | }); 226 | 227 | this.renderGroup = device.createBindGroup({ 228 | layout: GParticle.renderPipeline.getBindGroupLayout(2), 229 | entries: [ 230 | { 231 | binding: 0, 232 | resource: { 233 | buffer: this.paramsUB, 234 | }, 235 | }, 236 | { 237 | binding: 1, 238 | resource: { 239 | buffer: this.particleBuf, 240 | }, 241 | }, 242 | { 243 | binding: 2, 244 | resource: { 245 | buffer: this.indicesBuf, 246 | }, 247 | }, 248 | ], 249 | }); 250 | 251 | this.stageGroup = device.createBindGroup({ 252 | layout: GParticle.stagePipeline.getBindGroupLayout(2), 253 | entries: [ 254 | { 255 | binding: 0, 256 | resource: { 257 | buffer: this.paramsUB, 258 | }, 259 | }, 260 | { 261 | binding: 1, 262 | resource: { 263 | buffer: this.particleBuf, 264 | }, 265 | }, 266 | { 267 | binding: 3, 268 | resource: { 269 | buffer: this.stageBuf, 270 | }, 271 | }, 272 | ], 273 | }); 274 | 275 | this.updateGroup = device.createBindGroup({ 276 | layout: GParticle.updatePipeline.getBindGroupLayout(2), 277 | entries: [ 278 | { 279 | binding: 0, 280 | resource: { 281 | buffer: this.paramsUB, 282 | }, 283 | }, 284 | { 285 | binding: 1, 286 | resource: { 287 | buffer: this.particleBuf, 288 | }, 289 | }, 290 | { 291 | binding: 2, 292 | resource: { 293 | buffer: this.indicesBuf, 294 | }, 295 | }, 296 | ], 297 | }); 298 | 299 | // TODO: remove this garbage 300 | this.stageUniformGroup = device.createBindGroup({ 301 | layout: GParticle.stagePipeline.getBindGroupLayout(0), 302 | entries: [], 303 | }); 304 | 305 | this.updateUniformGroup = device.createBindGroup({ 306 | layout: GParticle.updatePipeline.getBindGroupLayout(0), 307 | entries: [], 308 | }); 309 | 310 | this.stageCameraGroup = device.createBindGroup({ 311 | layout: GParticle.stagePipeline.getBindGroupLayout(1), 312 | entries: [], 313 | }); 314 | 315 | this.updateCameraGroup = device.createBindGroup({ 316 | layout: GParticle.updatePipeline.getBindGroupLayout(1), 317 | entries: [], 318 | }); 319 | 320 | this.transform = new Transform(model); 321 | this.transform.scale = [1, 1, 1]; 322 | this.transform.update(); 323 | this.transform.updateInverse(); 324 | } 325 | 326 | private filterParticles(dt: number) { 327 | const now = GDevice.now; 328 | 329 | let write_index = 0; 330 | for (let i = 0; i < this.count; i++) { 331 | if (i > write_index) this.usedIndices[write_index] = this.usedIndices[i]; 332 | 333 | const index = this.usedIndices[i]; 334 | if (this.particleLifeSpan[index] < now) { 335 | // Particle deleted 336 | this.particleLifeSpan[index] = 0; 337 | this.freeIndices.push(index); 338 | this.usedListTop--; 339 | } else { 340 | write_index++; 341 | this.particleLifeSpan[index] += dt; 342 | } 343 | } 344 | this.count = write_index; 345 | } 346 | 347 | private spawnParticles(dt: number) { 348 | const toSpawn = Math.min(~~(this.spawn_rate * dt), this.max_spawn_per_frame); 349 | const now = GDevice.now; 350 | 351 | const spawning = this.freeIndices.splice(0, toSpawn); 352 | this.usedIndices.set(spawning, this.usedListTop); 353 | this.usedListTop += spawning.length; 354 | this.count += spawning.length; 355 | 356 | for (let i = 0; i < spawning.length; i++) { 357 | const newIndex = spawning[i]; 358 | // Initialize life span 359 | this.particleLifeSpan[newIndex] = 360 | now + 361 | rng( 362 | this.lifeSpan[0] - this.lifeSpan[1], 363 | this.lifeSpan[0] + this.lifeSpan[1], 364 | ); 365 | 366 | // Initialize the particle to stage buffer 367 | const offset = i * 8; 368 | this.cpuStageBuf[offset + 0] = rng( 369 | this.initPos[0] - this.variPos[0], 370 | this.initPos[0] + this.variPos[0], 371 | ); 372 | this.cpuStageBuf[offset + 1] = rng( 373 | this.initPos[1] - this.variPos[1], 374 | this.initPos[1] + this.variPos[1], 375 | ); 376 | this.cpuStageBuf[offset + 2] = rng( 377 | this.initPos[2] - this.variPos[2], 378 | this.initPos[2] + this.variPos[2], 379 | ); 380 | this.cpuStageBuf[offset + 3] = newIndex; 381 | this.cpuStageBuf[offset + 4] = rng( 382 | this.initVel[0] - this.variVel[0], 383 | this.initVel[0] + this.variVel[0], 384 | ); 385 | this.cpuStageBuf[offset + 5] = rng( 386 | this.initVel[1] - this.variVel[1], 387 | this.initVel[1] + this.variVel[1], 388 | ); 389 | this.cpuStageBuf[offset + 6] = rng( 390 | this.initVel[2] - this.variVel[2], 391 | this.initVel[2] + this.variVel[2], 392 | ); 393 | } 394 | 395 | return spawning.length; 396 | } 397 | 398 | stage(dt: number, pass: GPUComputePassEncoder) { 399 | this.filterParticles(dt); 400 | 401 | if (this.pause) return; 402 | const spawned = this.spawnParticles(dt); 403 | 404 | const view = new DataView(this.sphereBuffer); 405 | 406 | view.setFloat32(0, this.radius, true); 407 | view.setUint32(4, this.count, true); 408 | view.setUint32(8, spawned, true); 409 | view.setFloat32(12, -9.81, true); 410 | view.setFloat32(16, this.coeffients[0], true); 411 | view.setFloat32(20, this.coeffients[1], true); 412 | view.setFloat32(24, this.coeffients[2], true); 413 | view.setFloat32(28, this.coeffients[3], true); 414 | view.setFloat32(32, this.wind[0], true); 415 | view.setFloat32(36, this.wind[1], true); 416 | view.setFloat32(40, this.wind[2], true); 417 | 418 | const queue = GDevice.device.queue; 419 | queue.writeBuffer(this.stageBuf, 0, this.cpuStageBuf, 0, spawned * 8); 420 | queue.writeBuffer(this.indicesBuf, 0, this.usedIndices, 0, this.count); 421 | queue.writeBuffer(this.paramsUB, 0, this.sphereBuffer); 422 | 423 | pass.setBindGroup(0, this.stageUniformGroup); 424 | pass.setBindGroup(1, this.stageCameraGroup); 425 | pass.setBindGroup(2, this.stageGroup); 426 | pass.dispatchWorkgroups(Math.ceil(spawned / 256)); 427 | } 428 | 429 | update(pass: GPUComputePassEncoder) { 430 | if (this.pause) return; 431 | 432 | pass.setBindGroup(0, this.updateUniformGroup); 433 | pass.setBindGroup(1, this.updateCameraGroup); 434 | pass.setBindGroup(2, this.updateGroup); 435 | pass.dispatchWorkgroups(Math.ceil(this.count / 256)); 436 | } 437 | 438 | async postUpdate() {} 439 | 440 | draw(pass: GPURenderPassEncoder) { 441 | pass.setBindGroup(0, this.modelGroup); 442 | pass.setBindGroup(2, this.renderGroup); 443 | pass.draw(36, this.count); // 36 vertices per cube (sphere) 444 | } 445 | 446 | free() { 447 | this.pass.freeUniformIndex(this.uniformIndex); 448 | 449 | this.particleBuf.destroy(); 450 | this.paramsUB.destroy(); 451 | // this.vectorUB.destroy(); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/engine/primitives/cube.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyVec3, vec3 } from 'gl-matrix'; 2 | import { RenderPass } from '../render/interfaces'; 3 | import StaticMesh from '../render/staticmesh'; 4 | 5 | // prettier-ignore 6 | const positions = new Float32Array(3 * 36); 7 | 8 | // prettier-ignore 9 | const normals = new Float32Array(3 * 36); 10 | 11 | // prettier-ignore 12 | const uvs = new Float32Array(2 * 36); 13 | 14 | const p1 = new Float32Array(3); 15 | const p2 = new Float32Array(3); 16 | const cross = new Float32Array(3); 17 | const norm = new Float32Array(3); 18 | 19 | const initOneFace = ( 20 | a: number, 21 | b: number, 22 | c: number, 23 | d: number, 24 | points: ReadonlyVec3[], 25 | offset: number, 26 | ) => { 27 | vec3.sub(p1, points[c], points[b]); 28 | vec3.sub(p2, points[a], points[b]); 29 | vec3.cross(cross, p1, p2); 30 | vec3.normalize(norm, cross); 31 | 32 | for (let i = 0; i < 6; i++) { 33 | const elem_offset = (offset + i) * 3; 34 | positions.set(points[[a, b, c, a, c, d][i]], elem_offset); 35 | normals.set(norm, elem_offset); 36 | } 37 | }; 38 | 39 | const POINTS = [ 40 | [-0.5, -0.5, +0.5], 41 | [-0.5, +0.5, +0.5], 42 | [+0.5, +0.5, +0.5], 43 | [+0.5, -0.5, +0.5], 44 | [-0.5, -0.5, -0.5], 45 | [-0.5, +0.5, -0.5], 46 | [+0.5, +0.5, -0.5], 47 | [+0.5, -0.5, -0.5], 48 | ] as ReadonlyVec3[]; 49 | 50 | initOneFace(1, 0, 3, 2, POINTS, 0 * 6); 51 | initOneFace(2, 3, 7, 6, POINTS, 1 * 6); 52 | initOneFace(3, 0, 4, 7, POINTS, 2 * 6); 53 | initOneFace(6, 5, 1, 2, POINTS, 3 * 6); 54 | initOneFace(4, 5, 6, 7, POINTS, 4 * 6); 55 | initOneFace(5, 4, 0, 1, POINTS, 5 * 6); 56 | 57 | // let out: string[] = []; 58 | // for (let i = 0; i < positions.length; i += 3) { 59 | // out.push(`vec3(${positions[i]}, ${positions[i + 1]}, ${positions[i + 2]})`); 60 | // } 61 | // console.log(out.join(',\n')); 62 | 63 | export const CubePositions = positions; 64 | 65 | export default class Cube extends StaticMesh { 66 | constructor(pass: RenderPass, extent: ReadonlyVec3 = [1, 1, 1]) { 67 | super(pass, positions, normals, uvs); 68 | // TODO: generate UVs and set scale 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/engine/render/base.ts: -------------------------------------------------------------------------------- 1 | import Cloth from '../cloth/cloth'; 2 | import Engine from '../core/engine'; 3 | import CameraRotator from '../input/input'; 4 | import Cube from '../primitives/cube'; 5 | import Camera from './camera'; 6 | import { DeferredPass } from './deferred-pass'; 7 | import PointLight from './pointlight'; 8 | import Scene from './scene'; 9 | import Stats from 'stats-js'; 10 | import { GUI } from 'dat.gui'; 11 | import Particles from '../particles/particles'; 12 | import { vec3 } from 'gl-matrix'; 13 | 14 | export const GDevice: { 15 | readyState: 0 | 1 | 2; 16 | adapter: GPUAdapter; 17 | device: GPUDevice; 18 | format: GPUTextureFormat; 19 | screen: { width: number; height: number }; 20 | now: number; 21 | } = { 22 | readyState: 0, 23 | adapter: null, 24 | device: null, 25 | format: null, 26 | screen: null, 27 | now: performance.now(), 28 | }; 29 | 30 | export const checkDevice = () => { 31 | if (GDevice.readyState !== 2) throw new Error('Device not ready'); 32 | }; 33 | 34 | export default class Renderer { 35 | private RAF = 0; 36 | private lastRAF = performance.now(); 37 | 38 | private engine: Engine; 39 | private canvas: HTMLCanvasElement | OffscreenCanvas; 40 | private ctx: GPUCanvasContext; 41 | 42 | private stats = new Stats(); 43 | private gui = new GUI(); 44 | 45 | private static DefaultDepthStencilTex: GPUTexture = null; 46 | static DefaultDepthStencilView: GPUTextureView = null; 47 | 48 | private readonly dimension: [number, number]; 49 | 50 | private pass: DeferredPass; 51 | private scene: Scene; 52 | private mainCamera: Camera; 53 | private cameraCtrl: CameraRotator; 54 | 55 | private readonly cubes: Cube[] = []; 56 | private cloth: Cloth; 57 | private particles: Particles; 58 | 59 | constructor(engine: Engine) { 60 | this.engine = engine; 61 | this.canvas = engine.params.canvas; 62 | this.ctx = this.canvas.getContext('webgpu'); 63 | this.dimension = [this.canvas.width, this.canvas.height]; 64 | 65 | document.body.appendChild(this.stats.dom); 66 | } 67 | 68 | async init() { 69 | if (!GDevice.readyState) { 70 | GDevice.readyState = 1; 71 | GDevice.adapter = await navigator.gpu.requestAdapter({ 72 | powerPreference: 'high-performance', 73 | }); 74 | GDevice.device = await GDevice.adapter.requestDevice({ 75 | // requiredLimits: { 76 | // maxColorAttachmentBytesPerSample: 64, 77 | // }, 78 | }); 79 | GDevice.readyState = 2; 80 | } else return; 81 | 82 | console.log(GDevice.device.limits); 83 | 84 | GDevice.format = navigator.gpu.getPreferredCanvasFormat(); 85 | this.ctx.configure({ 86 | device: GDevice.device, 87 | format: GDevice.format, 88 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 89 | alphaMode: 'opaque', 90 | }); 91 | 92 | this.scene = new Scene(this); 93 | this.mainCamera = new Camera(this.scene); 94 | this.cameraCtrl = new CameraRotator(this.mainCamera); 95 | 96 | const p = this.engine.params; 97 | GDevice.screen = p.screen; 98 | this.resize(p.screen.width * p.dpr, p.screen.height * p.dpr); 99 | 100 | this.pass = new DeferredPass(); 101 | 102 | this.setupLights(); 103 | this.start(); 104 | await this.pass.init(); 105 | } 106 | 107 | setupCubes() { 108 | const POS_RANGE = 250; 109 | const rng = (min: number, max: number) => Math.random() * (max - min) + min; 110 | 111 | const CUBES = 2500; 112 | for (let i = 0; i < CUBES; i++) { 113 | const cube = new Cube(this.pass); 114 | cube.transform.position = [ 115 | rng(-POS_RANGE, POS_RANGE), 116 | rng(0, 2 * POS_RANGE), 117 | rng(-POS_RANGE, POS_RANGE), 118 | ]; 119 | const scale = Math.random() * 10 + 1; 120 | cube.transform.rotation = [ 121 | Math.random(), 122 | Math.random(), 123 | Math.random(), 124 | Math.random(), 125 | ]; 126 | cube.transform.scale = [scale, scale, scale]; 127 | this.pass.meshDrawList.push(cube); 128 | this.cubes.push(cube); 129 | } 130 | } 131 | 132 | setupLights() { 133 | const POS_RANGE = 500; 134 | const rng = (min: number, max: number) => Math.random() * (max - min) + min; 135 | 136 | const LIGHTS = 1024; 137 | for (let i = 0; i < LIGHTS; i++) { 138 | const light = new PointLight(); 139 | light.position = new Float32Array([ 140 | rng(-POS_RANGE, POS_RANGE), 141 | rng(0, POS_RANGE), 142 | rng(-POS_RANGE, POS_RANGE), 143 | ]); 144 | 145 | light.color = new Float32Array([ 146 | rng(0.25, 0.75), 147 | rng(0.25, 0.75), 148 | rng(0.25, 0.75), 149 | ]); 150 | 151 | light.radius = 128; 152 | 153 | this.pass.lightDrawList.push(light); 154 | } 155 | 156 | this.pass.updateLight(); 157 | } 158 | 159 | setupCloth() { 160 | const gui = this.gui; 161 | const options = { 162 | 'Debug Normal': false, 163 | 'Wind X': 2, 164 | 'Wind Y': -1, 165 | 'Wind Z': 5, 166 | 'Floor Y': 5, 167 | 'Simulation Speed': 1, 168 | 'Reset Wind': function () { 169 | options['Wind X'] = options['Wind Y'] = options['Wind Z'] = 0; 170 | updateWind(); 171 | gui.updateDisplay(); 172 | }, 173 | }; 174 | 175 | const updateWind = () => { 176 | this.cloth.setWindSpeed([ 177 | options['Wind X'], 178 | options['Wind Y'], 179 | options['Wind Z'], 180 | ]); 181 | }; 182 | 183 | const updateFloor = () => { 184 | this.cloth.setFloor(options['Floor Y']); 185 | }; 186 | 187 | const DIM = 64; 188 | this.cloth = new Cloth(this.pass, DIM, DIM, { 189 | mass: 1, 190 | rest_length: 100 / DIM, 191 | springConstant: DIM * DIM, 192 | dampingConstant: 50, 193 | floor: options['Floor Y'], 194 | wind: [options['Wind X'], options['Wind Y'], options['Wind Z']], 195 | gravity: [0, -9.81, 0], 196 | }); 197 | this.cloth.transform.position = [-DIM / 2, -DIM / 2, 0]; 198 | this.cloth.transform.update(); 199 | this.cloth.transform.updateInverse(); 200 | 201 | this.pass.clothDrawList.push(this.cloth); 202 | 203 | console.log(GDevice.device.limits); 204 | // setTimeout(() => this.stop(), 100); 205 | 206 | // Wind speed range 207 | const WR = 10; 208 | 209 | gui.add(options, 'Debug Normal').onChange(v => (Cloth.debug = v)); 210 | gui.add(options, 'Wind X', -WR, WR, 0.01).onChange(updateWind); 211 | gui.add(options, 'Wind Y', -WR, WR, 0.01).onChange(updateWind); 212 | gui.add(options, 'Wind Z', -WR, WR, 0.01).onChange(updateWind); 213 | gui.add(options, 'Floor Y', 0, 1.5 * DIM, 0.01).onChange(updateFloor); 214 | gui.add(options, 'Simulation Speed', 0.1, 25, 0.01).onChange( 215 | v => (Cloth.sampleRate = 1 / v), 216 | ); 217 | gui.add(options, 'Reset Wind'); 218 | 219 | this.cloth.fixedPoints.forEach(({ row, col, x, y }, i) => { 220 | const coord = { x, y }; 221 | const updateCoord = () => 222 | this.cloth.setFixedPointPosition(row, col, coord.x, coord.y); 223 | const folder = gui.addFolder(`Fixed Point#${i}`); 224 | folder.add(coord, 'x', -DIM * 2, DIM * 2, 0.01).onChange(updateCoord); 225 | folder.add(coord, 'y', -DIM * 2, DIM * 2, 0.01).onChange(updateCoord); 226 | }); 227 | } 228 | 229 | setupParticles() { 230 | const p = (this.particles = new Particles(this.pass, { 231 | max_num: 1000000, 232 | max_spawn_per_frame: 10000, 233 | })); 234 | this.pass.particlesDrawList.push(this.particles); 235 | 236 | const gui = this.gui; 237 | const options = { 238 | Pause: function () { 239 | p.pause = !p.pause; 240 | }, 241 | Radius: p.radius, 242 | }; 243 | const addVectorOption = ( 244 | folder: GUI, 245 | vec: vec3, 246 | range: vec3, 247 | positiveOnly = false, 248 | ) => { 249 | const vecOp = { 250 | X: vec[0], 251 | Y: vec[1], 252 | Z: vec[2], 253 | }; 254 | for (let i = 0; i < 3; i++) 255 | folder 256 | .add( 257 | vecOp, 258 | 'XYZ'.charAt(i), 259 | positiveOnly ? 0 : vec[i] - range[i], 260 | positiveOnly ? range[i] : vec[i] + range[i], 261 | range[i] * 0.01, 262 | ) 263 | .onChange(v => (vec[i] = v)); 264 | }; 265 | 266 | gui.add(options, 'Pause'); 267 | const constOptions = { 268 | 'Air Density': p.coeffients[0], 269 | Drag: p.coeffients[1], 270 | 'Groud Elasticity': p.coeffients[2], 271 | 'Groud Friction': p.coeffients[3], 272 | }; 273 | const constants = gui.addFolder('Constants'); 274 | for (let i = 0; i < 4; i++) { 275 | constants 276 | .add( 277 | constOptions, 278 | Object.keys(constOptions)[i], 279 | 0, 280 | [2, 0.025, 1, 1][i], 281 | 0.001, 282 | ) 283 | .onChange(v => (p.coeffients[i] = v)); 284 | } 285 | 286 | const particle = gui.addFolder('Particle'); 287 | 288 | particle.add(options, 'Radius', 0.01, 5, 0.001).onChange(v => (p.radius = v)); 289 | 290 | const spawn = particle.addFolder('Spawn'); 291 | const spawnOptions = { 292 | 'Spawn Rate': p.spawn_rate, 293 | 'Life Span': p.lifeSpan[0], 294 | 'Life Variance': p.lifeSpan[1], 295 | }; 296 | 297 | spawn 298 | .add(spawnOptions, 'Spawn Rate', 0, 100000, p.spawn_rate * 0.001) 299 | .onChange(v => (p.spawn_rate = v)); 300 | spawn 301 | .add(spawnOptions, 'Life Span', 5000, 25000, 100) 302 | .onChange(v => (p.lifeSpan[0] = v)); 303 | spawn 304 | .add(spawnOptions, 'Life Variance', 0, 5000, 100) 305 | .onChange(v => (p.lifeSpan[1] = v)); 306 | 307 | addVectorOption(gui.addFolder('Wind'), p.wind, [100, 100, 100]); 308 | addVectorOption( 309 | particle.addFolder('Initial Position'), 310 | p.initPos, 311 | [1000, 1000, 1000], 312 | ); 313 | addVectorOption( 314 | particle.addFolder('Position Variance'), 315 | p.variPos, 316 | [500, 500, 500], 317 | true, 318 | ); 319 | addVectorOption( 320 | particle.addFolder('Initial Velocity'), 321 | p.initVel, 322 | [100, 100, 100], 323 | ); 324 | addVectorOption( 325 | particle.addFolder('Velocity Variance'), 326 | p.variVel, 327 | [50, 50, 50], 328 | true, 329 | ); 330 | } 331 | 332 | resize(w: number, h: number) { 333 | this.canvas.width = w; 334 | this.canvas.height = h; 335 | 336 | this.ctx.configure({ 337 | device: GDevice.device, 338 | format: GDevice.format, 339 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 340 | alphaMode: 'opaque', 341 | }); 342 | 343 | if (this.dimension[0] < w || this.dimension[1] < h) { 344 | Renderer.DefaultDepthStencilTex?.destroy(); 345 | Renderer.DefaultDepthStencilTex = GDevice.device.createTexture({ 346 | size: { width: w, height: h }, 347 | mipLevelCount: 1, 348 | dimension: '2d', 349 | format: 'depth24plus-stencil8', 350 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, 351 | }); 352 | Renderer.DefaultDepthStencilView = 353 | Renderer.DefaultDepthStencilTex.createView(); 354 | } 355 | 356 | this.dimension[0] = w; 357 | this.dimension[1] = h; 358 | GDevice.screen.width = w; 359 | GDevice.screen.height = h; 360 | 361 | this.mainCamera.aspect = w / h; 362 | } 363 | 364 | start() { 365 | if (this.RAF) return; 366 | 367 | const cb = async (now: number) => { 368 | GDevice.now = now; 369 | this.stats.begin(); 370 | 371 | const t = now * 0.001; 372 | 373 | this.mainCamera.update(); 374 | 375 | for (let i = 0; i < this.cubes.length; i++) { 376 | const cube = this.cubes[i]; 377 | 378 | cube.transform.rotation = [Math.sin(t + i), Math.cos(t + i), 0, 0]; 379 | cube.transform.update(); 380 | cube.transform.updateInverse(); 381 | } 382 | 383 | const dt = Math.min(1 / 60, (now - this.lastRAF) / 1000); 384 | 385 | await this.pass.render( 386 | dt, 387 | now, 388 | this.ctx.getCurrentTexture().createView(), 389 | this.mainCamera.view, 390 | ); 391 | 392 | this.RAF = requestAnimationFrame(cb); 393 | this.lastRAF = now; 394 | 395 | this.stats.end(); 396 | }; 397 | this.RAF = requestAnimationFrame(cb); 398 | } 399 | 400 | stop() { 401 | if (!this.RAF) return; 402 | cancelAnimationFrame(this.RAF); 403 | this.RAF = 0; 404 | } 405 | 406 | get aspectRatio() { 407 | return this.dimension[0] / this.dimension[1]; 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/engine/render/camera.ts: -------------------------------------------------------------------------------- 1 | import { mat4, ReadonlyVec3, vec3, glMatrix } from 'gl-matrix'; 2 | import Scene from './scene'; 3 | 4 | export default class Camera { 5 | private scene: Scene; 6 | 7 | public fov = 0; 8 | public aspect = 0; 9 | public nearClip = 0; 10 | public farClip = 0; 11 | 12 | public distance = 0; 13 | public azimuth = 0; 14 | public incline = 0; 15 | 16 | public targetDistance = 0; 17 | 18 | public readonly view = new Float32Array(16 + 4); 19 | 20 | constructor(scene: Scene) { 21 | this.scene = scene; 22 | this.reset(); 23 | } 24 | 25 | public update() { 26 | this.distance += (this.targetDistance - this.distance) / 60; 27 | 28 | const temp = mat4.create(); 29 | const world = mat4.create(); 30 | world[14] = this.distance; 31 | 32 | const rotX = mat4.fromYRotation(mat4.create(), glMatrix.toRadian(-this.azimuth)); 33 | const rotY = mat4.fromXRotation(mat4.create(), glMatrix.toRadian(-this.incline)); 34 | mat4.mul(temp, rotX, rotY); 35 | 36 | const final = mat4.mul(mat4.create(), temp, world); 37 | 38 | mat4.getTranslation(this.view.subarray(16), final); 39 | 40 | const view = mat4.invert(mat4.create(), final); 41 | 42 | const proj = mat4.perspective( 43 | mat4.create(), 44 | glMatrix.toRadian(this.fov), 45 | this.aspect, 46 | this.nearClip, 47 | this.farClip, 48 | ); 49 | 50 | mat4.mul(this.view, proj, view); 51 | } 52 | 53 | public reset() { 54 | this.fov = 60; 55 | this.aspect = 1.33; 56 | this.nearClip = 0.1; 57 | this.farClip = 2500; 58 | 59 | this.targetDistance = this.distance = 100; 60 | this.azimuth = 0; 61 | this.incline = 20; 62 | } 63 | } 64 | 65 | const UP = new Float32Array([0, 1, 0]); 66 | 67 | const temp = new Float32Array(3); 68 | 69 | export class OldCamera { 70 | private scene: Scene; 71 | 72 | private _pov = (2 * Math.PI) / 5; 73 | private readonly _pos = new Float32Array(3); 74 | private readonly _dir = new Float32Array(3); 75 | 76 | private readonly projection = new Float32Array(16); 77 | private readonly view = new Float32Array(16); 78 | public readonly vp = new Float32Array(16); 79 | 80 | constructor(scene: Scene) { 81 | this.scene = scene; 82 | 83 | mat4.perspectiveZO( 84 | this.projection, 85 | this._pov, 86 | scene.renderer.aspectRatio, 87 | 1, 88 | 2000, 89 | ); 90 | this._pos.set([100, 100, 100]); 91 | this.lookAt([0, 0, 0]); 92 | } 93 | 94 | updateMatrix() { 95 | vec3.add(temp, this._pos, this._dir); 96 | mat4.lookAt(this.view, this._pos, temp, UP); 97 | mat4.mul(this.vp, this.projection, this.view); 98 | } 99 | 100 | lookAt(target: ReadonlyVec3) { 101 | vec3.sub(temp, target, this._pos); 102 | vec3.normalize(this._dir, temp); 103 | this.updateMatrix(); 104 | } 105 | 106 | get position() { 107 | return this._pos.slice(); 108 | } 109 | 110 | set position(target: ReadonlyVec3) { 111 | this._pos.set(target); 112 | this.updateMatrix(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/engine/render/deferred-pass.ts: -------------------------------------------------------------------------------- 1 | import { checkDevice, GDevice } from './base'; 2 | 3 | import GBufferVertWGSL from '../shaders/gbuffer.vert.wgsl'; 4 | import GBufferFragWGSL from '../shaders/gbuffer.frag.wgsl'; 5 | import QuadVertWGSL from '../shaders/quad.vert.wgsl'; 6 | import DeferredFragWGSL from '../shaders/deferred.frag.wgsl'; 7 | import PointLight from './pointlight'; 8 | import { Renderable, RenderPass } from './interfaces'; 9 | import Cloth, { GCloth } from '../cloth/cloth'; 10 | import Particles, { GParticle } from '../particles/particles'; 11 | 12 | export const GBuffer: { 13 | ready: boolean; 14 | views: GPUTextureView[]; 15 | basePipeline: GPURenderPipeline; 16 | basePassVertShader: GPUShaderModule; 17 | basePassFragShader: GPUShaderModule; 18 | deferredPipeline: GPURenderPipeline; 19 | basePassDesc: GPURenderPassDescriptor; 20 | deferredPassDesc: GPURenderPassDescriptor; 21 | texGroup: GPUBindGroup; 22 | dimGroup: GPUBindGroup; 23 | lightGroup: GPUBindGroup; 24 | viewGroup: GPUBindGroup; 25 | } = { 26 | ready: false, 27 | views: null, 28 | basePipeline: null, 29 | basePassVertShader: null, 30 | basePassFragShader: null, 31 | deferredPipeline: null, 32 | basePassDesc: null, 33 | deferredPassDesc: null, 34 | texGroup: null, 35 | dimGroup: null, 36 | lightGroup: null, 37 | viewGroup: null, 38 | }; 39 | 40 | export class DeferredPass implements RenderPass { 41 | private posnorm: GPUTexture; 42 | private albedo: GPUTexture; 43 | private depth: GPUTexture; 44 | 45 | private configUB: GPUBuffer; 46 | private lightNum = new Uint32Array([0]); 47 | private lightBuf = new Float32Array(DeferredPass.MAX_LIGHTS * PointLight.STRIDE); 48 | 49 | static readonly MAX_LIGHTS = 1024; 50 | static readonly MAX_MESHES = 4096; 51 | 52 | private clothDTUB: GPUBuffer; 53 | private particleDTUB: GPUBuffer; 54 | private camUB: GPUBuffer; 55 | private dimUB: GPUBuffer; 56 | private lightSB: GPUBuffer; 57 | private modelUB: GPUBuffer; 58 | 59 | public readonly lightDrawList: PointLight[] = []; 60 | public readonly meshDrawList: Renderable[] = []; 61 | public readonly clothDrawList: Cloth[] = []; 62 | public readonly particlesDrawList: Particles[] = []; 63 | 64 | private readonly freeModelUniformIndices: number[] = new Array( 65 | DeferredPass.MAX_MESHES, 66 | ); 67 | 68 | private modelBufs: Float32Array; 69 | 70 | public static readonly VertexLayout: GPUVertexBufferLayout = { 71 | arrayStride: Float32Array.BYTES_PER_ELEMENT * 8, 72 | attributes: [ 73 | { 74 | // position 75 | shaderLocation: 0, 76 | offset: 0, 77 | format: 'float32x3', 78 | }, 79 | { 80 | // normal 81 | shaderLocation: 1, 82 | offset: Float32Array.BYTES_PER_ELEMENT * 3, 83 | format: 'float32x3', 84 | }, 85 | { 86 | // uv 87 | shaderLocation: 2, 88 | offset: Float32Array.BYTES_PER_ELEMENT * 6, 89 | format: 'float32x2', 90 | }, 91 | ], 92 | }; 93 | 94 | async init() { 95 | checkDevice(); 96 | if (GBuffer.ready) return; 97 | 98 | const screen = GDevice.screen; 99 | const screenSize2D = [screen.width, screen.height]; 100 | const device = GDevice.device; 101 | 102 | for (let i = 0; i < DeferredPass.MAX_MESHES; i++) 103 | this.freeModelUniformIndices[i] = i; 104 | 105 | const perModelFloats = 106 | device.limits.minUniformBufferOffsetAlignment / 107 | Float32Array.BYTES_PER_ELEMENT; 108 | 109 | this.modelBufs = new Float32Array(perModelFloats * DeferredPass.MAX_MESHES); 110 | 111 | // Create textures 112 | this.posnorm = device.createTexture({ 113 | size: [...screenSize2D, 2], 114 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 115 | format: 'rgba16float', 116 | }); 117 | 118 | this.albedo = device.createTexture({ 119 | size: screenSize2D, 120 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 121 | format: 'bgra8unorm', 122 | }); 123 | 124 | this.depth = device.createTexture({ 125 | size: screenSize2D, 126 | format: 'depth24plus', 127 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 128 | }); 129 | 130 | GBuffer.views = [ 131 | this.posnorm.createView({ 132 | baseArrayLayer: 0, 133 | arrayLayerCount: 1, 134 | dimension: '2d', 135 | }), 136 | this.posnorm.createView({ 137 | baseArrayLayer: 1, 138 | arrayLayerCount: 1, 139 | dimension: '2d', 140 | }), 141 | this.albedo.createView(), 142 | ]; 143 | 144 | // Create buffers 145 | this.configUB = device.createBuffer({ 146 | size: Uint32Array.BYTES_PER_ELEMENT, 147 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 148 | mappedAtCreation: true, 149 | }); 150 | 151 | new Uint32Array(this.configUB.getMappedRange())[0] = this.lightNum[0]; 152 | this.configUB.unmap(); 153 | 154 | this.lightSB = device.createBuffer({ 155 | size: 156 | Float32Array.BYTES_PER_ELEMENT * 157 | PointLight.STRIDE * 158 | DeferredPass.MAX_LIGHTS, 159 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 160 | }); 161 | 162 | // 2 * mat4x4 * MAX_MODELS? 163 | this.modelUB = device.createBuffer({ 164 | size: device.limits.minUniformBufferOffsetAlignment * DeferredPass.MAX_MESHES, 165 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 166 | }); 167 | 168 | // f32 169 | this.clothDTUB = device.createBuffer({ 170 | size: Float32Array.BYTES_PER_ELEMENT, 171 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 172 | }); 173 | 174 | this.particleDTUB = device.createBuffer({ 175 | size: Float32Array.BYTES_PER_ELEMENT, 176 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 177 | }); 178 | 179 | // mat4x4 + vec3 180 | this.camUB = device.createBuffer({ 181 | size: Float32Array.BYTES_PER_ELEMENT * (16 + 4), 182 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 183 | }); 184 | 185 | // vec2 186 | this.dimUB = device.createBuffer({ 187 | size: Float32Array.BYTES_PER_ELEMENT * 2, 188 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 189 | }); 190 | 191 | GBuffer.basePassVertShader = device.createShaderModule({ 192 | code: GBufferVertWGSL, 193 | }); 194 | 195 | GBuffer.basePassFragShader = device.createShaderModule({ 196 | code: GBufferFragWGSL, 197 | }); 198 | 199 | GBuffer.basePipeline = await device.createRenderPipelineAsync({ 200 | layout: 'auto', 201 | vertex: { 202 | module: GBuffer.basePassVertShader, 203 | entryPoint: 'main', 204 | buffers: [DeferredPass.VertexLayout], 205 | }, 206 | fragment: { 207 | module: GBuffer.basePassFragShader, 208 | entryPoint: 'main', 209 | targets: [ 210 | // position 211 | { format: 'rgba16float' }, 212 | // normal 213 | { format: 'rgba16float' }, 214 | // albedo 215 | { format: 'bgra8unorm' }, 216 | ], 217 | }, 218 | depthStencil: { 219 | depthWriteEnabled: true, 220 | depthCompare: 'less', 221 | format: 'depth24plus', 222 | }, 223 | primitive: { 224 | topology: 'triangle-list', 225 | cullMode: 'back', 226 | }, 227 | }); 228 | 229 | const texLayout = device.createBindGroupLayout({ 230 | entries: GBuffer.views.map((_, binding) => ({ 231 | binding, 232 | visibility: GPUShaderStage.FRAGMENT, 233 | texture: { sampleType: 'unfilterable-float' }, 234 | })), 235 | }); 236 | 237 | GBuffer.texGroup = device.createBindGroup({ 238 | layout: texLayout, 239 | entries: GBuffer.views.map((resource, binding) => ({ binding, resource })), 240 | }); 241 | 242 | const lightLayout = device.createBindGroupLayout({ 243 | entries: [ 244 | // Light data 245 | { 246 | binding: 0, 247 | visibility: GPUShaderStage.FRAGMENT, 248 | buffer: { 249 | type: 'storage', 250 | }, 251 | }, 252 | // Light num 253 | { 254 | binding: 1, 255 | visibility: GPUShaderStage.FRAGMENT, 256 | buffer: { 257 | type: 'uniform', 258 | }, 259 | }, 260 | ], 261 | }); 262 | 263 | GBuffer.lightGroup = device.createBindGroup({ 264 | layout: lightLayout, 265 | entries: [ 266 | { binding: 0, resource: { buffer: this.lightSB } }, 267 | { binding: 1, resource: { buffer: this.configUB } }, 268 | ], 269 | }); 270 | 271 | const sizeLayout = device.createBindGroupLayout({ 272 | entries: [ 273 | { 274 | binding: 0, 275 | visibility: GPUShaderStage.FRAGMENT, 276 | buffer: { 277 | type: 'uniform', 278 | }, 279 | }, 280 | ], 281 | }); 282 | 283 | const quadVertShader = device.createShaderModule({ 284 | code: QuadVertWGSL, 285 | }); 286 | 287 | const deferredLightsFragShader = device.createShaderModule({ 288 | code: DeferredFragWGSL, 289 | }); 290 | 291 | GBuffer.deferredPipeline = await device.createRenderPipelineAsync({ 292 | layout: device.createPipelineLayout({ 293 | bindGroupLayouts: [texLayout, lightLayout, sizeLayout], 294 | }), 295 | vertex: { 296 | module: quadVertShader, 297 | entryPoint: 'main', 298 | }, 299 | fragment: { 300 | module: deferredLightsFragShader, 301 | entryPoint: 'main', 302 | targets: [ 303 | { 304 | format: GDevice.format, 305 | }, 306 | ], 307 | }, 308 | primitive: { 309 | topology: 'triangle-list', 310 | cullMode: 'none', 311 | }, 312 | }); 313 | 314 | GBuffer.basePassDesc = { 315 | colorAttachments: GBuffer.views.map((view, i) => ({ 316 | view, 317 | clearValue: i 318 | ? { r: 0, g: 0, b: 0, a: 0 } 319 | : { 320 | r: Number.MAX_VALUE, 321 | g: Number.MAX_VALUE, 322 | b: Number.MAX_VALUE, 323 | a: Number.MAX_VALUE, 324 | }, 325 | loadOp: 'clear', 326 | storeOp: 'store', 327 | })), 328 | depthStencilAttachment: { 329 | view: this.depth.createView(), 330 | depthClearValue: 1.0, 331 | depthLoadOp: 'clear', 332 | depthStoreOp: 'store', 333 | }, 334 | }; 335 | 336 | GBuffer.deferredPassDesc = { 337 | colorAttachments: [ 338 | { 339 | // view is acquired and set in render loop. 340 | view: undefined, 341 | clearValue: { r: 0, g: 0, b: 0, a: 1 }, 342 | loadOp: 'clear', 343 | storeOp: 'store', 344 | }, 345 | ], 346 | }; 347 | 348 | GBuffer.viewGroup = device.createBindGroup({ 349 | layout: GBuffer.basePipeline.getBindGroupLayout(1), 350 | entries: [ 351 | { 352 | binding: 0, 353 | resource: { 354 | buffer: this.camUB, 355 | }, 356 | }, 357 | ], 358 | }); 359 | 360 | GBuffer.dimGroup = device.createBindGroup({ 361 | layout: sizeLayout, 362 | entries: [{ binding: 0, resource: { buffer: this.dimUB } }], 363 | }); 364 | 365 | device.queue.writeBuffer(this.dimUB, 0, new Float32Array(screenSize2D)); 366 | 367 | GBuffer.ready = true; 368 | 369 | await Promise.all([ 370 | Cloth.initPipeline(this.camUB, this.clothDTUB), 371 | Particles.initPipeline(this.camUB, this.particleDTUB), 372 | ]); 373 | } 374 | 375 | updateSize() { 376 | // TODO 377 | } 378 | 379 | updateLight() { 380 | for (let i = 0; i < this.lightDrawList.length; i++) { 381 | const light = this.lightDrawList[i]; 382 | this.lightBuf.set(light.buffer, PointLight.STRIDE * i); 383 | } 384 | this.lightNum[0] = this.lightDrawList.length; 385 | } 386 | 387 | allocUniform() { 388 | const index = this.freeModelUniformIndices.shift(); 389 | const align = GDevice.device.limits.minUniformBufferOffsetAlignment; 390 | const perModelFloats = align / Float32Array.BYTES_PER_ELEMENT; 391 | 392 | const begin = index * perModelFloats; 393 | const end = index * perModelFloats + 16 * 2; 394 | 395 | return { 396 | index, 397 | offset: align * index, 398 | buffer: this.modelUB, 399 | model: this.modelBufs.subarray(begin, end), 400 | layout: GBuffer.basePipeline.getBindGroupLayout(0), 401 | }; 402 | } 403 | 404 | freeUniformIndex(index: number) { 405 | this.freeModelUniformIndices.push(index); 406 | } 407 | 408 | async render(dt: number, now: number, output: GPUTextureView, camBuf: Float32Array) { 409 | if (!GBuffer.ready) return; 410 | 411 | const device = GDevice.device; 412 | const queue = device.queue; 413 | 414 | queue.writeBuffer(this.camUB, 0, camBuf); 415 | queue.writeBuffer(this.configUB, 0, this.lightNum); 416 | queue.writeBuffer(this.lightSB, 0, this.lightBuf); 417 | queue.writeBuffer(this.modelUB, 0, this.modelBufs); 418 | 419 | const cmd = device.createCommandEncoder(); 420 | const cmds: GPUCommandEncoder[] = [cmd]; 421 | 422 | const SAMPLE_STEP = 1; 423 | 424 | // Milliseconds 425 | queue.writeBuffer(this.clothDTUB, 0, new Float32Array([SAMPLE_STEP * 0.001])); 426 | 427 | if (GCloth.ready && this.clothDrawList.length) { 428 | let loop = 0; 429 | while (Cloth.sampleTime < now) { 430 | Cloth.sampleTime += SAMPLE_STEP * Cloth.sampleRate; 431 | loop++; 432 | } 433 | 434 | for (let i = 0; i < Math.min(50, loop); i++) { 435 | // Cloth compute pass 436 | const ClothForcePass = cmd.beginComputePass(); 437 | ClothForcePass.setPipeline(GCloth.simPipeline); 438 | for (const cloth of this.clothDrawList) cloth.simulate(ClothForcePass); 439 | ClothForcePass.end(); 440 | 441 | // Cloth update pass 442 | const ClothUpdatePass = cmd.beginComputePass(); 443 | ClothUpdatePass.setBindGroup(2, GCloth.dtGroup); 444 | ClothUpdatePass.setPipeline(GCloth.updatePipeline); 445 | for (const cloth of this.clothDrawList) cloth.update(ClothUpdatePass); 446 | ClothUpdatePass.end(); 447 | } 448 | 449 | const ClothNormalPass = cmd.beginComputePass(); 450 | ClothNormalPass.setPipeline(GCloth.normalCalcPipeline); 451 | for (const cloth of this.clothDrawList) cloth.recalcNormals(ClothNormalPass); 452 | ClothNormalPass.end(); 453 | } 454 | 455 | queue.writeBuffer(this.particleDTUB, 0, new Float32Array([1 / 60])); 456 | 457 | if (GParticle.ready && this.particlesDrawList.length) { 458 | const StagePass = cmd.beginComputePass(); 459 | StagePass.setPipeline(GParticle.stagePipeline); 460 | for (const particles of this.particlesDrawList) 461 | particles.stage(dt, StagePass); 462 | StagePass.end(); 463 | 464 | const UpdatePass = cmd.beginComputePass(); 465 | UpdatePass.setPipeline(GParticle.updatePipeline); 466 | UpdatePass.setBindGroup(3, GParticle.dtGroup); 467 | for (const particles of this.particlesDrawList) particles.update(UpdatePass); 468 | UpdatePass.end(); 469 | } 470 | 471 | // GBuffer base pass 472 | const GBufferPass = cmd.beginRenderPass(GBuffer.basePassDesc); 473 | 474 | GBufferPass.setViewport(0, 0, GDevice.screen.width, GDevice.screen.height, 0, 1); 475 | 476 | // Draw meshes 477 | GBufferPass.setPipeline(GBuffer.basePipeline); 478 | GBufferPass.setBindGroup(1, GBuffer.viewGroup); 479 | 480 | for (const mesh of this.meshDrawList) mesh.draw(GBufferPass); 481 | 482 | // Draw clothes / debug normal 483 | if (GCloth.ready && this.clothDrawList.length) { 484 | if (Cloth.debug) { 485 | GBufferPass.setPipeline(GCloth.normalDebugPipeline); 486 | GBufferPass.setBindGroup(1, GCloth.debugViewGroup); 487 | for (const cloth of this.clothDrawList) cloth.debug(GBufferPass); 488 | } else { 489 | GBufferPass.setPipeline(GCloth.renderPipeline); 490 | GBufferPass.setBindGroup(1, GCloth.viewGroup); 491 | for (const cloth of this.clothDrawList) cloth.draw(GBufferPass); 492 | } 493 | } 494 | 495 | if (GParticle.ready) { 496 | GBufferPass.setPipeline(GParticle.renderPipeline); 497 | GBufferPass.setBindGroup(1, GParticle.viewGroup); 498 | for (const particples of this.particlesDrawList) particples.draw(GBufferPass); 499 | } 500 | 501 | GBufferPass.end(); 502 | 503 | // Render to swapchain texture 504 | GBuffer.deferredPassDesc.colorAttachments[0].view = output; 505 | 506 | // Deferred lighting pass 507 | const DeferredPass = cmd.beginRenderPass(GBuffer.deferredPassDesc); 508 | 509 | DeferredPass.setPipeline(GBuffer.deferredPipeline); 510 | DeferredPass.setBindGroup(0, GBuffer.texGroup); 511 | DeferredPass.setBindGroup(1, GBuffer.lightGroup); 512 | DeferredPass.setBindGroup(2, GBuffer.dimGroup); 513 | DeferredPass.draw(6); 514 | DeferredPass.end(); 515 | 516 | queue.submit(cmds.map(c => c.finish())); 517 | 518 | GBuffer.deferredPassDesc.colorAttachments[0].view = null; 519 | 520 | const t1 = this.particlesDrawList.map(f => f.postUpdate()); 521 | const t2 = this.clothDrawList.map(c => c.postUpdate()); 522 | await Promise.all(t1.concat(t2)); 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /src/engine/render/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface RenderPass { 2 | allocUniform(): { 3 | index: number; 4 | offset: number; 5 | buffer: GPUBuffer; 6 | model: Float32Array; 7 | layout: GPUBindGroupLayout; 8 | }; 9 | 10 | freeUniformIndex(index: number): void; 11 | } 12 | 13 | export interface Renderable { 14 | draw(pass: GPURenderPassEncoder); 15 | free(); 16 | } 17 | -------------------------------------------------------------------------------- /src/engine/render/pointlight.ts: -------------------------------------------------------------------------------- 1 | export default class PointLight { 2 | static readonly STRIDE = 8; 3 | 4 | readonly buffer = new Float32Array(PointLight.STRIDE); 5 | 6 | get position() { 7 | return this.buffer.slice(0, 3); 8 | } 9 | 10 | set position(value: Float32Array) { 11 | this.buffer.set(value.subarray(0, 3), 0); 12 | } 13 | 14 | get color() { 15 | return this.buffer.slice(4, 7); 16 | } 17 | 18 | set color(value: Float32Array) { 19 | this.buffer.set(value.subarray(0, 3), 4); 20 | } 21 | 22 | get radius() { 23 | return this.buffer[7]; 24 | } 25 | 26 | set radius(value: number) { 27 | this.buffer[7] = value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/engine/render/scene.ts: -------------------------------------------------------------------------------- 1 | import Renderer from './base'; 2 | 3 | export default class Scene { 4 | public renderer: Renderer; 5 | 6 | constructor(renderer: Renderer) { 7 | this.renderer = renderer; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/engine/render/staticmesh.ts: -------------------------------------------------------------------------------- 1 | import Transform from '../core/transform'; 2 | import { GDevice } from './base'; 3 | import { Renderable, RenderPass } from './interfaces'; 4 | 5 | export default class StaticMesh implements Renderable { 6 | public readonly transform: Transform; 7 | public readonly count: number; 8 | 9 | private readonly indexFormat: GPUIndexFormat; 10 | 11 | private readonly vbo: GPUBuffer; 12 | private readonly ibo: GPUBuffer; 13 | 14 | public readonly uniformIndex: number; 15 | private readonly modelGroup: GPUBindGroup; 16 | private readonly pass: RenderPass; 17 | 18 | constructor( 19 | pass: RenderPass, 20 | positions: Float32Array, 21 | normals: Float32Array, 22 | uvs: Float32Array, 23 | indices?: Uint32Array | Uint16Array, 24 | ) { 25 | if (GDevice.readyState !== 2) { 26 | throw new Error('GPUDevice not ready'); 27 | } 28 | if ( 29 | positions.length % 3 || 30 | positions.length !== normals.length || 31 | uvs.length / 2 !== normals.length / 3 32 | ) { 33 | throw new Error('StaticMesh: Buffer length mismatch'); 34 | } 35 | 36 | this.pass = pass; 37 | const device = GDevice.device; 38 | 39 | const elem = positions.length / 3; 40 | const stride = 3 + 3 + 2; 41 | this.vbo = device.createBuffer({ 42 | // position: vec3, normal: vec3, uv: vec2 43 | size: elem * stride * Float32Array.BYTES_PER_ELEMENT, 44 | usage: GPUBufferUsage.VERTEX, 45 | mappedAtCreation: true, 46 | }); 47 | 48 | const mapped = new Float32Array(this.vbo.getMappedRange()); 49 | for (let i = 0; i < elem; i++) { 50 | mapped[stride * i + 0] = positions[i * 3 + 0]; 51 | mapped[stride * i + 1] = positions[i * 3 + 1]; 52 | mapped[stride * i + 2] = positions[i * 3 + 2]; 53 | mapped[stride * i + 3] = normals[i * 3 + 0]; 54 | mapped[stride * i + 4] = normals[i * 3 + 1]; 55 | mapped[stride * i + 5] = normals[i * 3 + 2]; 56 | mapped[stride * i + 6] = uvs[i * 2 + 0]; 57 | mapped[stride * i + 7] = uvs[i * 2 + 1]; 58 | } 59 | this.vbo.unmap(); 60 | 61 | if (!indices) { 62 | this.indexFormat = null; 63 | this.ibo = null; 64 | 65 | this.count = elem; 66 | } else { 67 | this.count = indices.length / 3; 68 | 69 | this.ibo = device.createBuffer({ 70 | size: this.count, 71 | usage: GPUBufferUsage.INDEX, 72 | mappedAtCreation: true, 73 | }); 74 | 75 | if (indices.BYTES_PER_ELEMENT === 2) { 76 | new Uint16Array(this.ibo.getMappedRange()).set(indices); 77 | this.indexFormat = 'uint16'; 78 | } else if (indices.BYTES_PER_ELEMENT === 4) { 79 | new Uint32Array(this.ibo.getMappedRange()).set(indices); 80 | this.indexFormat = 'uint32'; 81 | } 82 | this.ibo.unmap(); 83 | } 84 | 85 | const { index, offset, buffer, model, layout } = pass.allocUniform(); 86 | 87 | this.uniformIndex = index; 88 | this.modelGroup = device.createBindGroup({ 89 | layout, 90 | entries: [ 91 | { 92 | binding: 0, 93 | resource: { 94 | buffer, 95 | offset, 96 | size: Float32Array.BYTES_PER_ELEMENT * 16 * 2, 97 | }, 98 | }, 99 | ], 100 | }); 101 | 102 | this.transform = new Transform(model); 103 | this.transform.update(); 104 | this.transform.updateInverse(); 105 | } 106 | 107 | free() { 108 | this.pass.freeUniformIndex(this.uniformIndex); 109 | this.vbo.destroy(); 110 | this.ibo?.destroy(); 111 | } 112 | 113 | draw(pass: GPURenderPassEncoder) { 114 | pass.setBindGroup(0, this.modelGroup); 115 | pass.setVertexBuffer(0, this.vbo); 116 | if (this.ibo) { 117 | pass.setIndexBuffer(this.ibo, this.indexFormat); 118 | pass.drawIndexed(this.count); 119 | } else { 120 | pass.draw(this.count); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/engine/shaders/cloth.wgsl: -------------------------------------------------------------------------------- 1 | struct ClothPoint { 2 | position: vec4, // using w component to store if this point is fixed (padded anyways) 3 | normal: vec4, 4 | force: vec4, 5 | velocity: vec4, 6 | }; 7 | 8 | struct ClothPointShared { 9 | p: vec4, 10 | v: vec4, 11 | }; 12 | 13 | struct ClothPoints { 14 | data: array, 15 | }; 16 | 17 | struct SimulationConstants { 18 | mass: f32, 19 | rest_length: f32, 20 | spring_constant: f32, 21 | damping_constant: f32, 22 | floor_y: f32, 23 | }; 24 | 25 | struct Dimension { 26 | size: vec2, 27 | }; 28 | 29 | struct Vectors { 30 | wind: vec3, 31 | gravity: vec3, 32 | }; 33 | 34 | struct Indices { 35 | data: array>, 36 | }; 37 | 38 | // Group 1 is per cloth 39 | @group(1) @binding(0) var constants: SimulationConstants; 40 | @group(1) @binding(1) var dimension: Dimension; 41 | @group(1) @binding(2) var vectors: Vectors; 42 | 43 | const TILE_SIZE = 16; 44 | const TILE_SIZE_U = 16u; 45 | const INNER_TILE = 14u; 46 | 47 | @group(0) @binding(0) var indices: Indices; 48 | 49 | @compute @workgroup_size(TILE_SIZE, TILE_SIZE, 1) 50 | fn init_indices( 51 | @builtin(workgroup_id) blockIdx : vec3, 52 | @builtin(local_invocation_id) threadIdx : vec3 53 | ) { 54 | let tx = threadIdx.x; 55 | let ty = threadIdx.y; 56 | 57 | let row = blockIdx.y * TILE_SIZE_U + ty; 58 | let col = blockIdx.x * TILE_SIZE_U + tx; 59 | 60 | let out_w = dimension.size.x; 61 | let out_h = dimension.size.y; 62 | 63 | if (row >= out_h - 1u || col >= out_w - 1u) { 64 | return; 65 | } 66 | 67 | let offset = (row * (out_w - 1u) + col) * 2u; 68 | 69 | // Triangle 1 70 | indices.data[offset][0] = row * out_w + col; 71 | indices.data[offset][1] = (row + 1u) * out_w + col; 72 | indices.data[offset][2] = row * out_w + col + 1u; 73 | 74 | // Triangle 2 75 | indices.data[offset + 1u][0] = row * out_w + col + 1u; 76 | indices.data[offset + 1u][1] = (row + 1u) * out_w + col; 77 | indices.data[offset + 1u][2] = (row + 1u) * out_w + col + 1u; 78 | } 79 | 80 | @group(0) @binding(0) var points: ClothPoints; 81 | 82 | var force: vec3; 83 | var p1: vec3; 84 | var v1: vec3; 85 | 86 | const SQRT2 = 1.4142135623730951; 87 | const EPSIL = 0.0001; 88 | 89 | fn spring_damper(p2: vec4, v2: vec4, rest_length: f32) { 90 | // Empty padded point 91 | if (v2.w < 0.0) { 92 | return; 93 | } 94 | 95 | let delta = p2.xyz - p1; 96 | let len = length(delta); 97 | 98 | if (len < EPSIL) { 99 | return; 100 | } 101 | 102 | let dir = normalize(delta); 103 | 104 | // Spring force 105 | force = force + constants.spring_constant * (len - rest_length) * dir; 106 | 107 | // Damper force 108 | let v_close = dot(v1 - v2.xyz, dir); 109 | force = force - constants.damping_constant * v_close * dir; 110 | } 111 | 112 | const AIR_DENSITY = 1.225; 113 | const DRAG_COEFFI = 1.5; 114 | 115 | fn aerodynamic(p2: ClothPointShared, p3: ClothPointShared) { 116 | // Empty padded points 117 | if (p2.v.w < 0.0 || p3.v.w < 0.0) { 118 | return; 119 | } 120 | 121 | let surf_v = (v1 + p2.v.xyz + p3.v.xyz) / 3.0; 122 | let delta_v = surf_v - vectors.wind; 123 | let len = length(delta_v); 124 | 125 | if (len < EPSIL) { 126 | return; 127 | } 128 | 129 | let dir = normalize(delta_v); 130 | 131 | let prod = cross(p2.p.xyz - p1, p3.p.xyz - p1); 132 | 133 | if (length(prod) < EPSIL) { 134 | return; 135 | } 136 | 137 | let norm = normalize(prod); 138 | let area = length(prod) / 2.0 * dot(norm, dir); 139 | 140 | force = force + -0.5 * AIR_DENSITY * len * len * DRAG_COEFFI * area * norm / 3.9; 141 | } 142 | 143 | var tile : array, 16>; 144 | 145 | @compute @workgroup_size(TILE_SIZE, TILE_SIZE, 1) 146 | fn calc_forces( 147 | @builtin(workgroup_id) blockIdx : vec3, 148 | @builtin(local_invocation_id) threadIdx : vec3 149 | ) { 150 | let tx = threadIdx.x; 151 | let ty = threadIdx.y; 152 | 153 | let row_o = i32(blockIdx.y * INNER_TILE + ty); 154 | let col_o = i32(blockIdx.x * INNER_TILE + tx); 155 | 156 | // Could be -1 so it's all casted to signed 157 | let row_i = i32(row_o) - 1; 158 | let col_i = i32(col_o) - 1; 159 | 160 | let out_w = i32(dimension.size.x); 161 | let out_h = i32(dimension.size.y); 162 | 163 | // Load tile 164 | if (row_i >= 0 && row_i < out_h && 165 | col_i >= 0 && col_i < out_w) { 166 | tile[ty][tx].p = points.data[row_i * out_w + col_i].position; 167 | tile[ty][tx].v = points.data[row_i * out_w + col_i].velocity; 168 | } else { 169 | tile[ty][tx].p = vec4(0.0, 0.0, 0.0, -1.0); 170 | tile[ty][tx].v = vec4(0.0, 0.0, 0.0, -1.0); 171 | } 172 | 173 | workgroupBarrier(); 174 | 175 | let cx = tx + 1u; 176 | let cy = ty + 1u; 177 | 178 | // Out of grid || out of tile || fixed point 179 | if (row_o >= out_h || col_o >= out_w || 180 | tx >= INNER_TILE || ty >= INNER_TILE || 181 | tile[cy][cx].p.w < 0.0) { 182 | return; 183 | } 184 | 185 | force = vectors.gravity * constants.mass; 186 | 187 | p1 = tile[cy][cx].p.xyz; 188 | v1 = tile[cy][cx].v.xyz; 189 | 190 | let rest_len = constants.rest_length; 191 | let diag_len = rest_len * SQRT2; 192 | 193 | // 8x spring damper force accumulation 194 | spring_damper(tile[cy - 1u][cx - 1u].p, tile[cy - 1u][cx - 1u].v, diag_len); 195 | spring_damper(tile[cy - 1u][cx - 0u].p, tile[cy - 1u][cx - 0u].v, rest_len); 196 | spring_damper(tile[cy - 1u][cx + 1u].p, tile[cy - 1u][cx + 1u].v, diag_len); 197 | 198 | spring_damper(tile[cy][cx - 1u].p, tile[cy][cx - 1u].v, rest_len); 199 | spring_damper(tile[cy][cx + 1u].p, tile[cy][cx + 1u].v, rest_len); 200 | 201 | spring_damper(tile[cy + 1u][cx - 1u].p, tile[cy + 1u][cx - 1u].v, diag_len); 202 | spring_damper(tile[cy + 1u][cx - 0u].p, tile[cy + 1u][cx - 0u].v, rest_len); 203 | spring_damper(tile[cy + 1u][cx + 1u].p, tile[cy + 1u][cx + 1u].v, diag_len); 204 | 205 | // 8 Triangles aerodynamic force accumulation 206 | aerodynamic(tile[cy - 1u][cx - 1u], tile[cy - 1u][cx - 0u]); 207 | aerodynamic(tile[cy - 1u][cx - 0u], tile[cy - 1u][cx + 1u]); 208 | aerodynamic(tile[cy - 1u][cx + 1u], tile[cy - 0u][cx + 1u]); 209 | aerodynamic(tile[cy - 0u][cx + 1u], tile[cy + 1u][cx + 1u]); 210 | aerodynamic(tile[cy + 1u][cx + 1u], tile[cy + 1u][cx + 0u]); 211 | aerodynamic(tile[cy + 1u][cx + 0u], tile[cy + 1u][cx - 1u]); 212 | aerodynamic(tile[cy + 1u][cx - 1u], tile[cy + 0u][cx - 1u]); 213 | aerodynamic(tile[cy + 0u][cx - 1u], tile[cy - 1u][cx - 1u]); 214 | 215 | points.data[row_o * out_w + col_o].force = vec4(force, 0.0); 216 | } 217 | 218 | var accum_norm: vec3; 219 | 220 | fn triangle_normal(p2: vec4, p3: vec4) { 221 | if (p2.w < 0.0 || p3.w < 0.0) { 222 | return; 223 | } 224 | 225 | let prod = cross(p2.xyz - p1, p3.xyz - p1); 226 | 227 | if (length(prod) < EPSIL) { 228 | return; 229 | } 230 | 231 | let norm = normalize(prod); 232 | 233 | accum_norm = accum_norm + norm; 234 | } 235 | 236 | var p_tile : array, 16>, 16>; 237 | 238 | @compute @workgroup_size(TILE_SIZE, TILE_SIZE, 1) 239 | fn calc_normal( 240 | @builtin(workgroup_id) blockIdx : vec3, 241 | @builtin(local_invocation_id) threadIdx : vec3 242 | ) { 243 | let tx = threadIdx.x; 244 | let ty = threadIdx.y; 245 | 246 | let row_o = i32(blockIdx.y * INNER_TILE + ty); 247 | let col_o = i32(blockIdx.x * INNER_TILE + tx); 248 | 249 | // Could be -1 so it's all casted to signed 250 | let row_i = i32(row_o) - 1; 251 | let col_i = i32(col_o) - 1; 252 | 253 | let out_w = i32(dimension.size.x); 254 | let out_h = i32(dimension.size.y); 255 | 256 | // Load position tile 257 | if (row_i >= 0 && row_i < out_h && 258 | col_i >= 0 && col_i < out_w) { 259 | p_tile[ty][tx] = points.data[row_i * out_w + col_i].position; 260 | } else { 261 | p_tile[ty][tx] = vec4(0.0, 0.0, 0.0, -1.0); 262 | } 263 | 264 | workgroupBarrier(); 265 | 266 | let cx = tx + 1u; 267 | let cy = ty + 1u; 268 | 269 | // Out of grid || out of tile 270 | if (row_o >= out_h || col_o >= out_w || 271 | tx >= INNER_TILE || ty >= INNER_TILE) { 272 | return; 273 | } 274 | 275 | p1 = p_tile[cy][cx].xyz; 276 | 277 | accum_norm = vec3(0.0); 278 | 279 | // 8 Triangles normal accumulation 280 | triangle_normal(p_tile[cy - 1u][cx - 1u], p_tile[cy - 1u][cx - 0u]); 281 | triangle_normal(p_tile[cy - 1u][cx - 0u], p_tile[cy - 1u][cx + 1u]); 282 | triangle_normal(p_tile[cy - 1u][cx + 1u], p_tile[cy - 0u][cx + 1u]); 283 | triangle_normal(p_tile[cy - 0u][cx + 1u], p_tile[cy + 1u][cx + 1u]); 284 | triangle_normal(p_tile[cy + 1u][cx + 1u], p_tile[cy + 1u][cx + 0u]); 285 | triangle_normal(p_tile[cy + 1u][cx + 0u], p_tile[cy + 1u][cx - 1u]); 286 | triangle_normal(p_tile[cy + 1u][cx - 1u], p_tile[cy + 0u][cx - 1u]); 287 | triangle_normal(p_tile[cy + 0u][cx - 1u], p_tile[cy - 1u][cx - 1u]); 288 | 289 | // 4 Triangle normal accumulation 290 | // triangle_normal(p_tile[cy - 1u][cx], p_tile[cy][cx - 1u]); 291 | // triangle_normal(p_tile[cy][cx - 1u], p_tile[cy + 1u][cx]); 292 | // triangle_normal(p_tile[cy + 1u][cx], p_tile[cy][cx + 1u]); 293 | // triangle_normal(p_tile[cy][cx + 1u], p_tile[cy - 1u][cx]); 294 | 295 | accum_norm = accum_norm; 296 | 297 | if (length(accum_norm) < EPSIL) { 298 | return; 299 | } 300 | 301 | let norm = normalize(accum_norm); 302 | points.data[row_o * out_w + col_o].normal = vec4(norm, 0.0); 303 | points.data[row_o * out_w + col_o].force = points.data[row_o * out_w + col_o].position + vec4(norm, 0.0); 304 | } 305 | 306 | struct DT { 307 | value: f32, 308 | }; 309 | 310 | @group(2) @binding(0) var dt: DT; 311 | 312 | @compute @workgroup_size(256) 313 | fn update( 314 | @builtin(workgroup_id) blockIdx : vec3, 315 | @builtin(local_invocation_id) threadIdx : vec3 316 | ) { 317 | let offset = i32(blockIdx.x * 256u + threadIdx.x); 318 | 319 | let out_w = i32(dimension.size.x); 320 | let out_h = i32(dimension.size.y); 321 | 322 | if (offset >= out_w * out_w) { 323 | return; 324 | } 325 | 326 | let p = points.data[offset]; 327 | 328 | if (p.position.w < 0.0) { 329 | points.data[offset].velocity = vec4(0.0); 330 | // Not necessary but... 331 | points.data[offset].force = vec4(0.0); 332 | return; 333 | } 334 | 335 | let a = p.force / constants.mass; 336 | points.data[offset].velocity = p.velocity + a * dt.value; 337 | let pos = p.position + p.velocity * dt.value; 338 | points.data[offset].position = vec4(pos.x, max(pos.y, constants.floor_y), pos.zw); 339 | } -------------------------------------------------------------------------------- /src/engine/shaders/deferred.frag.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var gBufferPosition: texture_2d; 2 | @group(0) @binding(1) var gBufferNormal: texture_2d; 3 | @group(0) @binding(2) var gBufferAlbedo: texture_2d; 4 | 5 | struct LightData { 6 | position : vec4, 7 | color : vec3, 8 | radius : f32, 9 | }; 10 | 11 | struct LightsBuffer { 12 | lights: array, 13 | }; 14 | @group(1) @binding(0) var lightsBuffer: LightsBuffer; 15 | 16 | struct Config { 17 | numLights : u32, 18 | }; 19 | @group(1) @binding(1) var config: Config; 20 | 21 | struct CanvasConstants { 22 | size: vec2, 23 | }; 24 | @group(2) @binding(0) var canvas : CanvasConstants; 25 | 26 | @fragment 27 | fn main(@builtin(position) coord : vec4) 28 | -> @location(0) vec4 { 29 | 30 | var result = vec3(0.0, 0.0, 0.0); 31 | 32 | let position = textureLoad( 33 | gBufferPosition, 34 | vec2(floor(coord.xy)), 35 | 0 36 | ); 37 | 38 | if (position.w > 10000.0) { 39 | discard; 40 | } 41 | 42 | let normal = textureLoad( 43 | gBufferNormal, 44 | vec2(floor(coord.xy)), 45 | 0 46 | ).xyz; 47 | 48 | let albedo = textureLoad( 49 | gBufferAlbedo, 50 | vec2(floor(coord.xy)), 51 | 0 52 | ).rgb; 53 | 54 | for (var i : u32 = 0u; i < config.numLights; i = i + 1u) { 55 | let dist = lightsBuffer.lights[i].position.xyz - position.xyz; 56 | let distance = length(dist); 57 | if (distance > lightsBuffer.lights[i].radius) { 58 | continue; 59 | } 60 | let lambert = max(dot(normal, normalize(dist)), 0.0); 61 | result = result + vec3( 62 | lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightsBuffer.lights[i].color * albedo); 63 | } 64 | 65 | // some manual ambient 66 | result = result + vec3(0.2, 0.2, 0.2); 67 | 68 | return vec4(result, 1.0); 69 | 70 | // return vec4(((normal + vec3(1.0)) / 2.0), 1.0); 71 | // return vec4(((position.xyz) + 0.5), 1.0); 72 | } 73 | -------------------------------------------------------------------------------- /src/engine/shaders/gbuffer.frag.wgsl: -------------------------------------------------------------------------------- 1 | struct GBufferOutput { 2 | @location(0) position : vec4, 3 | @location(1) normal : vec4, 4 | 5 | // Textures: diffuse color, specular color, smoothness, emissive etc. could go here 6 | @location(2) albedo : vec4, 7 | }; 8 | 9 | @fragment 10 | fn main(@location(0) fragPosition: vec3, 11 | @location(1) fragNormal: vec3, 12 | @location(2) fragUV : vec2) -> GBufferOutput { 13 | var output : GBufferOutput; 14 | 15 | output.position = vec4(fragPosition, 1.0); 16 | output.normal = vec4(fragNormal, 1.0); 17 | 18 | // Faking some kind of checkerboard texture 19 | // let uv = floor(30.0 * fragUV); 20 | // let c = 0.5 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0)); 21 | let c = 0.5; 22 | output.albedo = vec4(c, c, c, 1.0); 23 | return output; 24 | } -------------------------------------------------------------------------------- /src/engine/shaders/gbuffer.vert.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelMatrix : mat4x4, 3 | normalModelMatrix : mat4x4, 4 | }; 5 | 6 | struct Camera { 7 | viewProjectionMatrix : mat4x4, 8 | }; 9 | 10 | @group(0) @binding(0) var uniforms : Uniforms; 11 | @group(1) @binding(0) var camera : Camera; 12 | 13 | struct VertexOutput { 14 | @builtin(position) Position : vec4, 15 | @location(0) worldPos: vec3, // position in world space 16 | @location(1) worldNorm: vec3, // normal in world space 17 | @location(2) fragUV: vec2, 18 | }; 19 | 20 | @vertex 21 | fn main(@location(0) position : vec3, 22 | @location(1) normal : vec3, 23 | @location(2) uv : vec2) -> VertexOutput { 24 | var output : VertexOutput; 25 | output.worldPos = (uniforms.modelMatrix * vec4(position, 1.0)).xyz; 26 | output.Position = camera.viewProjectionMatrix * vec4(output.worldPos, 1.0); 27 | output.worldNorm = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz); 28 | output.fragUV = uv; 29 | return output; 30 | } -------------------------------------------------------------------------------- /src/engine/shaders/particles.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelMatrix : mat4x4, 3 | normalModelMatrix : mat4x4, 4 | }; 5 | 6 | struct Camera { 7 | viewProjectionMatrix : mat4x4, 8 | eyePos : vec3, 9 | }; 10 | 11 | struct Params { 12 | radius : f32, 13 | num : u32, 14 | spawn : u32, 15 | gravity : f32, 16 | air_den : f32, 17 | drag : f32, 18 | elas : f32, 19 | fric : f32, 20 | wind : vec3, 21 | }; 22 | 23 | struct RenderParticle { 24 | position: vec3, 25 | velocity: vec3, 26 | }; 27 | 28 | struct RenderParticles { 29 | data: array, 30 | }; 31 | 32 | struct Indices { 33 | data: array, 34 | }; 35 | 36 | @group(0) @binding(0) var uniforms : Uniforms; 37 | @group(1) @binding(0) var camera : Camera; 38 | @group(2) @binding(0) var params : Params; 39 | @group(2) @binding(1) var r_particles : RenderParticles; 40 | @group(2) @binding(2) var indices : Indices; 41 | 42 | struct VertexOutput { 43 | @builtin(position) Position : vec4, 44 | @location(0) sphere_center : vec3, // sphere center in world space 45 | @location(1) ray_dir : vec3, // ray direction 46 | }; 47 | 48 | @vertex 49 | fn vert_main( 50 | @builtin(vertex_index) VertexIndex : u32, 51 | @builtin(instance_index) InstanceIndex : u32) -> VertexOutput { 52 | 53 | var cube_pos = array, 36>( 54 | vec3(-0.5, 0.5, 0.5), 55 | vec3(-0.5, -0.5, 0.5), 56 | vec3(0.5, -0.5, 0.5), 57 | vec3(-0.5, 0.5, 0.5), 58 | vec3(0.5, -0.5, 0.5), 59 | vec3(0.5, 0.5, 0.5), 60 | vec3(0.5, 0.5, 0.5), 61 | vec3(0.5, -0.5, 0.5), 62 | vec3(0.5, -0.5, -0.5), 63 | vec3(0.5, 0.5, 0.5), 64 | vec3(0.5, -0.5, -0.5), 65 | vec3(0.5, 0.5, -0.5), 66 | vec3(0.5, -0.5, 0.5), 67 | vec3(-0.5, -0.5, 0.5), 68 | vec3(-0.5, -0.5, -0.5), 69 | vec3(0.5, -0.5, 0.5), 70 | vec3(-0.5, -0.5, -0.5), 71 | vec3(0.5, -0.5, -0.5), 72 | vec3(0.5, 0.5, -0.5), 73 | vec3(-0.5, 0.5, -0.5), 74 | vec3(-0.5, 0.5, 0.5), 75 | vec3(0.5, 0.5, -0.5), 76 | vec3(-0.5, 0.5, 0.5), 77 | vec3(0.5, 0.5, 0.5), 78 | vec3(-0.5, -0.5, -0.5), 79 | vec3(-0.5, 0.5, -0.5), 80 | vec3(0.5, 0.5, -0.5), 81 | vec3(-0.5, -0.5, -0.5), 82 | vec3(0.5, 0.5, -0.5), 83 | vec3(0.5, -0.5, -0.5), 84 | vec3(-0.5, 0.5, -0.5), 85 | vec3(-0.5, -0.5, -0.5), 86 | vec3(-0.5, -0.5, 0.5), 87 | vec3(-0.5, 0.5, -0.5), 88 | vec3(-0.5, -0.5, 0.5), 89 | vec3(-0.5, 0.5, 0.5) 90 | ); 91 | 92 | let p = r_particles.data[indices.data[InstanceIndex]].position; 93 | let center = vec3(p[0], p[1], p[2]); 94 | let world_pos = (uniforms.modelMatrix * vec4(center + cube_pos[VertexIndex] * params.radius * 2.0, 1.0)).xyz; 95 | 96 | var output : VertexOutput; 97 | output.sphere_center = (uniforms.modelMatrix * vec4(center, 1.0)).xyz; 98 | output.ray_dir = world_pos - camera.eyePos; 99 | output.Position = camera.viewProjectionMatrix * vec4(world_pos, 1.0); 100 | 101 | return output; 102 | } 103 | 104 | struct GBufferOutput { 105 | @builtin(frag_depth) depth : f32, 106 | @location(0) position : vec4, 107 | @location(1) normal : vec4, 108 | // Textures: diffuse color, specular color, smoothness, emissive etc. could go here 109 | @location(2) albedo : vec4, 110 | }; 111 | 112 | fn sphIntersect(ro: vec3, rd: vec3, sph: vec4) -> f32 { 113 | let oc = ro - sph.xyz; 114 | let b = dot(oc, rd); 115 | let c = dot(oc, oc) - sph.w * sph.w; 116 | let h = b * b - c; 117 | 118 | return select(- b - sqrt(h), 0.0, h < 0.0); 119 | } 120 | 121 | @fragment 122 | fn frag_main( 123 | @location(0) sphere_center: vec3, 124 | @location(1) ray_dir: vec3) -> GBufferOutput { 125 | 126 | let rd = normalize(ray_dir); 127 | let hit = sphIntersect(camera.eyePos, rd, vec4(sphere_center, params.radius)); 128 | 129 | var output : GBufferOutput; 130 | 131 | if (hit <= 0.0) { 132 | discard; 133 | } 134 | 135 | let world_pos = rd * hit + camera.eyePos; 136 | 137 | output.position = vec4(world_pos, 1.0); 138 | output.normal = vec4(normalize(world_pos - sphere_center), 1.0); 139 | 140 | // TODO 141 | let c = 0.5; 142 | output.albedo = vec4(c, c, c, 1.0); 143 | 144 | let clipPos = camera.viewProjectionMatrix * vec4(world_pos, 1.0); 145 | output.depth = clipPos.z / clipPos.w; 146 | 147 | return output; 148 | } 149 | 150 | struct StageParticle { 151 | position: vec4, 152 | velocity: vec3, 153 | }; 154 | 155 | struct StageParticles { 156 | data: array, 157 | }; 158 | 159 | struct DT { 160 | value: f32, 161 | }; 162 | 163 | @group(2) @binding(1) var w_particles : RenderParticles; 164 | @group(2) @binding(3) var stage : StageParticles; 165 | 166 | @compute @workgroup_size(256) 167 | fn spawn( 168 | @builtin(workgroup_id) blockIdx : vec3, 169 | @builtin(local_invocation_id) threadIdx : vec3 170 | ) { 171 | let index = blockIdx.x * 256u + threadIdx.x; 172 | if (index >= params.spawn) { 173 | return; 174 | } 175 | 176 | let particle = stage.data[index]; 177 | let write_index = u32(particle.position.w); 178 | w_particles.data[write_index].position = particle.position.xyz; 179 | w_particles.data[write_index].velocity = particle.velocity; 180 | } 181 | 182 | @group(3) @binding(0) var dt : DT; 183 | 184 | const pi = 3.1415926; 185 | const epsi = 0.0001; 186 | 187 | @compute @workgroup_size(256) 188 | fn update( 189 | @builtin(workgroup_id) blockIdx : vec3, 190 | @builtin(local_invocation_id) threadIdx : vec3 191 | ) { 192 | let index = blockIdx.x * 256u + threadIdx.x; 193 | if (index >= params.num) { 194 | return; 195 | } 196 | 197 | let i = indices.data[index]; 198 | let p = w_particles.data[i]; 199 | 200 | // mass = 1 201 | var force = vec3(0.0, params.gravity, 0.0); 202 | var v = p.velocity; 203 | let v_close = v - params.wind; 204 | force = force - 0.5 * params.air_den * length(v_close) * v_close * 205 | params.drag * pi * params.radius * params.radius; 206 | 207 | let pos = p.position + v * dt.value; 208 | 209 | // Ground collision 210 | if (pos.y < 0.0) { 211 | let vy = v.y; 212 | v = vec3(v.x, 0.0, v.z); 213 | var vlen = length(v); 214 | if (vlen > epsi) { 215 | v = v / vlen; 216 | let e = abs((1.0 + params.elas) * vy); 217 | let vlen = max(0.0, vlen - params.fric * e); 218 | v = v * vlen; 219 | } 220 | v = vec3(v.x, abs(vy) * params.elas, v.z); 221 | } 222 | 223 | w_particles.data[i].velocity = v + force * dt.value; 224 | w_particles.data[i].position = vec3(pos.x, max(pos.y, 0.0), pos.z); 225 | } -------------------------------------------------------------------------------- /src/engine/shaders/quad.vert.wgsl: -------------------------------------------------------------------------------- 1 | @vertex 2 | fn main(@builtin(vertex_index) VertexIndex : u32) 3 | -> @builtin(position) vec4 { 4 | var pos = array, 6>( 5 | vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), 6 | vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0)); 7 | 8 | return vec4(pos[VertexIndex], 0.0, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /src/engine/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface EngineParam { 2 | canvas: HTMLCanvasElement | OffscreenCanvas; 3 | screen: { 4 | width: number; 5 | height: number; 6 | }; 7 | dpr: number; 8 | } 9 | 10 | declare global { 11 | interface Window { 12 | require: typeof require; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/engine/ui/style/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | position: absolute !important; 6 | top: 0px; 7 | left: 0px; 8 | width: 100%; 9 | height: 100%; 10 | margin: 0px; 11 | overflow: hidden; 12 | user-select: none; 13 | scroll-behavior: smooth; 14 | font-family: sans-serif; 15 | } 16 | 17 | #webgpu { 18 | position: fixed; 19 | width: 100%; 20 | height: 100%; 21 | min-width: 100vw; 22 | max-width: 100%; 23 | min-height: 100vh; 24 | height: auto; 25 | left: 50%; 26 | top: 50%; 27 | transform: translate(-50%, -50%); 28 | object-fit: cover; 29 | z-index: -1; 30 | } 31 | 32 | #app > div { 33 | position: fixed; 34 | bottom: 0px; 35 | justify-content: center; 36 | display: flex; 37 | width: 100vw; 38 | } 39 | 40 | :root { 41 | --theme: #04e4e4; 42 | } 43 | 44 | #app > div > a { 45 | margin-left: 10px; 46 | margin-bottom: 5px; 47 | color: var(--theme); 48 | border: 2px solid var(--theme); 49 | border-radius: 2px; 50 | padding: 12px; 51 | font-size: 16px; 52 | font-family: sans-serif; 53 | letter-spacing: 5px; 54 | cursor: pointer; 55 | font-weight: bold; 56 | filter: drop-shadow(0 0 3px var(--theme)) drop-shadow(0 0 3px var(--theme)) 57 | contrast(2) brightness(2); 58 | transition: 0.5s; 59 | text-decoration: none; 60 | } 61 | 62 | #app > div > a:hover { 63 | color: black; 64 | background-color: var(--theme); 65 | filter: drop-shadow(0 0 5px var(--theme)) contrast(2) brightness(2); 66 | } 67 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import Engine from './engine/core/engine'; 4 | import { EngineParam } from './engine/types'; 5 | 6 | import './engine/ui/style/index.css'; 7 | 8 | const App = () => { 9 | useEffect(() => { 10 | // const worker = new Worker(new URL('./engine/worker.ts', import.meta.url)); 11 | 12 | // worker.addEventListener('message', e => { 13 | // console.log(e.data); 14 | // }); 15 | 16 | const canvas = document.getElementById('webgpu') as HTMLCanvasElement; 17 | // const offscreen = canvas.transferControlToOffscreen(); 18 | 19 | const params: EngineParam = { 20 | canvas, 21 | screen: { 22 | width: screen.width, 23 | height: screen.height, 24 | }, 25 | dpr: window.devicePixelRatio, 26 | }; 27 | 28 | const query = window.location.search; 29 | const engine = new Engine(params); 30 | engine.init().then(() => { 31 | if (query === '?cloth') engine.renderer.setupCloth(); 32 | else if (query === '?particles') engine.renderer.setupParticles(); 33 | else engine.renderer.setupCubes(); 34 | // engine.renderer.setupParticles(); 35 | }); 36 | 37 | // worker.postMessage(data); 38 | // return () => worker.terminate(); 39 | }, []); 40 | 41 | const base = `${location.origin}/${location.pathname}`.replace(/\/\/$/, ''); 42 | 43 | return ( 44 |
45 | Cubes 46 | Cloth 47 | Particles 48 |
49 | ); 50 | }; 51 | 52 | const root = createRoot(document.getElementById('app')); 53 | root.render(); 54 | -------------------------------------------------------------------------------- /src/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebGPU Test 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": ".", 5 | "target": "ES2020", 6 | "lib": ["dom", "esnext", "ES2020.Promise"], 7 | "noImplicitReturns": true, 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "module": "es2020", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "downlevelIteration": true 22 | }, 23 | "include": ["src/**/*", "node_modules/@webgpu/types/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = (_, argv) => ({ 5 | entry: './src/index.tsx', 6 | target: 'web', 7 | output: { 8 | filename: 'index.js', 9 | path: path.resolve(__dirname, 'build'), 10 | }, 11 | resolve: { 12 | extensions: ['.tsx', '.ts', '.js'], 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | exclude: /(node_modules|bower_components)/, 19 | use: { 20 | loader: 'swc-loader', 21 | options: { 22 | jsc: { 23 | parser: { 24 | syntax: 'typescript', 25 | tsx: true, 26 | }, 27 | transform: { 28 | react: { 29 | pragma: 'React.createElement', 30 | pragmaFrag: 'React.Fragment', 31 | throwIfNamespace: true, 32 | development: false, 33 | useBuiltins: false, 34 | runtime: 'automatic', 35 | }, 36 | }, 37 | target: 'es2020', 38 | minify: 39 | argv.mode === 'production' 40 | ? { compress: true, mangle: true } 41 | : undefined, 42 | }, 43 | }, 44 | }, 45 | }, 46 | { 47 | test: /\.css$/i, 48 | use: ['style-loader', 'css-loader'], 49 | }, 50 | { 51 | test: /\.svg$/, 52 | use: ['@svgr/webpack'], 53 | }, 54 | { 55 | test: /\.(ico|svg|png|jpg|gif|webp|mp4)$/i, 56 | use: [ 57 | { 58 | loader: 'url-loader', 59 | options: { 60 | limit: 8192, 61 | }, 62 | }, 63 | ], 64 | }, 65 | { 66 | test: /\.wgsl$/, 67 | type: 'asset/source', 68 | }, 69 | ], 70 | }, 71 | plugins: [ 72 | new HtmlWebpackPlugin({ 73 | template: path.resolve(__dirname, 'src', 'template', 'index.html'), 74 | filename: 'index.html', 75 | }), 76 | ], 77 | }); 78 | --------------------------------------------------------------------------------