├── dist ├── 1.x │ ├── test.d.ts │ ├── utils.d.ts │ ├── webgpu-utils.d.ts │ ├── format-info.d.ts │ ├── typed-arrays.d.ts │ ├── generate-mipmap.d.ts │ ├── wgsl-types.d.ts │ ├── texture-utils.d.ts │ └── data-definitions.d.ts └── 0.x │ ├── utils.d.ts │ ├── webgpu-utils.d.ts │ ├── typed-arrays.d.ts │ ├── generate-mipmap.d.ts │ ├── data-definitions.d.ts │ ├── texture-utils.d.ts │ ├── buffer-views.d.ts │ ├── attribute-utils.d.ts │ └── primitives.d.ts ├── .eslintignore ├── examples ├── images │ ├── array │ │ ├── 手拭.jpg │ │ ├── 竹輪.jpg │ │ ├── meat.jpg │ │ ├── tif.jpg │ │ ├── 肉寿司.jpg │ │ ├── biggrub.jpg │ │ ├── curtain.jpg │ │ ├── mascot.jpg │ │ ├── scomp.jpg │ │ ├── balloons.jpg │ │ ├── hamburger.jpg │ │ └── orange-fruit.jpg │ ├── clover.jpg │ ├── tokufuji.jpg │ ├── goldengate.jpg │ ├── hft-icon-16.png │ ├── yokohama │ │ ├── negx.jpg │ │ ├── negy.jpg │ │ ├── negz.jpg │ │ ├── posx.jpg │ │ ├── posy.jpg │ │ ├── posz.jpg │ │ └── readme.txt │ ├── niagarafalls2s │ │ ├── negx.jpg │ │ ├── negy.jpg │ │ ├── negz.jpg │ │ ├── posx.jpg │ │ ├── posy.jpg │ │ ├── posz.jpg │ │ └── readme.txt │ ├── color-adjustments │ │ ├── sepia.png │ │ ├── sunset.png │ │ ├── posterize.png │ │ ├── orange-to-green.png │ │ └── blacklight-poster.png │ ├── little_paris_under_tower │ │ ├── nx.jpeg │ │ ├── ny.jpeg │ │ ├── nz.jpeg │ │ ├── px.jpeg │ │ ├── py.jpeg │ │ ├── pz.jpeg │ │ └── little_paris_under_tower.md │ ├── 1024px-Mexican_Talavera_texture.png │ ├── 1024px-Mexican_Talavera_texture.txt │ ├── Baby_blue_aqua_mosaic_swimming_pool_square_seamless_tiled_floor_texture.jpg │ ├── Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.jpg │ ├── Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.txt │ └── Baby_blue_aqua_mosaic_swimming_pool_square_seamless_tiled_floor_texture.txt ├── background.html ├── stencil.html ├── stencil-cube.html ├── cube.html ├── cube-map.html ├── wireframe.html ├── 2d-array.html ├── instancing.html ├── primitives.html ├── bind-group-layouts.html ├── examples.css ├── instancing-size-only.html ├── reverse-z.html ├── cube-map.js ├── cube.js ├── bind-group-layouts.js ├── instancing.js ├── primitives.js └── instancing-size-only.js ├── resources ├── images │ ├── twgljs.png │ └── twgljs-icon.png ├── js │ └── index.js └── css │ └── base.css ├── .vscode ├── settings.json └── launch.json ├── jsr.json ├── test ├── tests │ ├── shaders │ │ └── example-01.wgsl.js │ ├── gpu-info.js │ └── generate-mipmap-test.js ├── mocha-support.js ├── index.js ├── src │ └── js │ │ └── example-inject.js ├── index.html ├── puppeteer.js ├── webgpu.js └── assert.js ├── src ├── webgpu-utils.ts ├── utils.ts ├── typed-arrays.ts ├── format-info.ts ├── wgsl-types.ts └── generate-mipmap.ts ├── typedoc.json ├── migration.md ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── test.yml │ └── build-and-deploy.yml ├── LICENSE.md ├── TODO.md ├── rollup.config.js ├── package.json ├── CHANGELIST.md ├── .eslintrc.cjs └── test.html /dist/1.x/test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | src/3rdParty/**/* 3 | examples/*.html 4 | test/mocha* 5 | -------------------------------------------------------------------------------- /examples/images/array/手拭.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/手拭.jpg -------------------------------------------------------------------------------- /examples/images/array/竹輪.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/竹輪.jpg -------------------------------------------------------------------------------- /examples/images/clover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/clover.jpg -------------------------------------------------------------------------------- /examples/images/tokufuji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/tokufuji.jpg -------------------------------------------------------------------------------- /resources/images/twgljs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/resources/images/twgljs.png -------------------------------------------------------------------------------- /examples/images/array/meat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/meat.jpg -------------------------------------------------------------------------------- /examples/images/array/tif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/tif.jpg -------------------------------------------------------------------------------- /examples/images/array/肉寿司.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/肉寿司.jpg -------------------------------------------------------------------------------- /examples/images/goldengate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/goldengate.jpg -------------------------------------------------------------------------------- /examples/images/array/biggrub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/biggrub.jpg -------------------------------------------------------------------------------- /examples/images/array/curtain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/curtain.jpg -------------------------------------------------------------------------------- /examples/images/array/mascot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/mascot.jpg -------------------------------------------------------------------------------- /examples/images/array/scomp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/scomp.jpg -------------------------------------------------------------------------------- /examples/images/hft-icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/hft-icon-16.png -------------------------------------------------------------------------------- /examples/images/yokohama/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/negx.jpg -------------------------------------------------------------------------------- /examples/images/yokohama/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/negy.jpg -------------------------------------------------------------------------------- /examples/images/yokohama/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/negz.jpg -------------------------------------------------------------------------------- /examples/images/yokohama/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/posx.jpg -------------------------------------------------------------------------------- /examples/images/yokohama/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/posy.jpg -------------------------------------------------------------------------------- /examples/images/yokohama/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/yokohama/posz.jpg -------------------------------------------------------------------------------- /resources/images/twgljs-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/resources/images/twgljs-icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "multisampled", 4 | "unfilterable", 5 | "unpadded" 6 | ] 7 | } -------------------------------------------------------------------------------- /examples/images/array/balloons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/balloons.jpg -------------------------------------------------------------------------------- /examples/images/array/hamburger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/hamburger.jpg -------------------------------------------------------------------------------- /examples/images/array/orange-fruit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/array/orange-fruit.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/negx.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/negy.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/negz.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/posx.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/posy.jpg -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/niagarafalls2s/posz.jpg -------------------------------------------------------------------------------- /examples/images/color-adjustments/sepia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/color-adjustments/sepia.png -------------------------------------------------------------------------------- /examples/images/color-adjustments/sunset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/color-adjustments/sunset.png -------------------------------------------------------------------------------- /examples/images/color-adjustments/posterize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/color-adjustments/posterize.png -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/nx.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/nx.jpeg -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/ny.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/ny.jpeg -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/nz.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/nz.jpeg -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/px.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/px.jpeg -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/py.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/py.jpeg -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/pz.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/little_paris_under_tower/pz.jpeg -------------------------------------------------------------------------------- /examples/images/1024px-Mexican_Talavera_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/1024px-Mexican_Talavera_texture.png -------------------------------------------------------------------------------- /examples/images/color-adjustments/orange-to-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/color-adjustments/orange-to-green.png -------------------------------------------------------------------------------- /examples/images/color-adjustments/blacklight-poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/color-adjustments/blacklight-poster.png -------------------------------------------------------------------------------- /examples/images/little_paris_under_tower/little_paris_under_tower.md: -------------------------------------------------------------------------------- 1 | from: https://polyhaven.com/a/little_paris_under_tower 2 | license: CC0 3 | photographer: Dimitrios Savva, https://polyhaven.com/all?a=Dimitrios%20Savva -------------------------------------------------------------------------------- /examples/images/1024px-Mexican_Talavera_texture.txt: -------------------------------------------------------------------------------- 1 | Timdwilliamson, CC BY-SA 4.0 , via Wikimedia Commons 2 | 3 | https://commons.wikimedia.org/wiki/File:Mexican_Talavera_texture.png -------------------------------------------------------------------------------- /examples/images/Baby_blue_aqua_mosaic_swimming_pool_square_seamless_tiled_floor_texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/Baby_blue_aqua_mosaic_swimming_pool_square_seamless_tiled_floor_texture.jpg -------------------------------------------------------------------------------- /examples/images/Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/webgpu-utils/HEAD/examples/images/Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.jpg -------------------------------------------------------------------------------- /examples/images/Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.txt: -------------------------------------------------------------------------------- 1 | Sisters.seamless, CC0, via Wikimedia Commons 2 | 3 | https://commons.wikimedia.org/wiki/File:Baby_blue_aqua_swimming_pool_square_seamless_tiled_floor_paving_texture.jpg -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@greggman/webgpu-utils", 3 | "version": "1.8.1", 4 | "exports": "./src/webgpu-utils.ts", 5 | "publish": { 6 | "include": [ 7 | "LICENSE.md", 8 | "README.md", 9 | "src/**/*.ts" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /examples/images/Baby_blue_aqua_mosaic_swimming_pool_square_seamless_tiled_floor_texture.txt: -------------------------------------------------------------------------------- 1 | Timdwilliamson, CC BY-SA 4.0 , via Wikimedia Commons 2 | 3 | https://commons.wikimedia.org/wiki/File:Mexican_Talavera_texture.png -------------------------------------------------------------------------------- /test/tests/shaders/example-01.wgsl.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | alias material_index = u32; 3 | alias color = vec3f; 4 | 5 | struct material { 6 | index: material_type, 7 | diffuse: color, 8 | } 9 | 10 | @group(0) @binding(1) var materials: array; 11 | `; -------------------------------------------------------------------------------- /dist/0.x/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const roundUpToMultipleOf: (v: number, multiple: number) => number; 2 | export declare function keysOf(obj: { 3 | [k in T]: unknown; 4 | }): readonly T[]; 5 | export declare function range(count: number, fn: (i: number) => T): T[]; 6 | -------------------------------------------------------------------------------- /dist/1.x/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const roundUpToMultipleOf: (v: number, multiple: number) => number; 2 | export declare function keysOf(obj: { 3 | [k in T]: unknown; 4 | }): readonly T[]; 5 | export declare function range(count: number, fn: (i: number) => T): T[]; 6 | -------------------------------------------------------------------------------- /src/webgpu-utils.ts: -------------------------------------------------------------------------------- 1 | export * from './buffer-views.js'; 2 | export * from './data-definitions.js'; 3 | export * from './generate-mipmap.js'; 4 | export * from './attribute-utils.js'; 5 | export * from './texture-utils.js'; 6 | export * from './typed-arrays.js'; 7 | export * as primitives from './primitives.js'; 8 | -------------------------------------------------------------------------------- /dist/0.x/webgpu-utils.d.ts: -------------------------------------------------------------------------------- 1 | export * from './buffer-views.js'; 2 | export * from './data-definitions.js'; 3 | export * from './generate-mipmap.js'; 4 | export * from './attribute-utils.js'; 5 | export * from './texture-utils.js'; 6 | export * from './typed-arrays.js'; 7 | export * as primitives from './primitives.js'; 8 | -------------------------------------------------------------------------------- /dist/1.x/webgpu-utils.d.ts: -------------------------------------------------------------------------------- 1 | export * from './buffer-views.js'; 2 | export * from './data-definitions.js'; 3 | export * from './generate-mipmap.js'; 4 | export * from './attribute-utils.js'; 5 | export * from './texture-utils.js'; 6 | export * from './typed-arrays.js'; 7 | export * as primitives from './primitives.js'; 8 | -------------------------------------------------------------------------------- /test/mocha-support.js: -------------------------------------------------------------------------------- 1 | /* global globalThis */ 2 | export const describe = globalThis.describe; 3 | export const it = globalThis.it; 4 | export const before = globalThis.before; 5 | export const after = globalThis.after; 6 | export const beforeEach = globalThis.beforeEach; 7 | export const afterEach = globalThis.afterEach; 8 | 9 | -------------------------------------------------------------------------------- /examples/images/yokohama/readme.txt: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | 7 | 8 | 9 | License 10 | ======= 11 | 12 | This work is licensed under a Creative Commons Attribution 3.0 Unported License. 13 | http://creativecommons.org/licenses/by/3.0/ 14 | -------------------------------------------------------------------------------- /examples/images/niagarafalls2s/readme.txt: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | 7 | 8 | 9 | License 10 | ======= 11 | 12 | This work is licensed under a Creative Commons Attribution 3.0 Unported License. 13 | http://creativecommons.org/licenses/by/3.0/ 14 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "intentionallyNotExported": ["kWGSLTypeInfo"], 3 | "highlightLanguages": [ 4 | "bash", 5 | "console", 6 | "css", 7 | "html", 8 | "javascript", 9 | "json", 10 | "jsonc", 11 | "json5", 12 | "tsx", 13 | "typescript", 14 | "wgsl", 15 | ] 16 | } -------------------------------------------------------------------------------- /dist/1.x/format-info.d.ts: -------------------------------------------------------------------------------- 1 | import { TypedArrayConstructor } from "./typed-arrays.js"; 2 | export type FormatInfo = { 3 | blockWidth: number; 4 | blockHeight: number; 5 | bytesPerBlock: number; 6 | unitsPerElement: number; 7 | Type: TypedArrayConstructor; 8 | }; 9 | export declare function getTextureFormatInfo(format: GPUTextureFormat): FormatInfo; 10 | -------------------------------------------------------------------------------- /migration.md: -------------------------------------------------------------------------------- 1 | # Migration Notes 2 | 3 | ## 0.x -> 1.x 4 | 5 | * primitive functions changed to named parameters 6 | 7 | * old: `createSphereVertices(2, 12)` 8 | * new: `createSphereVertices({radius = 2, subdivisionsAxis = 12})` 9 | 10 | This means you only have to specify what you change where as with 11 | the previous style you had to specify everything up to the parameter 12 | you actually wanted to change. 13 | -------------------------------------------------------------------------------- /examples/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/stencil.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - stencil 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - stencil
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/stencil-cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - stencil cube 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - stencil cube
11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # -- clip-for-deploy-start -- 4 | 5 | /docs 6 | /dist 7 | /screenshots 8 | index.html 9 | 10 | # -- clip-for-deploy-end -- 11 | 12 | # dependencies 13 | /node_modules 14 | /.pnp 15 | .pnp.js 16 | 17 | # testing 18 | /coverage 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /examples/cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - cube 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/cube-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - cube-map 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - cube-map
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/wireframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - wireframe 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - wireframe
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/2d-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - 2d-array 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - 2d-array texture
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/instancing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - instancing 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - instancing
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/primitives.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - primitives 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - primitives
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/bind-group-layouts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - bind group layouts 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - bind group layouts
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/examples.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light dark; 3 | font-family: monospace; 4 | } 5 | 6 | html, body { 7 | margin: 0; 8 | height: 100%; 9 | } 10 | 11 | canvas { 12 | width: 100%; 13 | height: 100%; 14 | display: block; 15 | } 16 | 17 | #b { 18 | position: absolute; 19 | text-align: center; 20 | width: 100%; 21 | top: 1em; 22 | left: 0; 23 | } 24 | 25 | #fail { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | width: 100%; 30 | height: 100%; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | color: red; 35 | } -------------------------------------------------------------------------------- /examples/instancing-size-only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - instancing (size only) 5 | 6 | 7 | 8 | 9 | 10 |
webgpu-utils - instancing (size only)
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global prettyPrint */ 4 | 5 | document.addEventListener("DOMContentLoaded", function() { 6 | Array.prototype.forEach.call(document.querySelectorAll('pre>code'), function(section) { 7 | // Unwrap 8 | const parent = section.parentElement; 9 | while (section.firstChild) { 10 | const child = section.firstChild; 11 | section.removeChild(child); 12 | parent.appendChild(child); 13 | } 14 | parent.removeChild(section); 15 | // Add class 16 | parent.className = "prettyprint showlinemods"; 17 | }); 18 | prettyPrint(); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "NodeNext", 6 | "outDir": "dist/2.x", 7 | "moduleResolution": "NodeNext", 8 | "declaration": true, 9 | "typeRoots": [ 10 | "./node_modules/@webgpu/types", 11 | "./node_modules/@types", 12 | ], 13 | }, 14 | "include": [ 15 | "src/**/*.ts", 16 | "examples/**/*.js", 17 | "test/**/*.js", 18 | "build/**/*.js", 19 | ".eslintrc.cjs", 20 | "*.js", 21 | "test/tests/**/*.html", 22 | ], 23 | "exclude": [ 24 | "examples/3rdparty/**/*.js", 25 | "test/mocha.js", 26 | ], 27 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "debug unit tests in node", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", 15 | "args": [ 16 | "--timeout=0", 17 | "--grep=works with complex alias", 18 | "test/tests" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/tests/gpu-info.js: -------------------------------------------------------------------------------- 1 | import { describe, it } from '../mocha-support.js'; 2 | 3 | function objLikeToObj(objLike) { 4 | const obj = {}; 5 | for (const k in objLike) { 6 | obj[k] = objLike[k]; 7 | } 8 | return obj; 9 | } 10 | 11 | export async function getInfo() { 12 | const adapter = await navigator.gpu.requestAdapter(); 13 | const title = JSON.stringify({ 14 | gpu: objLikeToObj(adapter?.info || await adapter?.requestAdapterInfo() || { webgpuError: 'no info' }), 15 | features: [...adapter.features], 16 | userAgentData: JSON.parse(JSON.stringify(navigator.userAgentData || { userAgentData: 'none' })), 17 | }, null, 2); 18 | 19 | describe('gpu info', () => { 20 | it(title, () => {}); 21 | }); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | - test 8 | pull_request: 9 | jobs: 10 | test: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Checkout 🍔🍟🥤 14 | uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Use Node.js 😂 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | 23 | - name: Test 🧪 24 | run: | 25 | npm ci 26 | npm run check-ci 27 | 28 | - name: Upload Artifact ⬆️ 29 | uses: actions/upload-artifact@v4 30 | with: 31 | path: ./screenshots/*.png 32 | overwrite: true 33 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | import {getInfo} from './tests/gpu-info.js'; 3 | 4 | async function main() { 5 | const settings = typeof window === 'undefined' ? {} : Object.fromEntries(new URLSearchParams(window.location.search).entries()); 6 | if (settings.reporter) { 7 | mocha.reporter(settings.reporter); 8 | } 9 | if (settings.grep) { 10 | mocha.grep(new RegExp(settings.grep, 'i'), false); 11 | } 12 | 13 | await getInfo(); 14 | await Promise.all([ 15 | import('./tests/buffer-views-test.js'), 16 | import('./tests/data-definition-test.js'), 17 | import('./tests/generate-mipmap-test.js'), 18 | import('./tests/attribute-utils-test.js'), 19 | import('./tests/texture-utils-test.js'), 20 | ]); 21 | 22 | mocha.run((failures) => { 23 | window.testsPromiseInfo.resolve(failures); 24 | }); 25 | } 26 | 27 | main(); -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | permissions: 7 | contents: write 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | environment: deploy 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 20 20 | 21 | - name: Install and Build 🔧 22 | run: | 23 | npm ci 24 | npm run build-ci 25 | 26 | - name: Deploy 🚀 27 | uses: JamesIves/github-pages-deploy-action@v4 28 | with: 29 | folder: . 30 | 31 | - name: Publish to NPM 📖 32 | uses: JS-DevTools/npm-publish@v2 33 | with: 34 | token: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /dist/0.x/typed-arrays.d.ts: -------------------------------------------------------------------------------- 1 | export type TypedArrayConstructor = Int8ArrayConstructor | Uint8ArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; 2 | export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; 3 | export declare class TypedArrayViewGenerator { 4 | arrayBuffer: ArrayBuffer; 5 | byteOffset: number; 6 | constructor(sizeInBytes: number); 7 | align(alignment: number): void; 8 | pad(numBytes: number): void; 9 | getView(Ctor: TypedArrayConstructor, numElements: number): T; 10 | } 11 | export declare function subarray(arr: TypedArray, offset: number, length: number): T; 12 | export declare const isTypedArray: (arr: any) => any; 13 | -------------------------------------------------------------------------------- /dist/1.x/typed-arrays.d.ts: -------------------------------------------------------------------------------- 1 | export type TypedArrayConstructor = Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; 2 | export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; 3 | export declare class TypedArrayViewGenerator { 4 | arrayBuffer: ArrayBuffer; 5 | byteOffset: number; 6 | constructor(sizeInBytes: number); 7 | align(alignment: number): void; 8 | pad(numBytes: number): void; 9 | getView(Ctor: TypedArrayConstructor, numElements: number): T; 10 | } 11 | export declare function subarray(arr: TypedArray, offset: number, length: number): T; 12 | export declare const isTypedArray: (arr: any) => any; 13 | -------------------------------------------------------------------------------- /test/src/js/example-inject.js: -------------------------------------------------------------------------------- 1 | /* global GPUAdapter */ 2 | // eslint-disable-next-line strict 3 | "use strict"; 4 | 5 | function makePromise() { 6 | const info = {}; 7 | const promise = new Promise((resolve, reject) => { 8 | Object.assign(info, {resolve, reject}); 9 | }); 10 | info.promise = promise; 11 | return info; 12 | } 13 | 14 | window.testsPromiseInfo = makePromise(); 15 | 16 | window.addEventListener('error', (event) => { 17 | console.error(event); 18 | window.testsPromiseInfo.reject(1); 19 | }); 20 | 21 | // eslint-disable-next-line no-lone-blocks 22 | { 23 | GPUAdapter.prototype.requestDevice = (function (origFn) { 24 | return async function (...args) { 25 | const device = await origFn.call(this, args); 26 | if (device) { 27 | device.addEventListener('uncapturederror', function (e) { 28 | console.error(e.error.message); 29 | window.testsPromiseInfo.reject(1); 30 | }); 31 | } 32 | return device; 33 | }; 34 | })(GPUAdapter.prototype.requestDevice); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gregg Tavares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To Do 2 | 3 | - [ ] document texture functions 4 | - [ ] handle that alignment on storage is different than uniform 5 | - [ ] handle without views via index 6 | - [ ] make defs smaller (don't expand arrays) 7 | - [ ] add group/binding map 8 | - [ ] check makeStructuredView.set doesn't conflict with field named `'set'` 9 | - [ ] allow querying offset and range for manual setting 10 | - [ ] show creating your own spec. 11 | - [ ] make interleave compute arrayStride (highest offset + size of that attribute) 12 | - [ ] handle separate buffers? 13 | 14 | ## Done 15 | 16 | - [X] handle @align @size attributes 17 | - [X] handle sub setting `set('foo.bar', value); 18 | 19 | This already works. you can do `set({foo: {bar: value}})`; 20 | 21 | - [X] Uniform and Storage defs should be different than Structure defs 22 | 23 | Just that they have binding and group 24 | 25 | - [X] handle without views 26 | - [X] handle storage buffers 27 | - [X] make defs for name (instead of all) 28 | 29 | No, input is the shader source. It's seems an over optimization 30 | to pass in a single name and then have to parse again to get a different 31 | name. 32 | -------------------------------------------------------------------------------- /dist/0.x/generate-mipmap.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Converts a `GPUExtent3D` into an array of numbers 4 | * 5 | * `GPUExtent3D` has two forms `[width, height?, depth?]` or 6 | * `{width: number, height?: number, depthOrArrayLayers?: number}` 7 | * 8 | * You pass one of those in here and it returns an array of 3 numbers 9 | * so that your code doesn't have to deal with multiple forms. 10 | * 11 | * @param size 12 | * @returns an array of 3 numbers, [width, height, depthOrArrayLayers] 13 | */ 14 | export declare function normalizeGPUExtent3D(size: GPUExtent3D): number[]; 15 | /** 16 | * Given a GPUExtent3D returns the number of mip levels needed 17 | * 18 | * @param size 19 | * @returns number of mip levels needed for the given size 20 | */ 21 | export declare function numMipLevels(size: GPUExtent3D, dimension?: GPUTextureDimension): number; 22 | /** 23 | * Generates mip levels from level 0 to the last mip for an existing texture 24 | * 25 | * The texture must have been created with TEXTURE_BINDING and 26 | * RENDER_ATTACHMENT and been created with mip levels 27 | * 28 | * @param device 29 | * @param texture 30 | */ 31 | export declare function generateMipmap(device: GPUDevice, texture: GPUTexture): void; 32 | -------------------------------------------------------------------------------- /examples/reverse-z.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgpu-utils - reverse-z 5 | 6 | 7 | 30 | 31 | 32 | 33 |
34 |
35 |
reverse-z perspective projection infinite z
36 |
37 |
standard perspective
38 |
39 |
40 |
webgpu-utils - primitives reverse-z
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const roundUpToMultipleOf = (v: number, multiple: number) => (((v + multiple - 1) / multiple) | 0) * multiple; 2 | 3 | export function keysOf(obj: { [k in T]: unknown }): readonly T[] { 4 | return (Object.keys(obj) as unknown[]) as T[]; 5 | } 6 | 7 | export function range(count: number, fn: (i: number) => T) { 8 | return new Array(count).fill(0).map((_, i) => fn(i)); 9 | } 10 | 11 | const isIterable = (v: any) => 12 | v !== null && typeof v[Symbol.iterator] === 'function'; 13 | 14 | export function normalizeExtent3D(extent: GPUExtent3D): [number, number, number] { 15 | if (!extent) { 16 | return [1, 1, 1]; 17 | } 18 | if (isIterable(extent)) { 19 | const [w, h = 1, d = 1] = [...extent as number[]]; 20 | return [w, h, d]; 21 | } 22 | const { width = 1, height = 1, depthOrArrayLayers = 1 } = extent as GPUExtent3DDict; 23 | return [width, height, depthOrArrayLayers]; 24 | } 25 | 26 | export function normalizeOrigin3D(origin?: GPUOrigin3D): [number, number, number] { 27 | if (!origin) { 28 | return [0, 0, 0]; 29 | } 30 | if (isIterable(origin)) { 31 | const [x, y = 0, z = 0] = [...origin as number[]]; 32 | return [x, y, z]; 33 | } 34 | const { x = 0, y = 0, z = 0 } = origin as GPUOrigin3DDict; 35 | return [x, y, z]; 36 | } 37 | 38 | export function addOrigin3D(a?: GPUOrigin3D, b?: GPUOrigin3D): [number, number, number] { 39 | const an = normalizeOrigin3D(a); 40 | const bn = normalizeOrigin3D(b); 41 | return an.map((v, i) => v + bn[i]) as [number, number, number]; 42 | } -------------------------------------------------------------------------------- /dist/1.x/generate-mipmap.d.ts: -------------------------------------------------------------------------------- 1 | export declare function guessTextureBindingViewDimensionForTexture(dimension: GPUTextureDimension | undefined, depthOrArrayLayers: number): GPUTextureViewDimension; 2 | /** 3 | * Converts a `GPUExtent3D` into an array of numbers 4 | * 5 | * `GPUExtent3D` has two forms `[width, height?, depth?]` or 6 | * `{width: number, height?: number, depthOrArrayLayers?: number}` 7 | * 8 | * You pass one of those in here and it returns an array of 3 numbers 9 | * so that your code doesn't have to deal with multiple forms. 10 | * 11 | * @param size 12 | * @returns an array of 3 numbers, [width, height, depthOrArrayLayers] 13 | */ 14 | export declare function normalizeGPUExtent3D(size: GPUExtent3D): number[]; 15 | /** 16 | * Given a GPUExtent3D returns the number of mip levels needed 17 | * 18 | * @param size 19 | * @returns number of mip levels needed for the given size 20 | */ 21 | export declare function numMipLevels(size: GPUExtent3D, dimension?: GPUTextureDimension): number; 22 | /** 23 | * Generates mip levels from level 0 to the last mip for an existing texture 24 | * 25 | * The texture must have been created with TEXTURE_BINDING and RENDER_ATTACHMENT 26 | * and been created with mip levels 27 | * 28 | * @param device A GPUDevice 29 | * @param texture The texture to create mips for 30 | * @param textureBindingViewDimension This is only needed in compatibility mode 31 | * and it is only needed when the texture is going to be used as a cube map. 32 | */ 33 | export declare function generateMipmap(device: GPUDevice, texture: GPUTexture, textureBindingViewDimension?: GPUTextureViewDimension): void; 34 | -------------------------------------------------------------------------------- /src/typed-arrays.ts: -------------------------------------------------------------------------------- 1 | import { 2 | roundUpToMultipleOf, 3 | } from './utils.js'; 4 | 5 | export type TypedArrayConstructor = 6 | | Int8ArrayConstructor 7 | | Uint8ArrayConstructor 8 | | Uint8ClampedArrayConstructor 9 | | Int16ArrayConstructor 10 | | Uint16ArrayConstructor 11 | | Int32ArrayConstructor 12 | | Uint32ArrayConstructor 13 | | Float16ArrayConstructor 14 | | Float32ArrayConstructor 15 | | Float64ArrayConstructor; 16 | 17 | export type TypedArray = 18 | | Int8Array 19 | | Uint8Array 20 | | Uint8ClampedArray 21 | | Int16Array 22 | | Uint16Array 23 | | Int32Array 24 | | Uint32Array 25 | | Float16Array 26 | | Float32Array 27 | | Float64Array; 28 | 29 | export class TypedArrayViewGenerator { 30 | arrayBuffer: ArrayBuffer; 31 | byteOffset: number; 32 | 33 | constructor(sizeInBytes: number) { 34 | this.arrayBuffer = new ArrayBuffer(sizeInBytes); 35 | this.byteOffset = 0; 36 | } 37 | align(alignment: number) { 38 | this.byteOffset = roundUpToMultipleOf(this.byteOffset, alignment); 39 | } 40 | pad(numBytes: number) { 41 | this.byteOffset += numBytes; 42 | } 43 | getView(Ctor: TypedArrayConstructor, numElements: number): T { 44 | // @ts-expect-error this is a bug in ts https://github.com/microsoft/TypeScript/issues/62343 45 | const view = new Ctor(this.arrayBuffer, this.byteOffset, numElements); 46 | this.byteOffset += view.byteLength; 47 | return view as T; 48 | } 49 | } 50 | 51 | export function subarray(arr: TypedArray, offset: number, length: number): T { 52 | return arr.subarray(offset, offset + length) as T; 53 | } 54 | 55 | export const isTypedArray = (arr: any) => ArrayBuffer.isView(arr) && !(arr instanceof DataView); 56 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | webgpu-utils tests 6 | 7 | 8 | 9 | 10 |
11 | 24 | 25 | 26 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /dist/0.x/data-definitions.d.ts: -------------------------------------------------------------------------------- 1 | export type FieldDefinition = { 2 | offset: number; 3 | type: TypeDefinition; 4 | }; 5 | export type FieldDefinitions = { 6 | [x: string]: FieldDefinition; 7 | }; 8 | export type TypeDefinition = { 9 | size: number; 10 | }; 11 | export type StructDefinition = TypeDefinition & { 12 | fields: FieldDefinitions; 13 | size: number; 14 | }; 15 | export type IntrinsicDefinition = TypeDefinition & { 16 | type: string; 17 | numElements?: number; 18 | }; 19 | export type ArrayDefinition = TypeDefinition & { 20 | elementType: TypeDefinition; 21 | numElements: number; 22 | }; 23 | /** 24 | * @group(x) @binding(y) var<...> definition 25 | */ 26 | export interface VariableDefinition { 27 | binding: number; 28 | group: number; 29 | size: number; 30 | typeDefinition: TypeDefinition; 31 | } 32 | export type StructDefinitions = { 33 | [x: string]: StructDefinition; 34 | }; 35 | export type VariableDefinitions = { 36 | [x: string]: VariableDefinition; 37 | }; 38 | type ShaderDataDefinitions = { 39 | uniforms: VariableDefinitions; 40 | storages: VariableDefinitions; 41 | structs: StructDefinitions; 42 | }; 43 | /** 44 | * Given a WGSL shader, returns data definitions for structures, 45 | * uniforms, and storage buffers 46 | * 47 | * Example: 48 | * 49 | * ```js 50 | * const code = ` 51 | * struct MyStruct { 52 | * color: vec4f, 53 | * brightness: f32, 54 | * kernel: array, 55 | * }; 56 | * @group(0) @binding(0) var myUniforms: MyUniforms; 57 | * `; 58 | * const defs = makeShaderDataDefinitions(code); 59 | * const myUniformValues = makeStructuredView(defs.uniforms.myUniforms); 60 | * 61 | * myUniformValues.set({ 62 | * color: [1, 0, 1, 1], 63 | * brightness: 0.8, 64 | * kernel: [ 65 | * 1, 0, -1, 66 | * 2, 0, -2, 67 | * 1, 0, -1, 68 | * ], 69 | * }); 70 | * device.queue.writeBuffer(uniformBuffer, 0, myUniformValues.arrayBuffer); 71 | * ``` 72 | * 73 | * @param code WGSL shader. Note: it is not required for this to be a complete shader 74 | * @returns definitions of the structures by name. Useful for passing to {@link makeStructuredView} 75 | */ 76 | export declare function makeShaderDataDefinitions(code: string): ShaderDataDefinitions; 77 | export {}; 78 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | import fs from 'fs'; 5 | 6 | const pkg = JSON.parse(fs.readFileSync('package.json', {encoding: 'utf8'})); 7 | const banner = `/* webgpu-utils@${pkg.version}, license MIT */`; 8 | const major = pkg.version.split('.')[0]; 9 | const dist = `dist/${major}.x`; 10 | 11 | const plugins = [ 12 | nodeResolve(), 13 | typescript({ tsconfig: './tsconfig.json' }), 14 | ]; 15 | const shared = { 16 | watch: { 17 | clearScreen: false, 18 | }, 19 | }; 20 | 21 | export default [ 22 | { 23 | input: 'src/webgpu-utils.ts', 24 | output: [ 25 | { 26 | file: `${dist}/webgpu-utils.module.js`, 27 | format: 'esm', 28 | sourcemap: true, 29 | freeze: false, 30 | banner, 31 | }, 32 | ], 33 | plugins, 34 | ...shared, 35 | }, 36 | { 37 | input: 'src/webgpu-utils.ts', 38 | output: [ 39 | { 40 | file: `${dist}/webgpu-utils.module.min.js`, 41 | format: 'esm', 42 | sourcemap: true, 43 | freeze: false, 44 | banner, 45 | }, 46 | ], 47 | plugins: [ 48 | ...plugins, 49 | terser(), 50 | ], 51 | ...shared, 52 | }, 53 | { 54 | input: 'src/webgpu-utils.ts', 55 | output: [ 56 | { 57 | name: 'webgpuUtils', 58 | file: `${dist}/webgpu-utils.js`, 59 | format: 'umd', 60 | sourcemap: true, 61 | freeze: false, 62 | banner, 63 | }, 64 | ], 65 | plugins, 66 | ...shared, 67 | }, 68 | { 69 | input: 'src/webgpu-utils.ts', 70 | output: [ 71 | { 72 | name: 'webgpuUtils', 73 | file: `${dist}/webgpu-utils.min.js`, 74 | format: 'umd', 75 | sourcemap: true, 76 | freeze: false, 77 | banner, 78 | }, 79 | ], 80 | plugins: [ 81 | ...plugins, 82 | terser(), 83 | ], 84 | ...shared, 85 | }, 86 | ]; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgpu-utils", 3 | "version": "2.0.2", 4 | "description": "webgpu utilities", 5 | "main": "dist/2.x/webgpu-utils.module.js", 6 | "module": "dist/2.x/webgpu-utils.module.js", 7 | "types": "dist/2.x/webgpu-utils.d.ts", 8 | "type": "module", 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "build": "npm run make && npm run docs && npm run makeindex", 14 | "build-ci": "npm run build && node build/tools/prep-for-deploy.js", 15 | "make": "rollup -c && node build/tools/fixup.d.ts.js", 16 | "check": "npm run lint", 17 | "check-ci": "npm run pre-push", 18 | "docs": "typedoc --disableSources src/webgpu-utils.ts", 19 | "makeindex": "node build/tools/makeindex.js", 20 | "lint": "eslint \"src/**/*.{js,ts,tsx}\"", 21 | "pre-push": "npm run lint && npm run build && npm run test", 22 | "watch": "rollup -c -w", 23 | "serve": "servez", 24 | "start": "node build/tools/serve.js", 25 | "test": "node test/puppeteer.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/greggman/webgpu-utils.git" 30 | }, 31 | "files": [ 32 | "dist/**" 33 | ], 34 | "keywords": [ 35 | "webgpu", 36 | "gpu", 37 | "3d", 38 | "graphics" 39 | ], 40 | "author": "Gregg Tavares", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/greggman/webgpu-utils/issues" 44 | }, 45 | "homepage": "https://github.com/greggman/webgpu-utils#readme", 46 | "devDependencies": { 47 | "@rollup/plugin-node-resolve": "^16.0.1", 48 | "@rollup/plugin-terser": "^0.4.4", 49 | "@rollup/plugin-typescript": "^12.1.4", 50 | "@tsconfig/recommended": "^1.0.10", 51 | "@typescript-eslint/eslint-plugin": "^8.41.0", 52 | "@typescript-eslint/parser": "^8.41.0", 53 | "@webgpu/types": "^0.1.64", 54 | "eslint": "^8.57.1", 55 | "eslint-plugin-html": "^7.1.0", 56 | "eslint-plugin-one-variable-per-var": "^0.0.3", 57 | "eslint-plugin-optional-comma-spacing": "^0.0.4", 58 | "eslint-plugin-require-trailing-comma": "^0.0.1", 59 | "express": "^4.21.2", 60 | "markdown-it": "^14.1.0", 61 | "mocha": "^11.7.1", 62 | "puppeteer": "^24.17.0", 63 | "rollup": "^4.49.0", 64 | "servez": "^2.3.2", 65 | "tslib": "^2.8.1", 66 | "typedoc": "^0.28.11", 67 | "typescript": "^5.9.2", 68 | "wgsl_reflect": "github:brendan-duncan/wgsl_reflect#7ea42bc30920f0244ade6b0d1ab7fc7ab6f2e95b" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dist/1.x/wgsl-types.d.ts: -------------------------------------------------------------------------------- 1 | import { TypedArrayConstructor } from './typed-arrays.js'; 2 | export type TypeDef = { 3 | numElements: number; 4 | align: number; 5 | size: number; 6 | type: string; 7 | View: TypedArrayConstructor; 8 | flatten?: boolean; 9 | pad?: readonly number[]; 10 | }; 11 | export declare const kWGSLTypeInfo: { 12 | readonly 'atomic': TypeDef; 13 | readonly 'atomic': TypeDef; 14 | readonly 'vec2': TypeDef; 15 | readonly 'vec2': TypeDef; 16 | readonly 'vec2': TypeDef; 17 | readonly 'vec2': TypeDef; 18 | readonly 'vec3': TypeDef; 19 | readonly 'vec3': TypeDef; 20 | readonly 'vec3': TypeDef; 21 | readonly 'vec3': TypeDef; 22 | readonly 'vec4': TypeDef; 23 | readonly 'vec4': TypeDef; 24 | readonly 'vec4': TypeDef; 25 | readonly 'vec4': TypeDef; 26 | readonly 'mat2x2': TypeDef; 27 | readonly 'mat2x2': TypeDef; 28 | readonly 'mat3x2': TypeDef; 29 | readonly 'mat3x2': TypeDef; 30 | readonly 'mat4x2': TypeDef; 31 | readonly 'mat4x2': TypeDef; 32 | readonly 'mat2x3': TypeDef; 33 | readonly 'mat2x3': TypeDef; 34 | readonly 'mat3x3': TypeDef; 35 | readonly 'mat3x3': TypeDef; 36 | readonly 'mat4x3': TypeDef; 37 | readonly 'mat4x3': TypeDef; 38 | readonly 'mat2x4': TypeDef; 39 | readonly 'mat2x4': TypeDef; 40 | readonly 'mat3x4': TypeDef; 41 | readonly 'mat3x4': TypeDef; 42 | readonly 'mat4x4': TypeDef; 43 | readonly 'mat4x4': TypeDef; 44 | readonly i32: TypeDef; 45 | readonly u32: TypeDef; 46 | readonly f32: TypeDef; 47 | readonly f16: TypeDef; 48 | readonly vec2f: TypeDef; 49 | readonly vec2i: TypeDef; 50 | readonly vec2u: TypeDef; 51 | readonly vec2h: TypeDef; 52 | readonly vec3i: TypeDef; 53 | readonly vec3u: TypeDef; 54 | readonly vec3f: TypeDef; 55 | readonly vec3h: TypeDef; 56 | readonly vec4i: TypeDef; 57 | readonly vec4u: TypeDef; 58 | readonly vec4f: TypeDef; 59 | readonly vec4h: TypeDef; 60 | readonly mat2x2f: TypeDef; 61 | readonly mat2x2h: TypeDef; 62 | readonly mat3x2f: TypeDef; 63 | readonly mat3x2h: TypeDef; 64 | readonly mat4x2f: TypeDef; 65 | readonly mat4x2h: TypeDef; 66 | readonly mat2x3f: TypeDef; 67 | readonly mat2x3h: TypeDef; 68 | readonly mat3x3f: TypeDef; 69 | readonly mat3x3h: TypeDef; 70 | readonly mat4x3f: TypeDef; 71 | readonly mat4x3h: TypeDef; 72 | readonly mat2x4f: TypeDef; 73 | readonly mat2x4h: TypeDef; 74 | readonly mat3x4f: TypeDef; 75 | readonly mat3x4h: TypeDef; 76 | readonly mat4x4f: TypeDef; 77 | readonly mat4x4h: TypeDef; 78 | readonly bool: TypeDef; 79 | }; 80 | export type WGSLType = keyof typeof kWGSLTypeInfo; 81 | export declare const kWGSLTypes: readonly WGSLType[]; 82 | -------------------------------------------------------------------------------- /CHANGELIST.md: -------------------------------------------------------------------------------- 1 | # Change List 2 | 3 | ### 2.0.0 4 | 5 | * Allow uploading multiple mip levels from data 6 | 7 | Note: The breaking change here is that the 1.x would call `generateMipmap` automatically 8 | if `texture.mipLevelCount > 1`. That was arguably a bug and in fact you can see all the 9 | examples were passing in an option of `{ mips: true }` even though `mips` wasn't an option. 😅 10 | `mips` is now an option. Set it to `true` if you want `generateMipmap` on creation and/or 11 | on `copySourceToTexture` / `copySourcesToTexture`. Code that wasn't setting it before 12 | and was getting lucky to have mip maps generated will need to pass it in. 13 | 14 | ### 1.11.0 15 | 16 | * Switch to undefined bindGroupLayouts instead of empty bindGroupLayouts 17 | 18 | ### 1.10.3 19 | 20 | * Fix so can use in worker 21 | 22 | ### 1.10.1 23 | 24 | * Fix so `@size` doesn't increase size of intrinsic view. 25 | 26 | ### 1.10.0 27 | 28 | * Support compatibility mode - eg mip generation 29 | 30 | ### 1.9.0 31 | 32 | * Support `minBindingSize` in bind group layouts. 33 | 34 | ### 1.8.0 35 | 36 | * Support creating 3d textures from images/canvases... 37 | 38 | ### 1.7.0 39 | 40 | * Add `primitives.deindex` 41 | * Add `primitives.generateTriangleNormals` 42 | 43 | ### 1.6.0 44 | 45 | * Export `ShaderDataDefinitions` 46 | 47 | ### 1.5.1 48 | 49 | * handle empty bind groups. 50 | 51 | ### 1.5.0 52 | 53 | * Add support for storage textures, external textures, and samplers 54 | 55 | ### 1.4.0 56 | 57 | * Support `atomic` 58 | 59 | ### 1.3.0 60 | 61 | * Add `makeBindGroupLayoutDescriptors` 62 | 63 | ### 1.2.0 64 | 65 | * Add `getSizeOfUnsizedArrayElement` 66 | 67 | ### 1.1.0 68 | 69 | * Make `generateMipmap` support compatibility mode 70 | 71 | ### 1.0.0 72 | 73 | * switch primitive functions to use named parameters. 74 | 75 | ### 0.15.0 76 | 77 | * add `setIntrinsicsToView` 78 | 79 | ### 0.14.3 80 | 81 | * Fixes for vec2 typos 82 | 83 | ### 0.14.2 84 | 85 | * Handle bool issue fix 86 | 87 | ### 0.14.0 88 | 89 | * Use latest wgsl_reflect 90 | 91 | ### 0.13.0 92 | 93 | * Support making views of unsized arrays 94 | 95 | ### 0.12.0 96 | 97 | * Use newer version of wgsl_reflect and handle arrays of arrays 98 | 99 | ### 0.11.0 100 | 101 | * Add primitives 102 | 103 | ### 0.10.0 104 | 105 | * Add instancing support 106 | 107 | ### 0.9.0 108 | 109 | * Change `createBufferInfoFromArrays` to `createBuffersAndAttributesFromArrays` 110 | 111 | ### 0.8.0 112 | 113 | * Add (alpha version) of `createBufferInfoFromArrays` 114 | 115 | ### 0.7.0 116 | 117 | * Support for multiple sources for layers 118 | * Generate mips for layers 119 | 120 | ### 0.6.0 121 | 122 | * Support typedArrays and native arrays via `copySourceToTexture` 123 | and `createTextureFromSource` 124 | 125 | ### 0.5.0 126 | 127 | * Add 'premultipliedAlpha' and 'colorSpace' texture options 128 | to `copySourceToTexture` 129 | 130 | ### 0.4.3 131 | 132 | * try changing type of `ArrayBufferViews.views` to `any` because 133 | otherwise it's too cumbersome to use in TypeScript. 134 | 135 | ### 0.4.2 136 | 137 | * Update wgsl_reflect to handle `array` constructor 138 | * Add docs 139 | 140 | ### 0.3.0 141 | 142 | * Add support for setting arrays as arrays 143 | * add texture related functions 144 | 145 | * `generateMipmap` 146 | * `copySourceToTexture` 147 | * `getSizeFromSource` 148 | * `createTextureFromSource` 149 | * `createTextureFromImage` 150 | * `numMipLevels` 151 | * `normalizeGPUExtent3D` 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /test/puppeteer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from 'node:process'; 4 | import puppeteer from 'puppeteer'; 5 | import path from 'node:path'; 6 | import fs from 'node:fs'; 7 | import express from 'express'; 8 | import url from 'node:url'; 9 | const app = express(); 10 | const port = 3000; 11 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); // eslint-disable-line 12 | 13 | app.use(express.static(path.dirname(__dirname))); 14 | const server = app.listen(port, (err) => { 15 | if (err) { 16 | console.error('Error starting server:', err); 17 | // eslint-disable-next-line no-process-exit 18 | process.exit(1); 19 | } 20 | console.log(`Example app listening on port ${port}!`); 21 | test(port); 22 | }); 23 | 24 | function makePromiseInfo() { 25 | const info = {}; 26 | const promise = new Promise((resolve, reject) => { 27 | Object.assign(info, {resolve, reject}); 28 | }); 29 | info.promise = promise; 30 | return info; 31 | } 32 | 33 | const exampleInjectJS = fs.readFileSync('test/src/js/example-inject.js', {encoding: 'utf-8'}); 34 | 35 | function getExamples(port) { 36 | return fs.readdirSync('examples') 37 | .filter(f => f.endsWith('.html')) 38 | .map(f => ({ 39 | url: `http://localhost:${port}/examples/${f}`, 40 | js: exampleInjectJS, 41 | screenshot: true, 42 | })); 43 | } 44 | 45 | async function test(port) { 46 | const browser = await puppeteer.launch({ 47 | headless: "new", 48 | protocolTimeout: 4 * 60 * 1000, // 4 mins 49 | args: [ 50 | //'--enable-unsafe-webgpu', 51 | //'--enable-webgpu-developer-features', 52 | //'--use-angle=swiftshader', 53 | '--user-agent=puppeteer', 54 | '--no-sandbox', 55 | ], 56 | }); 57 | const page = await browser.newPage(); 58 | 59 | page.on('console', async e => { 60 | const args = await Promise.all(e.args().map(a => a.jsonValue())); 61 | console.log(...args); 62 | }); 63 | 64 | let totalFailures = 0; 65 | let waitingPromiseInfo; 66 | 67 | // Get the "viewport" of the page, as reported by the page. 68 | page.on('domcontentloaded', async () => { 69 | const failures = await page.evaluate(() => { 70 | return window.testsPromiseInfo.promise; 71 | }); 72 | 73 | totalFailures += failures; 74 | 75 | waitingPromiseInfo.resolve(); 76 | }); 77 | 78 | const testPages = [ 79 | {url: `http://localhost:${port}/test/index.html?reporter=spec` }, 80 | ...getExamples(port), 81 | ]; 82 | 83 | for (const {url, js, screenshot} of testPages) { 84 | waitingPromiseInfo = makePromiseInfo(); 85 | console.log(`===== [ ${url} ] =====`); 86 | const id = js 87 | ? await page.evaluateOnNewDocument(js) 88 | : undefined; 89 | await page.goto(url); 90 | await page.waitForNetworkIdle(); 91 | if (js) { 92 | await page.evaluate(() => { 93 | setTimeout(() => { 94 | window.testsPromiseInfo.resolve(0); 95 | }, 10); 96 | }); 97 | } 98 | await waitingPromiseInfo.promise; 99 | if (screenshot) { 100 | const dir = 'screenshots'; 101 | fs.mkdirSync(dir, { recursive: true }); 102 | const name = /\/([a-z0-9_-]+).html/.exec(url)[1]; 103 | const path = `${dir}/${name}.png`; 104 | await page.screenshot({path}); 105 | } 106 | if (js) { 107 | await page.removeScriptToEvaluateOnNewDocument(id.identifier); 108 | } 109 | } 110 | 111 | await browser.close(); 112 | server.close(); 113 | 114 | process.exit(totalFailures ? 1 : 0); // eslint-disable-line 115 | } 116 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* global module, __dirname */ 2 | module.exports = { 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | browser: true, 6 | es2022: true, 7 | }, 8 | parserOptions: { 9 | sourceType: 'module', 10 | ecmaVersion: 2022, 11 | tsconfigRootDir: __dirname, 12 | project: ['./tsconfig.json'], 13 | extraFileExtensions: ['.html'], 14 | }, 15 | settings: { 16 | react: { 17 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 18 | }, 19 | }, 20 | root: true, 21 | plugins: [ 22 | '@typescript-eslint', 23 | 'eslint-plugin-html', 24 | 'eslint-plugin-optional-comma-spacing', 25 | 'eslint-plugin-one-variable-per-var', 26 | 'eslint-plugin-require-trailing-comma', 27 | ], 28 | extends: [ 29 | 'eslint:recommended', 30 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 31 | ], 32 | rules: { 33 | 'brace-style': [2, '1tbs', { allowSingleLine: false }], 34 | camelcase: [0], 35 | 'comma-dangle': 0, 36 | 'comma-spacing': 0, 37 | 'comma-style': [2, 'last'], 38 | 'consistent-return': 2, 39 | curly: [2, 'all'], 40 | 'dot-notation': 0, 41 | 'eol-last': [0], 42 | eqeqeq: 2, 43 | 'global-strict': [0], 44 | 'key-spacing': [0], 45 | 'keyword-spacing': [1, { before: true, after: true, overrides: {} }], 46 | 'new-cap': 2, 47 | 'new-parens': 2, 48 | 'no-alert': 2, 49 | 'no-array-constructor': 2, 50 | 'no-caller': 2, 51 | 'no-catch-shadow': 2, 52 | 'no-comma-dangle': [0], 53 | 'no-const-assign': 2, 54 | 'no-eval': 2, 55 | 'no-extend-native': 2, 56 | 'no-extra-bind': 2, 57 | 'no-extra-parens': [2, 'functions'], 58 | 'no-implied-eval': 2, 59 | 'no-irregular-whitespace': 2, 60 | 'no-iterator': 2, 61 | 'no-label-var': 2, 62 | 'no-labels': 2, 63 | 'no-lone-blocks': 2, 64 | 'no-loop-func': 2, 65 | 'no-multi-spaces': [0], 66 | 'no-multi-str': 2, 67 | 'no-native-reassign': 2, 68 | 'no-new-func': 2, 69 | 'no-new-object': 2, 70 | 'no-new-wrappers': 2, 71 | 'no-new': 2, 72 | 'no-obj-calls': 2, 73 | 'no-octal-escape': 2, 74 | 'no-process-exit': 2, 75 | 'no-proto': 2, 76 | 'no-return-assign': 2, 77 | 'no-script-url': 2, 78 | 'no-sequences': 2, 79 | 'no-shadow-restricted-names': 2, 80 | 'no-shadow': [0], 81 | 'no-spaced-func': 2, 82 | 'no-trailing-spaces': 2, 83 | 'no-undef-init': 2, 84 | //'no-undef': 2, // ts recommends this be off: https://typescript-eslint.io/linting/troubleshooting 85 | 'no-underscore-dangle': 2, 86 | 'no-unreachable': 2, 87 | 'no-unused-expressions': 2, 88 | 'no-use-before-define': 0, 89 | 'no-var': 2, 90 | 'no-with': 2, 91 | 'one-variable-per-var/one-variable-per-var': [2], 92 | 'optional-comma-spacing/optional-comma-spacing': [2, { after: true }], 93 | 'prefer-const': 2, 94 | 'require-trailing-comma/require-trailing-comma': [2], 95 | 'semi-spacing': [2, { before: false, after: true }], 96 | semi: [2, 'always'], 97 | 'space-before-function-paren': [ 98 | 2, 99 | { 100 | anonymous: 'always', 101 | named: 'never', 102 | asyncArrow: 'always', 103 | }, 104 | ], 105 | 'space-infix-ops': 2, 106 | 'space-unary-ops': [2, { words: true, nonwords: false }], 107 | strict: [2, 'function'], 108 | yoda: [2, 'never'], 109 | '@typescript-eslint/no-empty-function': 'off', 110 | '@typescript-eslint/no-explicit-any': 'off', // TODO: Reenable this and figure out how to fix code. 111 | '@typescript-eslint/no-non-null-assertion': 'off', 112 | '@typescript-eslint/no-unused-vars': 2, 113 | }, 114 | }; 115 | -------------------------------------------------------------------------------- /src/format-info.ts: -------------------------------------------------------------------------------- 1 | import { TypedArrayConstructor } from "./typed-arrays.js"; 2 | 3 | // [blockWidth, blockHeight, bytesPerBlock, units per TypedArray element, TypedArrayConstructor] 4 | const kFormatInfo = { 5 | 'rgba8unorm-srgb': [1, 1, 4, 4, Uint8Array], 6 | 'bgra8unorm-srgb': [1, 1, 4, 4, Uint8Array], 7 | 'rgb10a2uint': [1, 1, 4, 1, Uint32Array], 8 | 'rgb10a2unorm': [1, 1, 4, 1, Uint32Array], 9 | 'rg11b10ufloat': [1, 1, 4, 1, Uint32Array], 10 | 'rgb9e5ufloat': [1, 1, 4, 1, Uint32Array], 11 | 'stencil8': [1, 1, 1, 1, Uint8Array], 12 | 'depth16unorm': [1, 1, 2, 1, Uint16Array], 13 | 'depth32float': [1, 1, 4, 1, Float32Array], 14 | 'depth24plus-stencil8': [], 15 | 'depth32float-stencil8': [], 16 | 'bc1-rgba-unorm': [4, 4, 8], 17 | 'bc1-rgba-unorm-srgb': [4, 4, 8], 18 | 'bc2-rgba-unorm': [4, 4, 16], 19 | 'bc2-rgba-unorm-srgb': [4, 4, 16], 20 | 'bc3-rgba-unorm': [4, 4, 16], 21 | 'bc3-rgba-unorm-srgb': [4, 4, 16], 22 | 'bc4-r-unorm': [4, 4, 8], 23 | 'bc4-r-snorm': [4, 4, 8], 24 | 'bc5-rg-unorm': [4, 4, 16], 25 | 'bc5-rg-snorm': [4, 4, 16], 26 | 'bc6h-rgb-ufloat': [4, 4, 16], 27 | 'bc6h-rgb-float': [4, 4, 16], 28 | 'bc7-rgba-unorm': [4, 4, 16], 29 | 'bc7-rgba-unorm-srgb': [4, 4, 16], 30 | 'etc2-rgb8unorm': [4, 4, 8], 31 | 'etc2-rgb8unorm-srgb': [4, 4, 8], 32 | 'etc2-rgb8a1unorm': [4, 4, 8], 33 | 'etc2-rgb8a1unorm-srgb': [4, 4, 8], 34 | 'etc2-rgba8unorm': [4, 4, 16], 35 | 'etc2-rgba8unorm-srgb': [4, 4, 16], 36 | 'eac-r11unorm': [4, 4, 8], 37 | 'eac-r11snorm': [4, 4, 8], 38 | 'eac-rg11unorm': [4, 4, 16], 39 | 'eac-rg11snorm': [4, 4, 16], 40 | 'astc-4x4-unorm': [4, 4, 16], 41 | 'astc-4x4-unorm-srgb': [4, 4, 16], 42 | 'astc-5x4-unorm': [5, 4, 16], 43 | 'astc-5x4-unorm-srgb': [5, 4, 16], 44 | 'astc-5x5-unorm': [5, 5, 16], 45 | 'astc-5x5-unorm-srgb': [5, 5, 16], 46 | 'astc-6x5-unorm': [6, 5, 16], 47 | 'astc-6x5-unorm-srgb': [6, 5, 16], 48 | 'astc-6x6-unorm': [6, 6, 16], 49 | 'astc-6x6-unorm-srgb': [6, 6, 16], 50 | 'astc-8x5-unorm': [8, 5, 16], 51 | 'astc-8x5-unorm-srgb': [8, 5, 16], 52 | 'astc-8x6-unorm': [8, 6, 16], 53 | 'astc-8x6-unorm-srgb': [8, 6, 16], 54 | 'astc-8x8-unorm': [8, 8, 16], 55 | 'astc-8x8-unorm-srgb': [8, 8, 16], 56 | 'astc-10x5-unorm': [10, 5, 16], 57 | 'astc-10x5-unorm-srgb': [10, 5, 16], 58 | 'astc-10x6-unorm': [10, 6, 16], 59 | 'astc-10x6-unorm-srgb': [10, 6, 16], 60 | 'astc-10x8-unorm': [10, 8, 16], 61 | 'astc-10x8-unorm-srgb': [10, 8, 16], 62 | 'astc-10x10-unorm': [10, 10, 16], 63 | 'astc-10x10-unorm-srgb': [10, 10, 16], 64 | 'astc-12x10-unorm': [12, 10, 16], 65 | 'astc-12x10-unorm-srgb': [12, 10, 16], 66 | 'astc-12x12-unorm': [12, 12, 16], 67 | 'astc-12x12-unorm-srgb': [12, 12, 16], 68 | } as const; 69 | 70 | const kFormatToTypedArray: {[key: string]: TypedArrayConstructor} = { 71 | '8snorm': Int8Array, 72 | '8unorm': Uint8Array, 73 | '8sint': Int8Array, 74 | '8uint': Uint8Array, 75 | '16snorm': Int16Array, 76 | '16unorm': Uint16Array, 77 | '16sint': Int16Array, 78 | '16uint': Uint16Array, 79 | '32snorm': Int32Array, 80 | '32unorm': Uint32Array, 81 | '32sint': Int32Array, 82 | '32uint': Uint32Array, 83 | '16float': Float16Array, 84 | '32float': Float32Array, 85 | } as const; 86 | 87 | const kTextureFormatRE = /([a-z]+)(\d+)([a-z]+)/; 88 | 89 | export type FormatInfo = { 90 | blockWidth: number, 91 | blockHeight: number, 92 | bytesPerBlock: number, 93 | unitsPerElement: number, 94 | Type: TypedArrayConstructor 95 | }; 96 | 97 | export function getTextureFormatInfo(format: GPUTextureFormat): FormatInfo { 98 | const info = kFormatInfo[format as keyof typeof kFormatInfo]; 99 | if (info) { 100 | const [blockWidth, blockHeight, bytesPerBlock, unitsPerElement = 1, Type = Uint8Array] = info; 101 | if (bytesPerBlock === undefined) { 102 | throw new Error(`Format ${format} is not supported`); 103 | } 104 | return { 105 | blockWidth, 106 | blockHeight, 107 | bytesPerBlock, 108 | unitsPerElement, 109 | Type, 110 | }; 111 | } 112 | 113 | // this is a hack! It will only work for common formats 114 | const [, channels, bits, typeName] = kTextureFormatRE.exec(format)!; 115 | // TODO: if the regex fails, use table for other formats? 116 | const numChannels = channels.length; 117 | const bytesPerChannel = parseInt(bits) / 8; 118 | const bytesPerBlock = numChannels * bytesPerChannel; 119 | const Type = kFormatToTypedArray[`${bits}${typeName}`] ?? Uint8Array; 120 | 121 | return { 122 | blockWidth: 1, 123 | blockHeight: 1, 124 | bytesPerBlock, 125 | unitsPerElement: 1, 126 | Type, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/wgsl-types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | keysOf, 3 | } from './utils.js'; 4 | import { 5 | TypedArrayConstructor, 6 | } from './typed-arrays.js'; 7 | 8 | export type TypeDef = { 9 | numElements: number; 10 | align: number; 11 | size: number; 12 | type: string; 13 | View: TypedArrayConstructor; 14 | flatten?: boolean, 15 | pad?: readonly number[]; 16 | }; 17 | 18 | const createTypeDefs = >(defs: T): { readonly [K in keyof T]: TypeDef } => defs; 19 | 20 | const b = createTypeDefs({ 21 | i32: { numElements: 1, align: 4, size: 4, type: 'i32', View: Int32Array }, 22 | u32: { numElements: 1, align: 4, size: 4, type: 'u32', View: Uint32Array }, 23 | f32: { numElements: 1, align: 4, size: 4, type: 'f32', View: Float32Array }, 24 | f16: { numElements: 1, align: 2, size: 2, type: 'u16', View: Uint16Array }, 25 | 26 | vec2f: { numElements: 2, align: 8, size: 8, type: 'f32', View: Float32Array }, 27 | vec2i: { numElements: 2, align: 8, size: 8, type: 'i32', View: Int32Array }, 28 | vec2u: { numElements: 2, align: 8, size: 8, type: 'u32', View: Uint32Array }, 29 | vec2h: { numElements: 2, align: 4, size: 4, type: 'u16', View: Uint16Array }, 30 | vec3i: { numElements: 3, align: 16, size: 12, type: 'i32', View: Int32Array }, 31 | vec3u: { numElements: 3, align: 16, size: 12, type: 'u32', View: Uint32Array }, 32 | vec3f: { numElements: 3, align: 16, size: 12, type: 'f32', View: Float32Array }, 33 | vec3h: { numElements: 3, align: 8, size: 6, type: 'u16', View: Uint16Array }, 34 | vec4i: { numElements: 4, align: 16, size: 16, type: 'i32', View: Int32Array }, 35 | vec4u: { numElements: 4, align: 16, size: 16, type: 'u32', View: Uint32Array }, 36 | vec4f: { numElements: 4, align: 16, size: 16, type: 'f32', View: Float32Array }, 37 | vec4h: { numElements: 4, align: 8, size: 8, type: 'u16', View: Uint16Array }, 38 | 39 | // AlignOf(vecR) SizeOf(array) 40 | mat2x2f: { numElements: 4, align: 8, size: 16, type: 'f32', View: Float32Array }, 41 | mat2x2h: { numElements: 4, align: 4, size: 8, type: 'u16', View: Uint16Array }, 42 | mat3x2f: { numElements: 6, align: 8, size: 24, type: 'f32', View: Float32Array }, 43 | mat3x2h: { numElements: 6, align: 4, size: 12, type: 'u16', View: Uint16Array }, 44 | mat4x2f: { numElements: 8, align: 8, size: 32, type: 'f32', View: Float32Array }, 45 | mat4x2h: { numElements: 8, align: 4, size: 16, type: 'u16', View: Uint16Array }, 46 | mat2x3f: { numElements: 8, align: 16, size: 32, pad: [3, 1], type: 'f32', View: Float32Array }, 47 | mat2x3h: { numElements: 8, align: 8, size: 16, pad: [3, 1], type: 'u16', View: Uint16Array }, 48 | mat3x3f: { numElements: 12, align: 16, size: 48, pad: [3, 1], type: 'f32', View: Float32Array }, 49 | mat3x3h: { numElements: 12, align: 8, size: 24, pad: [3, 1], type: 'u16', View: Uint16Array }, 50 | mat4x3f: { numElements: 16, align: 16, size: 64, pad: [3, 1], type: 'f32', View: Float32Array }, 51 | mat4x3h: { numElements: 16, align: 8, size: 32, pad: [3, 1], type: 'u16', View: Uint16Array }, 52 | mat2x4f: { numElements: 8, align: 16, size: 32, type: 'f32', View: Float32Array }, 53 | mat2x4h: { numElements: 8, align: 8, size: 16, type: 'u16', View: Uint16Array }, 54 | mat3x4f: { numElements: 12, align: 16, size: 48, pad: [3, 1], type: 'f32', View: Float32Array }, 55 | mat3x4h: { numElements: 12, align: 8, size: 24, pad: [3, 1], type: 'u16', View: Uint16Array }, 56 | mat4x4f: { numElements: 16, align: 16, size: 64, type: 'f32', View: Float32Array }, 57 | mat4x4h: { numElements: 16, align: 8, size: 32, type: 'u16', View: Uint16Array }, 58 | 59 | // Note: At least as of WGSL V1 you can not create a bool for uniform or storage. 60 | // You can only create one in an internal struct. But, this code generates 61 | // views of structs and it needs to not fail if the struct has a bool 62 | bool: { numElements: 0, align: 1, size: 0, type: 'bool', View: Uint32Array }, 63 | } as const); 64 | 65 | export const kWGSLTypeInfo = createTypeDefs({ 66 | ...b, 67 | 68 | 'atomic': b.i32, 69 | 'atomic': b.u32, 70 | 71 | 'vec2': b.vec2i, 72 | 'vec2': b.vec2u, 73 | 'vec2': b.vec2f, 74 | 'vec2': b.vec2h, 75 | 'vec3': b.vec3i, 76 | 'vec3': b.vec3u, 77 | 'vec3': b.vec3f, 78 | 'vec3': b.vec3h, 79 | 'vec4': b.vec4i, 80 | 'vec4': b.vec4u, 81 | 'vec4': b.vec4f, 82 | 'vec4': b.vec4h, 83 | 84 | 'mat2x2': b.mat2x2f, 85 | 'mat2x2': b.mat2x2h, 86 | 'mat3x2': b.mat3x2f, 87 | 'mat3x2': b.mat3x2h, 88 | 'mat4x2': b.mat4x2f, 89 | 'mat4x2': b.mat4x2h, 90 | 'mat2x3': b.mat2x3f, 91 | 'mat2x3': b.mat2x3h, 92 | 'mat3x3': b.mat3x3f, 93 | 'mat3x3': b.mat3x3h, 94 | 'mat4x3': b.mat4x3f, 95 | 'mat4x3': b.mat4x3h, 96 | 'mat2x4': b.mat2x4f, 97 | 'mat2x4': b.mat2x4h, 98 | 'mat3x4': b.mat3x4f, 99 | 'mat3x4': b.mat3x4h, 100 | 'mat4x4': b.mat4x4f, 101 | 'mat4x4': b.mat4x4h, 102 | } as const); 103 | export type WGSLType = keyof typeof kWGSLTypeInfo; 104 | export const kWGSLTypes: readonly WGSLType[] = keysOf(kWGSLTypeInfo); 105 | -------------------------------------------------------------------------------- /dist/0.x/texture-utils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { TypedArray } from './typed-arrays.js'; 3 | export type CopyTextureOptions = { 4 | flipY?: boolean; 5 | premultipliedAlpha?: boolean; 6 | colorSpace?: PredefinedColorSpace; 7 | dimension?: GPUTextureViewDimension; 8 | baseArrayLayer?: number; 9 | }; 10 | export type TextureData = { 11 | data: TypedArray | number[]; 12 | }; 13 | export type TextureCreationData = TextureData & { 14 | width?: number; 15 | height?: number; 16 | }; 17 | export type TextureRawDataSource = TextureCreationData | TypedArray | number[]; 18 | export type TextureSource = GPUImageCopyExternalImage['source'] | TextureRawDataSource; 19 | /** 20 | * Gets the size of a mipLevel. Returns an array of 3 numbers [width, height, depthOrArrayLayers] 21 | */ 22 | export declare function getSizeForMipFromTexture(texture: GPUTexture, mipLevel: number): number[]; 23 | /** 24 | * Copies a an array of "sources" (Video, Canvas, OffscreenCanvas, ImageBitmap) 25 | * to a texture and then optionally generates mip levels 26 | */ 27 | export declare function copySourcesToTexture(device: GPUDevice, texture: GPUTexture, sources: TextureSource[], options?: CopyTextureOptions): void; 28 | /** 29 | * Copies a "source" (Video, Canvas, OffscreenCanvas, ImageBitmap) 30 | * to a texture and then optionally generates mip levels 31 | */ 32 | export declare function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: TextureSource, options?: CopyTextureOptions): void; 33 | /** 34 | * @property mips if true and mipLevelCount is not set then wll automatically generate 35 | * the correct number of mip levels. 36 | * @property format Defaults to "rgba8unorm" 37 | * @property mipLeveLCount Defaults to 1 or the number of mips needed for a full mipmap if `mips` is true 38 | */ 39 | export type CreateTextureOptions = CopyTextureOptions & { 40 | mips?: boolean; 41 | usage?: GPUTextureUsageFlags; 42 | format?: GPUTextureFormat; 43 | mipLevelCount?: number; 44 | }; 45 | /** 46 | * Gets the size from a source. This is to smooth out the fact that different 47 | * sources have a different way to get their size. 48 | */ 49 | export declare function getSizeFromSource(source: TextureSource, options: CreateTextureOptions): number[]; 50 | /** 51 | * Create a texture from an array of sources (Video, Canvas, OffscreenCanvas, ImageBitmap) 52 | * and optionally create mip levels. If you set `mips: true` and don't set a mipLevelCount 53 | * then it will automatically make the correct number of mip levels. 54 | * 55 | * Example: 56 | * 57 | * ```js 58 | * const texture = createTextureFromSource( 59 | * device, 60 | * [ 61 | * someCanvasOrVideoOrImageImageBitmap0, 62 | * someCanvasOrVideoOrImageImageBitmap1, 63 | * ], 64 | * { 65 | * usage: GPUTextureUsage.TEXTURE_BINDING | 66 | * GPUTextureUsage.RENDER_ATTACHMENT | 67 | * GPUTextureUsage.COPY_DST, 68 | * mips: true, 69 | * } 70 | * ); 71 | * ``` 72 | */ 73 | export declare function createTextureFromSources(device: GPUDevice, sources: TextureSource[], options?: CreateTextureOptions): GPUTexture; 74 | /** 75 | * Create a texture from a source (Video, Canvas, OffscreenCanvas, ImageBitmap) 76 | * and optionally create mip levels. If you set `mips: true` and don't set a mipLevelCount 77 | * then it will automatically make the correct number of mip levels. 78 | * 79 | * Example: 80 | * 81 | * ```js 82 | * const texture = createTextureFromSource( 83 | * device, 84 | * someCanvasOrVideoOrImageImageBitmap, 85 | * { 86 | * usage: GPUTextureUsage.TEXTURE_BINDING | 87 | * GPUTextureUsage.RENDER_ATTACHMENT | 88 | * GPUTextureUsage.COPY_DST, 89 | * mips: true, 90 | * } 91 | * ); 92 | * ``` 93 | */ 94 | export declare function createTextureFromSource(device: GPUDevice, source: TextureSource, options?: CreateTextureOptions): GPUTexture; 95 | export type CreateTextureFromBitmapOptions = CreateTextureOptions & ImageBitmapOptions; 96 | /** 97 | * Load an ImageBitmap 98 | * @param url 99 | * @param options 100 | * @returns the loaded ImageBitmap 101 | */ 102 | export declare function loadImageBitmap(url: string, options?: ImageBitmapOptions): Promise; 103 | /** 104 | * Load images and create a texture from them, optionally generating mip levels 105 | * 106 | * Assumes all the urls reference images of the same size. 107 | * 108 | * Example: 109 | * 110 | * ```js 111 | * const texture = await createTextureFromImage( 112 | * device, 113 | * [ 114 | * 'https://someimage1.url', 115 | * 'https://someimage2.url', 116 | * ], 117 | * { 118 | * mips: true, 119 | * flipY: true, 120 | * }, 121 | * ); 122 | * ``` 123 | */ 124 | export declare function createTextureFromImages(device: GPUDevice, urls: string[], options?: CreateTextureFromBitmapOptions): Promise; 125 | /** 126 | * Load an image and create a texture from it, optionally generating mip levels 127 | * 128 | * Example: 129 | * 130 | * ```js 131 | * const texture = await createTextureFromImage(device, 'https://someimage.url', { 132 | * mips: true, 133 | * flipY: true, 134 | * }); 135 | * ``` 136 | */ 137 | export declare function createTextureFromImage(device: GPUDevice, url: string, options?: CreateTextureFromBitmapOptions): Promise; 138 | -------------------------------------------------------------------------------- /dist/1.x/texture-utils.d.ts: -------------------------------------------------------------------------------- 1 | import { TypedArray } from './typed-arrays.js'; 2 | export type CopyTextureOptions = { 3 | flipY?: boolean; 4 | premultipliedAlpha?: boolean; 5 | colorSpace?: PredefinedColorSpace; 6 | dimension?: GPUTextureDimension; 7 | viewDimension?: GPUTextureViewDimension; 8 | baseArrayLayer?: number; 9 | }; 10 | export type TextureData = { 11 | data: TypedArray | number[]; 12 | }; 13 | export type TextureCreationData = TextureData & { 14 | width?: number; 15 | height?: number; 16 | }; 17 | export type TextureRawDataSource = TextureCreationData | TypedArray | number[]; 18 | export type TextureSource = GPUCopyExternalImageSourceInfo['source'] | TextureRawDataSource; 19 | /** 20 | * Gets the size of a mipLevel. Returns an array of 3 numbers [width, height, depthOrArrayLayers] 21 | */ 22 | export declare function getSizeForMipFromTexture(texture: GPUTexture, mipLevel: number): number[]; 23 | /** 24 | * Copies a an array of "sources" (Video, Canvas, OffscreenCanvas, ImageBitmap) 25 | * to a texture and then optionally generates mip levels 26 | */ 27 | export declare function copySourcesToTexture(device: GPUDevice, texture: GPUTexture, sources: TextureSource[], options?: CopyTextureOptions): void; 28 | /** 29 | * Copies a "source" (Video, Canvas, OffscreenCanvas, ImageBitmap) 30 | * to a texture and then optionally generates mip levels 31 | */ 32 | export declare function copySourceToTexture(device: GPUDevice, texture: GPUTexture, source: TextureSource, options?: CopyTextureOptions): void; 33 | /** 34 | * @property mips if true and mipLevelCount is not set then wll automatically generate 35 | * the correct number of mip levels. 36 | * @property format Defaults to "rgba8unorm" 37 | * @property mipLeveLCount Defaults to 1 or the number of mips needed for a full mipmap if `mips` is true 38 | */ 39 | export type CreateTextureOptions = CopyTextureOptions & { 40 | mips?: boolean; 41 | usage?: GPUTextureUsageFlags; 42 | format?: GPUTextureFormat; 43 | mipLevelCount?: number; 44 | }; 45 | /** 46 | * Gets the size from a source. This is to smooth out the fact that different 47 | * sources have a different way to get their size. 48 | */ 49 | export declare function getSizeFromSource(source: TextureSource, options: CreateTextureOptions): number[]; 50 | /** 51 | * Create a texture from an array of sources (Video, Canvas, OffscreenCanvas, ImageBitmap) 52 | * and optionally create mip levels. If you set `mips: true` and don't set a mipLevelCount 53 | * then it will automatically make the correct number of mip levels. 54 | * 55 | * Example: 56 | * 57 | * ```js 58 | * const texture = createTextureFromSource( 59 | * device, 60 | * [ 61 | * someCanvasOrVideoOrImageImageBitmap0, 62 | * someCanvasOrVideoOrImageImageBitmap1, 63 | * ], 64 | * { 65 | * usage: GPUTextureUsage.TEXTURE_BINDING | 66 | * GPUTextureUsage.RENDER_ATTACHMENT | 67 | * GPUTextureUsage.COPY_DST, 68 | * mips: true, 69 | * } 70 | * ); 71 | * ``` 72 | * 73 | * Note: If you are supporting compatibility mode you will need to pass in your 74 | * intended view dimension for cubemaps. Example: 75 | * 76 | * ```js 77 | * const texture = createTextureFromSource( 78 | * device, 79 | * [ 80 | * someCanvasOrVideoOrImageImageBitmapPosX, 81 | * someCanvasOrVideoOrImageImageBitmapNegY, 82 | * someCanvasOrVideoOrImageImageBitmapPosY, 83 | * someCanvasOrVideoOrImageImageBitmapNegY, 84 | * someCanvasOrVideoOrImageImageBitmapPosZ, 85 | * someCanvasOrVideoOrImageImageBitmapNegZ, 86 | * ], 87 | * { 88 | * usage: GPUTextureUsage.TEXTURE_BINDING | 89 | * GPUTextureUsage.RENDER_ATTACHMENT | 90 | * GPUTextureUsage.COPY_DST, 91 | * mips: true, 92 | * viewDimension: 'cube', // <=- Required for compatibility mode 93 | * } 94 | * ); 95 | * ``` 96 | */ 97 | export declare function createTextureFromSources(device: GPUDevice, sources: TextureSource[], options?: CreateTextureOptions): GPUTexture; 98 | /** 99 | * Create a texture from a source (Video, Canvas, OffscreenCanvas, ImageBitmap) 100 | * and optionally create mip levels. If you set `mips: true` and don't set a mipLevelCount 101 | * then it will automatically make the correct number of mip levels. 102 | * 103 | * Example: 104 | * 105 | * ```js 106 | * const texture = createTextureFromSource( 107 | * device, 108 | * someCanvasOrVideoOrImageImageBitmap, 109 | * { 110 | * usage: GPUTextureUsage.TEXTURE_BINDING | 111 | * GPUTextureUsage.RENDER_ATTACHMENT | 112 | * GPUTextureUsage.COPY_DST, 113 | * mips: true, 114 | * } 115 | * ); 116 | * ``` 117 | */ 118 | export declare function createTextureFromSource(device: GPUDevice, source: TextureSource, options?: CreateTextureOptions): GPUTexture; 119 | export type CreateTextureFromBitmapOptions = CreateTextureOptions & ImageBitmapOptions; 120 | /** 121 | * Load an ImageBitmap 122 | * @param url 123 | * @param options 124 | * @returns the loaded ImageBitmap 125 | */ 126 | export declare function loadImageBitmap(url: string, options?: ImageBitmapOptions): Promise; 127 | /** 128 | * Load images and create a texture from them, optionally generating mip levels 129 | * 130 | * Assumes all the urls reference images of the same size. 131 | * 132 | * Example: 133 | * 134 | * ```js 135 | * const texture = await createTextureFromImage( 136 | * device, 137 | * [ 138 | * 'https://someimage1.url', 139 | * 'https://someimage2.url', 140 | * ], 141 | * { 142 | * mips: true, 143 | * flipY: true, 144 | * }, 145 | * ); 146 | * ``` 147 | */ 148 | export declare function createTextureFromImages(device: GPUDevice, urls: string[], options?: CreateTextureFromBitmapOptions): Promise; 149 | /** 150 | * Load an image and create a texture from it, optionally generating mip levels 151 | * 152 | * Example: 153 | * 154 | * ```js 155 | * const texture = await createTextureFromImage(device, 'https://someimage.url', { 156 | * mips: true, 157 | * flipY: true, 158 | * }); 159 | * ``` 160 | */ 161 | export declare function createTextureFromImage(device: GPUDevice, url: string, options?: CreateTextureFromBitmapOptions): Promise; 162 | -------------------------------------------------------------------------------- /test/webgpu.js: -------------------------------------------------------------------------------- 1 | import { assertTruthy } from './assert.js'; 2 | 3 | /* global GPUDevice */ 4 | /* global GPUBufferUsage */ 5 | /* global GPUMapMode */ 6 | 7 | const deviceToResources = new Map(); 8 | 9 | function addDeviceResource(device, resource) { 10 | const resources = deviceToResources.get(device) || []; 11 | deviceToResources.set(device, resources); 12 | resources.push(resource); 13 | return resource; 14 | } 15 | 16 | function freeDeviceResources(device) { 17 | const resources = deviceToResources.get(device) || []; 18 | resources.forEach(r => r.destroy()); 19 | } 20 | 21 | GPUDevice.prototype.createTexture = (function (origFn) { 22 | return function (...args) { 23 | return addDeviceResource(this, origFn.call(this, ...args)); 24 | }; 25 | })(GPUDevice.prototype.createTexture); 26 | 27 | GPUDevice.prototype.createBuffer = (function (origFn) { 28 | return function (...args) { 29 | return addDeviceResource(this, origFn.call(this, ...args)); 30 | }; 31 | })(GPUDevice.prototype.createBuffer); 32 | 33 | export function testWithDeviceWithOptions(options, fn, ...args) { 34 | return async function () { 35 | const adapter = await globalThis?.navigator?.gpu?.requestAdapter(options); 36 | const device = await adapter?.requestDevice({ 37 | requiredFeatures: adapter.features, 38 | }); 39 | if (!device) { 40 | this.skip(); 41 | return; 42 | } 43 | device.pushErrorScope('validation'); 44 | 45 | let caughtErr; 46 | try { 47 | await fn.call(this, device, ...args); 48 | } catch (e) { 49 | caughtErr = e; 50 | } finally { 51 | const error = await device.popErrorScope(); 52 | freeDeviceResources(device); 53 | device.destroy(); 54 | assertTruthy(!error, error?.message); 55 | } 56 | if (caughtErr) { 57 | throw caughtErr; 58 | } 59 | }; 60 | } 61 | 62 | export function testWithDevice(fn, ...args) { 63 | return async function () { 64 | await testWithDeviceWithOptions({}, fn, ...args).call(this); 65 | }; 66 | } 67 | 68 | export function testWithDeviceAndDocument(fn) { 69 | return async function () { 70 | if (typeof document === 'undefined') { 71 | this.skip(); 72 | return; 73 | } 74 | await testWithDevice(fn, document).call(this); 75 | }; 76 | } 77 | 78 | export const roundUp = (v, r) => Math.ceil(v / r) * r; 79 | export const mipValueSize = (v, mipLevel) => Math.max(1, Math.floor(v / 2 ** mipLevel)); 80 | export const mipSize = (texture, mipLevel) => [ 81 | mipValueSize(texture.width, mipLevel), 82 | mipValueSize(texture.height, mipLevel), 83 | texture.dimension === '3d' 84 | ? mipValueSize(texture.depthOrArrayLayers, mipLevel) 85 | : 1, 86 | ]; 87 | 88 | function getTextureFormatInfo(texture) { 89 | const m = /^([a-z]+)(\d+)([a-z]+)$/.exec(texture.format); 90 | const [, channels, bits, encoding] = m; 91 | const numChannels = channels.length; 92 | const bytesPerPixel = bits * numChannels / 8; 93 | 94 | const types = { 95 | unorm: { 96 | '8': Uint8Array, 97 | '16': Uint16Array, 98 | '32': Uint32Array, 99 | }, 100 | snorm: { 101 | '8': Int8Array, 102 | '16': Int16Array, 103 | '32': Int32Array, 104 | }, 105 | uint: { 106 | '8': Uint8Array, 107 | '16': Uint16Array, 108 | '32': Uint32Array, 109 | }, 110 | sint: { 111 | '8': Int8Array, 112 | '16': Int16Array, 113 | '32': Int32Array, 114 | }, 115 | float: { 116 | '16': Float16Array, 117 | '32': Float32Array, 118 | }, 119 | }; 120 | const Ctor = types[encoding]?.[bits]; 121 | if (!Ctor) { 122 | throw new Error(`Unsupported texture format: ${texture.format}`); 123 | } 124 | 125 | return { 126 | numChannels, 127 | bits, 128 | encoding, 129 | bytesPerPixel, 130 | Ctor, 131 | }; 132 | } 133 | 134 | export async function readTexturePadded(device, texture, mipLevel = 0, layer = 0) { 135 | // Super hacky 136 | const { bytesPerPixel, Ctor } = getTextureFormatInfo(texture); 137 | const size = mipSize(texture, mipLevel); 138 | const bytesPerRow = roundUp(size[0] * bytesPerPixel, 256); 139 | const buffer = device.createBuffer({ 140 | size: bytesPerRow * size[1] * size[2], 141 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, 142 | }); 143 | const encoder = device.createCommandEncoder(); 144 | encoder.copyTextureToBuffer( 145 | { texture, mipLevel, origin: [0, 0, layer] }, 146 | { buffer, bytesPerRow, rowsPerImage: size[1] }, 147 | size, 148 | ); 149 | device.queue.submit([encoder.finish()]); 150 | 151 | await buffer.mapAsync(GPUMapMode.READ); 152 | // Get a copy of the result because on unmap the view dies. 153 | const result = new Ctor(buffer.getMappedRange()).slice(); 154 | buffer.unmap(); 155 | buffer.destroy(); 156 | return result; 157 | } 158 | 159 | export async function readTextureUnpadded(device, texture, mipLevel = 0, layer = 0) { 160 | const { bytesPerPixel, Ctor } = getTextureFormatInfo(texture); 161 | const size = mipSize(texture, mipLevel); 162 | const bytesPerRow = size[0] * bytesPerPixel; 163 | const paddedBytesPerRow = roundUp(bytesPerRow, 256); 164 | const bytesPerImage = bytesPerRow * size[1]; 165 | 166 | const padded = new Uint8Array((await readTexturePadded(device, texture, mipLevel, layer)).buffer); 167 | const unpadded = new Uint8Array(bytesPerImage * size[2]); 168 | let paddedOffset = 0; 169 | let unpaddedOffset = 0; 170 | for (let z = 0; z < size[2]; ++z) { 171 | for (let y = 0; y < size[1]; ++y) { 172 | unpadded.set(padded.subarray(paddedOffset, paddedOffset + bytesPerRow), unpaddedOffset); 173 | unpaddedOffset += bytesPerRow; 174 | paddedOffset += paddedBytesPerRow; 175 | } 176 | } 177 | return new Ctor(unpadded.buffer); 178 | } 179 | 180 | export async function readBuffer(device, srcBuffer) { 181 | const copyBuffer = device.createBuffer({ 182 | size: srcBuffer.size, 183 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, 184 | }); 185 | const encoder = device.createCommandEncoder(); 186 | encoder.copyBufferToBuffer(srcBuffer, 0, copyBuffer, 0, srcBuffer.size); 187 | device.queue.submit([encoder.finish()]); 188 | await copyBuffer.mapAsync(GPUMapMode.READ); 189 | // Get a copy of the result because on unmap the view dies. 190 | const result = new Uint8Array(copyBuffer.getMappedRange()).slice(); 191 | copyBuffer.unmap(); 192 | copyBuffer.destroy(); 193 | return result; 194 | } -------------------------------------------------------------------------------- /examples/cube-map.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | import { mat4 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 3 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 4 | import GUI from './3rdparty/muigui-0.x.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct MyVSOutput { 27 | @builtin(position) position: vec4f, 28 | @location(0) pos: vec4f, 29 | }; 30 | 31 | @vertex 32 | fn myVSMain(@builtin(vertex_index) vNdx: u32) -> MyVSOutput { 33 | let pos = array( 34 | vec2f(-1, 3), 35 | vec2f(-1, -1), 36 | vec2f( 3, -1), 37 | ); 38 | let p = vec4f(pos[vNdx], 0, 1); 39 | var vsOut: MyVSOutput; 40 | vsOut.position = p; 41 | vsOut.pos = p; 42 | return vsOut; 43 | } 44 | 45 | struct FSUniforms { 46 | viewDirectionProjectionInverse: mat4x4f, 47 | }; 48 | 49 | @group(0) @binding(1) var fsUniforms: FSUniforms; 50 | @group(0) @binding(2) var diffuseSampler: sampler; 51 | @group(0) @binding(3) var diffuseTexture: texture_cube; 52 | 53 | @fragment 54 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 55 | let t = fsUniforms.viewDirectionProjectionInverse * v.pos; 56 | return textureSample( 57 | diffuseTexture, 58 | diffuseSampler, 59 | normalize(t.xyz / t.w), 60 | ); 61 | } 62 | `; 63 | 64 | const module = device.createShaderModule({code}); 65 | 66 | const pipeline = device.createRenderPipeline({ 67 | layout: 'auto', 68 | vertex: { 69 | module, 70 | entryPoint: 'myVSMain', 71 | }, 72 | fragment: { 73 | module, 74 | entryPoint: 'myFSMain', 75 | targets: [ 76 | {format: presentationFormat}, 77 | ], 78 | }, 79 | primitive: { 80 | topology: 'triangle-list', 81 | }, 82 | }); 83 | 84 | const sampler = device.createSampler({ 85 | magFilter: 'linear', 86 | minFilter: 'linear', 87 | mipmapFilter: 'linear', 88 | }); 89 | 90 | const texture = await wgh.createTextureFromImages(device, [ 91 | 'images/little_paris_under_tower/px.jpeg', 92 | 'images/little_paris_under_tower/nx.jpeg', 93 | 'images/little_paris_under_tower/py.jpeg', 94 | 'images/little_paris_under_tower/ny.jpeg', 95 | 'images/little_paris_under_tower/pz.jpeg', 96 | 'images/little_paris_under_tower/nz.jpeg', 97 | // 'images/niagarafalls2s/posx.jpg', 98 | // 'images/niagarafalls2s/negx.jpg', 99 | // 'images/niagarafalls2s/posy.jpg', 100 | // 'images/niagarafalls2s/negy.jpg', 101 | // 'images/niagarafalls2s/posz.jpg', 102 | // 'images/niagarafalls2s/negz.jpg', 103 | // 'images/yokohama/posx.jpg', 104 | // 'images/yokohama/negx.jpg', 105 | // 'images/yokohama/posy.jpg', 106 | // 'images/yokohama/negy.jpg', 107 | // 'images/yokohama/posz.jpg', 108 | // 'images/yokohama/negz.jpg', 109 | ], { 110 | mips: true, 111 | viewDimension: 'cube', 112 | }); 113 | 114 | const defs = wgh.makeShaderDataDefinitions(code); 115 | const fsUniformValues = wgh.makeStructuredView(defs.uniforms.fsUniforms); 116 | 117 | const fsUniformBuffer = device.createBuffer({ 118 | size: fsUniformValues.arrayBuffer.byteLength, 119 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 120 | }); 121 | 122 | const bindGroup = device.createBindGroup({ 123 | layout: pipeline.getBindGroupLayout(0), 124 | entries: [ 125 | { binding: 1, resource: { buffer: fsUniformBuffer } }, 126 | { binding: 2, resource: sampler }, 127 | { binding: 3, resource: texture.createView({ dimension: 'cube' }) }, 128 | ], 129 | }); 130 | 131 | const renderPassDescriptor = { 132 | colorAttachments: [ 133 | { 134 | // view: undefined, // Assigned later 135 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 136 | loadOp: 'clear', 137 | storeOp: 'store', 138 | }, 139 | ], 140 | }; 141 | 142 | const settings = { 143 | fov: 90, 144 | vertical: 0, 145 | }; 146 | const gui = new GUI(); 147 | gui.add(settings, 'fov', 1, 179); 148 | gui.add(settings, 'vertical', -3, 3).name('up/down'); 149 | 150 | function render(time) { 151 | time *= 0.001; 152 | 153 | const orbitSpeed = time * 0.1; 154 | const radius = 20; 155 | const projection = mat4.perspective(settings.fov * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); 156 | const eye = [Math.cos(orbitSpeed) * radius, 0, Math.sin(orbitSpeed) * radius]; 157 | const target = [0, settings.vertical * radius, 0]; 158 | const up = [0, 1, 0]; 159 | 160 | const mat = fsUniformValues.views.viewDirectionProjectionInverse; 161 | mat4.lookAt(eye, target, up, mat); 162 | mat4.setTranslation(mat, [0, 0, 0], mat); 163 | mat4.multiply(projection, mat, mat); 164 | mat4.inverse(mat, mat); 165 | 166 | device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues.arrayBuffer); 167 | 168 | const canvasTexture = context.getCurrentTexture(); 169 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 170 | 171 | const commandEncoder = device.createCommandEncoder(); 172 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 173 | passEncoder.setPipeline(pipeline); 174 | passEncoder.setBindGroup(0, bindGroup); 175 | passEncoder.draw(3); 176 | passEncoder.end(); 177 | device.queue.submit([commandEncoder.finish()]); 178 | 179 | requestAnimationFrame(render); 180 | } 181 | requestAnimationFrame(render); 182 | 183 | const observer = new ResizeObserver(entries => { 184 | for (const entry of entries) { 185 | const canvas = entry.target; 186 | const width = entry.contentBoxSize[0].inlineSize; 187 | const height = entry.contentBoxSize[0].blockSize; 188 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 189 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 190 | } 191 | }); 192 | observer.observe(canvas); 193 | } 194 | 195 | function fail(msg) { 196 | const elem = document.querySelector('#fail'); 197 | elem.style.display = ''; 198 | elem.children[0].textContent = msg; 199 | } 200 | 201 | 202 | main(); 203 | -------------------------------------------------------------------------------- /resources/css/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | iframe { 6 | border: none; 7 | display: block; 8 | position: fixed; 9 | left: 0; 10 | top: 0; 11 | width: 100%; 12 | height: 100%; 13 | z-index: -1; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | font-family: Helvetica, Arial, sanf-serif; 19 | line-height: 1.5em; 20 | } 21 | html { 22 | background-color: rgb(51, 102, 204); 23 | } 24 | 25 | .nav ul { 26 | list-style: none; 27 | background-color: #444; 28 | padding: 0; 29 | margin: 0; 30 | } 31 | .nav li { 32 | line-height: 2em; 33 | font-family: sans-serif; 34 | font-size: 1.2em; 35 | border-bottom: 1px solid #888; 36 | } 37 | 38 | .nav a { 39 | padding-left: 1em; 40 | padding-right: 1em; 41 | text-decoration: none; 42 | color: #fff; 43 | display: block; 44 | transition: .3s background-color; 45 | } 46 | 47 | .nav a:hover { 48 | background-color: #8AF; 49 | color: #000; 50 | } 51 | 52 | .nav a.active { 53 | background-color: #fff; 54 | color: #444; 55 | cursor: default; 56 | } 57 | 58 | @media screen and (min-width: 600px) { 59 | .nav li { 60 | border-bottom: none; 61 | font-size: 1.4em; 62 | } 63 | 64 | /* Option 1 - Display Inline */ 65 | .nav li { 66 | display: inline-block; 67 | margin-right: -4px; 68 | } 69 | } 70 | 71 | 72 | @media screen and (max-width: 400px) { 73 | ul { 74 | padding-left: 1em; 75 | } 76 | } 77 | 78 | h1 { 79 | line-height: 1.5em; 80 | position: relative; 81 | } 82 | h2,h3 { 83 | margin-top: 1.5em; 84 | } 85 | 86 | #pronounce { 87 | position: absolute; 88 | font-size: xx-small; 89 | bottom: 0em; 90 | left: 0px; 91 | line-height: 1em; 92 | height: 1em; 93 | display: block; 94 | } 95 | 96 | #frame { 97 | margin: 10px; 98 | max-width: 800px; 99 | margin: auto; 100 | background-color: rgba(255, 255, 255, 0.9); 101 | } 102 | #content { 103 | padding: 20px; 104 | } 105 | 106 | code { 107 | background-color: #DDD; 108 | color: black; 109 | font-family: monospace; 110 | font-size: larger; 111 | padding: 0.2em; 112 | } 113 | 114 | pre { 115 | background-color: #CCC; 116 | overflow-x: auto; 117 | font-size: small; 118 | } 119 | 120 | .lang-javascript { 121 | font-size: small; 122 | background-color: #CCC; 123 | display: block; 124 | padding: 1em; 125 | line-height: 1.2em; 126 | border: 1px solid #000; 127 | box-shadow: 3px 3px 10px #ccc; 128 | font-family: "Lucida Console", Monaco, monospace; 129 | } 130 | 131 | /* --- Prettify --- */ 132 | pre.prettyprint .nocode { background-color: none; color: #000 } 133 | pre.prettyprint .str { color: #080 } /* string */ 134 | pre.prettyprint .kwd { color: #008 } /* keyword */ 135 | pre.prettyprint .com { color: #800 } /* comment */ 136 | pre.prettyprint .typ { color: #606 } /* type */ 137 | pre.prettyprint .lit { color: #066 } /* literal */ 138 | pre.prettyprint .pun { color: #660 } /* punctuation */ 139 | pre.prettyprint .pln { color: #000 } /* plaintext */ 140 | pre.prettyprint .tag { color: #008 } /* html/xml tag */ 141 | pre.prettyprint .atn { color: #606 } /* attribute name */ 142 | pre.prettyprint .atv { color: #080 } /* attribute value */ 143 | pre.prettyprint .dec { color: #606 } /* decimal */ 144 | pre.prettyprint .var { color: #606 } /* variable name */ 145 | pre.prettyprint .fun { color: #F00 } /* function name */ 146 | 147 | pre.prettyprint ul.modifiedlines { 148 | list-style-type: none; 149 | padding-left: 0; 150 | } 151 | pre.prettyprint ul.modifiedlines li.linemodified { 152 | list-style-type: none; 153 | background-color: #A1EAF6; 154 | } 155 | pre.prettyprint ul.modifiedlines li.linedeleted { 156 | list-style-type: none; 157 | background-color: #F0B9B9; 158 | text-decoration: line-through; 159 | } 160 | 161 | pre.prettyprint ul.modifiedlines li.lineadded { 162 | list-style-type: none; 163 | background-color: #A2EDC9; 164 | } 165 | 166 | 167 | pre.prettyprint, code.prettyprint { 168 | color: #000; 169 | background: #ddd; 170 | border: 1px solid #000; 171 | box-shadow: 3px 3px 10px #ccc; 172 | font-family: "Lucida Console", Monaco, monospace; 173 | width: 95%; 174 | margin: auto; 175 | padding: 1em; 176 | text-align: left; /* override justify on body */ 177 | goverflow: auto; /* allow scroll bar in case of long lines - goes together with white-space: nowrap! */ 178 | white-space: pre; /* was nowrap, prevent line wrapping */ 179 | line-height: 1.2em; 180 | } 181 | @media print{ 182 | pre.prettyprint .str, code.prettyprint .str{color:#060} 183 | pre.prettyprint .kwd, code.prettyprint .kwd{color:#006;font-weight:bold} 184 | pre.prettyprint .com, code.prettyprint .com{color:#600;font-style:italic} 185 | pre.prettyprint .typ, code.prettyprint .typ{color:#404;font-weight:bold} 186 | pre.prettyprint .lit, code.prettyprint .lit{color:#044} 187 | pre.prettyprint .pun, code.prettyprint .pun{color:#440} 188 | pre.prettyprint .pln, code.prettyprint .pln{color:#000} 189 | pre.prettyprint .tag, code.prettyprint .tag{color:#006;font-weight:bold} 190 | pre.prettyprint .atn, code.prettyprint .atn{color:#404} 191 | pre.prettyprint .atv, code.prettyprint .atv{color:#060} 192 | pre.prettyprint, code.prettyprint { 193 | color: #000; 194 | background: #EEE; 195 | font-size: 8pt; 196 | font-family: "Lucida Console", Monaco, monospace; 197 | width: 95%; 198 | margin: auto; 199 | padding: 6px 3px 13px 3px; /* padding-bottom solves hor. scrollbar hiding single line of code in IE6 but causes vert. scrollbar... */ 200 | text-align: left; /* override justify on body */ 201 | goverflow: auto; /* allow scroll bar in case of long lines - goes together with white-space: nowrap! */ 202 | white-space: pre; /* was nowrap, prevent line wrapping */ 203 | line-height: 10pt; 204 | } 205 | } 206 | 207 | @media (prefers-color-scheme: dark) { 208 | #frame { 209 | background-color: rgba(0, 0, 0, 0.9); 210 | color: #ddd; 211 | } 212 | 213 | a { 214 | color: #8AF; 215 | } 216 | 217 | a:visited { 218 | color: #CAF; 219 | } 220 | 221 | code { 222 | background-color: #444; 223 | color: #ddd; 224 | } 225 | 226 | pre { 227 | background-color: #CCC; 228 | } 229 | 230 | .lang-javascript { 231 | background-color: #CCC; 232 | } 233 | 234 | pre.prettyprint, code.prettyprint { 235 | color: #CCC; 236 | background: #333; 237 | box-shadow: 3px 3px 10px #000; 238 | } 239 | 240 | pre.prettyprint .str { color: #b9ca4a } /* string */ 241 | pre.prettyprint .kwd { color: #c397d8 } /* keyword */ 242 | pre.prettyprint .com { color: #f3efb2 } /* comment */ 243 | pre.prettyprint .typ { color: #7aa6da } /* type */ 244 | pre.prettyprint .lit { color: #45e7a6 } /* literal */ 245 | pre.prettyprint .pun { color: #7ecce0 } /* punctuation */ 246 | pre.prettyprint .pln { color: #eaeaea } /* plaintext */ 247 | pre.prettyprint .tag { color: #d54e53 } /* html/xml tag */ 248 | pre.prettyprint .atn { color: #e78c45 } /* attribute name */ 249 | pre.prettyprint .atv { color: #70c0b1 } /* attribute value */ 250 | pre.prettyprint .dec { color: #e78c45 } /* decimal */ 251 | pre.prettyprint .var { color: #d54e53 } /* variable name */ 252 | pre.prettyprint .fun { color: #7aa6da } /* function name */ 253 | } -------------------------------------------------------------------------------- /dist/0.x/buffer-views.d.ts: -------------------------------------------------------------------------------- 1 | import { StructDefinition, TypeDefinition, VariableDefinition } from './data-definitions.js'; 2 | import { TypedArrayConstructor, TypedArray } from './typed-arrays.js'; 3 | type TypeDef = { 4 | numElements: number; 5 | align: number; 6 | size: number; 7 | type: string; 8 | View: TypedArrayConstructor; 9 | flatten?: boolean; 10 | pad?: readonly number[]; 11 | }; 12 | declare const typeInfo: { 13 | readonly [K: string]: TypeDef; 14 | }; 15 | export type kType = Extract; 16 | export declare const kTypes: readonly kType[]; 17 | /** 18 | * Set which intrinsic types to make views for. 19 | * 20 | * Example: 21 | * 22 | * Given a an array of intrinsics like this 23 | * `array` 24 | * 25 | * The default is to create a single `Float32Array(4 * 200)` 26 | * because creating 200 `Float32Array` views is not usually 27 | * what you want. 28 | * 29 | * If you do want individual views then you'd call 30 | * `setIntrinsicsToView(['vec3f`])` and now you get 31 | * an array of 200 `Float32Array`s. 32 | * 33 | * Note: `setIntrinsicsToView` always sets ALL types. The list you 34 | * pass it is the types you want views created for, all other types 35 | * will be reset to do the default. In other words 36 | * 37 | * ```js 38 | * setIntrinsicsToView(['vec3f`]) 39 | * setIntrinsicsToView(['vec2f`]) 40 | * ``` 41 | * 42 | * Only `vec2f` will have views created. `vec3f` has been reset to the default by 43 | * the second call 44 | * 45 | * You can pass in `true` as the 2nd parameter to make it set which types 46 | * to flatten and all others will be set to have views created. 47 | * 48 | * To reset all types to the default call it with no arguments 49 | * 50 | * @param types array of types to make views for 51 | * @param flatten whether to flatten or expand the specified types. 52 | */ 53 | export declare function setIntrinsicsToView(types?: readonly kType[], flatten?: boolean): void; 54 | export type TypedArrayOrViews = TypedArray | Views | Views[]; 55 | export interface Views { 56 | [x: string]: TypedArrayOrViews; 57 | } 58 | export type ArrayBufferViews = { 59 | views: any; // because otherwise this is too much of a PITA to use in typescript 60 | arrayBuffer: ArrayBuffer; 61 | }; 62 | /** 63 | * Creates a set of named TypedArray views on an ArrayBuffer. If you don't 64 | * pass in an ArrayBuffer, one will be created. If you're using an unsized 65 | * array then you must pass in your own arraybuffer 66 | * 67 | * Example: 68 | * 69 | * ```js 70 | * const code = ` 71 | * struct Stuff { 72 | * direction: vec3f, 73 | * strength: f32, 74 | * matrix: mat4x4f, 75 | * }; 76 | * @group(0) @binding(0) var uni: Stuff; 77 | * `; 78 | * const defs = makeShaderDataDefinitions(code); 79 | * const views = makeTypedArrayViews(devs.uniforms.uni.typeDefinition); 80 | * ``` 81 | * 82 | * views would effectively be 83 | * 84 | * ```js 85 | * views = { 86 | * direction: Float32Array(arrayBuffer, 0, 3), 87 | * strength: Float32Array(arrayBuffer, 3, 4), 88 | * matrix: Float32Array(arraybuffer, 4, 20), 89 | * }; 90 | * ``` 91 | * 92 | * You can use the views directly or you can use @link {setStructuredView} 93 | * 94 | * @param typeDef Definition of the various types of views. 95 | * @param arrayBuffer Optional ArrayBuffer to use (if one provided one will be created) 96 | * @param offset Optional offset in existing ArrayBuffer to start the views. 97 | * @returns A bunch of named TypedArray views and the ArrayBuffer 98 | */ 99 | export declare function makeTypedArrayViews(typeDef: TypeDefinition, arrayBuffer?: ArrayBuffer, offset?: number): ArrayBufferViews; 100 | /** 101 | * Given a set of TypeArrayViews and matching JavaScript data 102 | * sets the content of the views. 103 | * 104 | * Example: 105 | * 106 | * ```js 107 | * const code = ` 108 | * struct Stuff { 109 | * direction: vec3f, 110 | * strength: f32, 111 | * matrix: mat4x4f, 112 | * }; 113 | * @group(0) @binding(0) var uni: Stuff; 114 | * `; 115 | * const defs = makeShaderDataDefinitions(code); 116 | * const views = makeTypedArrayViews(devs.uniforms.uni.typeDefinition); 117 | * 118 | * setStructuredViews({ 119 | * direction: [1, 2, 3], 120 | * strength: 45, 121 | * matrix: [ 122 | * 1, 0, 0, 0, 123 | * 0, 1, 0, 0, 124 | * 0, 0, 1, 0, 125 | * 0, 0, 0, 1, 126 | * ], 127 | * }); 128 | * ``` 129 | * 130 | * The code above will set the various views, which all point to different 131 | * locations within the same array buffer. 132 | * 133 | * See @link {makeTypedArrayViews}. 134 | * 135 | * @param data The new values 136 | * @param views TypedArray views as returned from {@link makeTypedArrayViews} 137 | */ 138 | export declare function setStructuredView(data: any, views: TypedArrayOrViews): void; 139 | export type StructuredView = ArrayBufferViews & { 140 | /** 141 | * Sets the contents of the TypedArrays based on the data passed in 142 | * Note: The data may be sparse 143 | * 144 | * example: 145 | * 146 | * ```js 147 | * const code = ` 148 | * struct HSL { 149 | * hue: f32, 150 | * sat: f32, 151 | * lum: f32, 152 | * }; 153 | * struct MyUniforms { 154 | * colors: array, 155 | * brightness: f32, 156 | * kernel: array, 157 | * }; 158 | * @group(0) @binding(0) var myUniforms: MyUniforms; 159 | * `; 160 | * const defs = makeShaderDataDefinitions(code); 161 | * const myUniformValues = makeStructuredView(defs.uniforms.myUniforms); 162 | * 163 | * myUniformValues.set({ 164 | * colors: [ 165 | * , 166 | * , 167 | * { hue: 0.5, sat: 1.0, lum: 0.5 }, // only set the 3rd color 168 | * ], 169 | * brightness: 0.8, 170 | * kernel: [ 171 | * 1, 0, -1, 172 | * 2, 0, -2, 173 | * 1, 0, -1, 174 | * ], 175 | * }); 176 | * ``` 177 | * 178 | * @param data 179 | */ 180 | set(data: any): void; 181 | }; 182 | /** 183 | * Given a VariableDefinition, create matching TypedArray views 184 | * @param varDef A VariableDefinition as returned from {@link makeShaderDataDefinitions} 185 | * @param arrayBuffer Optional ArrayBuffer for the views 186 | * @param offset Optional offset into the ArrayBuffer for the views 187 | * @returns TypedArray views for the various named fields of the structure as well 188 | * as a `set` function to make them easy to set, and the arrayBuffer 189 | */ 190 | export declare function makeStructuredView(varDef: VariableDefinition | StructDefinition, arrayBuffer?: ArrayBuffer, offset?: number): StructuredView; 191 | /** 192 | * Sets values on an existing array buffer from a TypeDefinition 193 | * @param typeDef A type definition provided by @link {makeShaderDataDefinitions} 194 | * @param data The source data 195 | * @param arrayBuffer The arrayBuffer who's data to set. 196 | * @param offset An offset in the arrayBuffer to start at. 197 | */ 198 | export declare function setTypedValues(typeDef: TypeDefinition, data: any, arrayBuffer: ArrayBuffer, offset?: number): void; 199 | /** 200 | * Same as @link {setTypedValues} except it takes a @link {VariableDefinition}. 201 | * @param typeDef A variable definition provided by @link {makeShaderDataDefinitions} 202 | * @param data The source data 203 | * @param arrayBuffer The arrayBuffer who's data to set. 204 | * @param offset An offset in the arrayBuffer to start at. 205 | */ 206 | export declare function setStructuredValues(varDef: VariableDefinition, data: any, arrayBuffer: ArrayBuffer, offset?: number): void; 207 | export {}; 208 | -------------------------------------------------------------------------------- /dist/1.x/data-definitions.d.ts: -------------------------------------------------------------------------------- 1 | import { WGSLType } from './wgsl-types.js'; 2 | export type FieldDefinition = { 3 | offset: number; 4 | type: TypeDefinition; 5 | }; 6 | export type FieldDefinitions = { 7 | [x: string]: FieldDefinition; 8 | }; 9 | export type TypeDefinition = { 10 | size: number; 11 | }; 12 | export type StructDefinition = TypeDefinition & { 13 | fields: FieldDefinitions; 14 | size: number; 15 | }; 16 | export { WGSLType }; 17 | export type IntrinsicDefinition = TypeDefinition & { 18 | type: WGSLType; 19 | numElements?: number; 20 | }; 21 | export type ArrayDefinition = TypeDefinition & { 22 | elementType: TypeDefinition; 23 | numElements: number; 24 | }; 25 | export type TextureDefinition = TypeDefinition & { 26 | type: string; 27 | }; 28 | export type SamplerDefinition = TypeDefinition & { 29 | type: string; 30 | }; 31 | /** 32 | * @group(x) @binding(y) var<...> definition 33 | */ 34 | export interface VariableDefinition { 35 | binding: number; 36 | group: number; 37 | size: number; 38 | typeDefinition: TypeDefinition; 39 | } 40 | export type StructDefinitions = { 41 | [x: string]: StructDefinition; 42 | }; 43 | export type VariableDefinitions = { 44 | [x: string]: VariableDefinition; 45 | }; 46 | export type Resource = { 47 | name: string; 48 | group: number; 49 | entry: GPUBindGroupLayoutEntry; 50 | }; 51 | export type EntryPoint = { 52 | stage: GPUShaderStageFlags; 53 | resources: Resource[]; 54 | }; 55 | export type EntryPoints = { 56 | [x: string]: EntryPoint; 57 | }; 58 | /** 59 | * The data definitions and other reflection data from some WGSL shader source. 60 | */ 61 | export type ShaderDataDefinitions = { 62 | /** 63 | * definitions for uniform bindings by name 64 | */ 65 | uniforms: VariableDefinitions; 66 | /** 67 | * definitions for storage bindings by name 68 | */ 69 | storages: VariableDefinitions; 70 | /** 71 | * definitions for sampler bindings by name 72 | */ 73 | samplers: VariableDefinitions; 74 | /** 75 | * definitions for texture bindings by name 76 | */ 77 | textures: VariableDefinitions; 78 | /** 79 | * definitions for storage texture bindings by name 80 | */ 81 | storageTextures: VariableDefinitions; 82 | /** 83 | * definitions for external texture bindings by name 84 | */ 85 | externalTextures: VariableDefinitions; 86 | /** 87 | * definitions for structures by name 88 | */ 89 | structs: StructDefinitions; 90 | /** 91 | * Entry points by name. 92 | */ 93 | entryPoints: EntryPoints; 94 | }; 95 | /** 96 | * This should be compatible with GPUProgramableStage 97 | */ 98 | export type ProgrammableStage = { 99 | entryPoint?: string; 100 | }; 101 | /** 102 | * Compatible with GPURenderPipelineDescriptor and GPUComputePipelineDescriptor 103 | */ 104 | export type PipelineDescriptor = { 105 | vertex?: ProgrammableStage; 106 | fragment?: ProgrammableStage; 107 | compute?: ProgrammableStage; 108 | }; 109 | /** 110 | * Gets GPUBindGroupLayoutDescriptors for the given pipeline. 111 | * 112 | * Important: Assumes your pipeline is valid (it doesn't check for errors). 113 | * 114 | * Note: In WebGPU some layouts must be specified manually. For example an unfiltered-float 115 | * sampler can not be derived since it is unknown at compile time pipeline creation time 116 | * which texture you'll use. 117 | * 118 | * ```js 119 | * import { 120 | * makeShaderDataDefinitions, 121 | * makeBindGroupLayoutDescriptors, 122 | * } from 'webgpu-utils'; 123 | * 124 | * const code = ` 125 | * @group(0) @binding(0) var mat: mat4x4f; 126 | * 127 | * struct MyVSOutput { 128 | * @builtin(position) position: vec4f, 129 | * @location(1) texcoord: vec2f, 130 | * }; 131 | * 132 | * @vertex 133 | * fn myVSMain(v: MyVSInput) -> MyVSOutput { 134 | * var vsOut: MyVSOutput; 135 | * vsOut.position = mat * v.position; 136 | * vsOut.texcoord = v.texcoord; 137 | * return vsOut; 138 | * } 139 | * 140 | * @group(0) @binding(2) var diffuseSampler: sampler; 141 | * @group(0) @binding(3) var diffuseTexture: texture_2d; 142 | * 143 | * @fragment 144 | * fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 145 | * return textureSample(diffuseTexture, diffuseSampler, v.texcoord); 146 | * } 147 | * `; 148 | * 149 | * const module = device.createShaderModule({code}); 150 | * const defs = wgh.makeShaderDataDefinitions(code); 151 | * 152 | * const pipelineDesc = { 153 | * vertex: { 154 | * module, 155 | * entryPoint: 'myVSMain', 156 | * buffers: bufferLayouts, 157 | * }, 158 | * fragment: { 159 | * module, 160 | * entryPoint: 'myFSMain', 161 | * targets: [ 162 | * {format: 'rgba8unorm'}, 163 | * ], 164 | * }, 165 | * }; 166 | * 167 | * const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, pipelineDesc); 168 | * const bindGroupLayouts = descriptors.map(desc => device.createBindGroupLayout(desc)); 169 | * const layout = device.createPipelineLayout({ bindGroupLayouts }); 170 | * const pipeline = device.createRenderPipeline({ 171 | * layout, 172 | * ...pipelineDesc, 173 | * }); 174 | * ``` 175 | * 176 | * @param defs ShaderDataDefinitions or an array of ShaderDataDefinitions as 177 | * returned from {@link makeShaderDataDefinitions}. If an array of more than 1 178 | * definition it's assumed the vertex shader is in the first and the fragment 179 | * shader in the second. 180 | * @param desc A PipelineDescriptor. You should be able to pass in the same object you would pass 181 | * to `createRenderPipeline` or `createComputePipeline`. In particular, you need 182 | * the `vertex` / `fragment` or `compute` properties with or without entryPoints. 183 | * The existence of the property means this shader type exists in the pipeline. If 184 | * no entry point is specified the default entry point will be used, which, like 185 | * WebGPU, defaults to the only entry point of that type. If there is more than one 186 | * it's an error and you must specify an entry point. 187 | * @returns An array of GPUBindGroupLayoutDescriptors which you can pass, one at a time, to 188 | * `createBindGroupLayout`. Note: the array will be sparse if there are gaps in group 189 | * numbers. Note: Each GPUBindGroupLayoutDescriptor.entries will be sorted by binding. 190 | */ 191 | export declare function makeBindGroupLayoutDescriptors(defs: ShaderDataDefinitions | ShaderDataDefinitions[], desc: PipelineDescriptor): GPUBindGroupLayoutDescriptor[]; 192 | /** 193 | * Given a WGSL shader, returns data definitions for structures, 194 | * uniforms, and storage buffers 195 | * 196 | * Example: 197 | * 198 | * ```js 199 | * const code = ` 200 | * struct MyStruct { 201 | * color: vec4f, 202 | * brightness: f32, 203 | * kernel: array, 204 | * }; 205 | * @group(0) @binding(0) var myUniforms: MyUniforms; 206 | * `; 207 | * const defs = makeShaderDataDefinitions(code); 208 | * const myUniformValues = makeStructuredView(defs.uniforms.myUniforms); 209 | * 210 | * myUniformValues.set({ 211 | * color: [1, 0, 1, 1], 212 | * brightness: 0.8, 213 | * kernel: [ 214 | * 1, 0, -1, 215 | * 2, 0, -2, 216 | * 1, 0, -1, 217 | * ], 218 | * }); 219 | * device.queue.writeBuffer(uniformBuffer, 0, myUniformValues.arrayBuffer); 220 | * ``` 221 | * 222 | * @param code WGSL shader. Note: it is not required for this to be a complete shader 223 | * @returns definitions of the structures by name. Useful for passing to {@link makeStructuredView} 224 | */ 225 | export declare function makeShaderDataDefinitions(code: string): ShaderDataDefinitions; 226 | -------------------------------------------------------------------------------- /test/tests/generate-mipmap-test.js: -------------------------------------------------------------------------------- 1 | import { describe, it } from '../mocha-support.js'; 2 | import { 3 | generateMipmap, 4 | numMipLevels, 5 | } from '../../dist/2.x/webgpu-utils.module.js'; 6 | import { assertArrayEqualApproximately, assertEqual } from '../assert.js'; 7 | import { readTextureUnpadded, testWithDeviceWithOptions } from '../webgpu.js'; 8 | 9 | // prevent global document 10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 11 | const document = undefined; 12 | 13 | /* global GPUTextureUsage */ 14 | 15 | describe('generate-mipmap tests', () => { 16 | 17 | it('returns correct number of mip levels', () => { 18 | assertEqual(numMipLevels([1]), 1); 19 | assertEqual(numMipLevels([2]), 2); 20 | assertEqual(numMipLevels([3]), 2); 21 | assertEqual(numMipLevels([4]), 3); 22 | assertEqual(numMipLevels([4]), 3); 23 | 24 | assertEqual(numMipLevels([1, 1]), 1); 25 | assertEqual(numMipLevels([1, 2]), 2); 26 | assertEqual(numMipLevels([1, 3]), 2); 27 | assertEqual(numMipLevels([1, 4]), 3); 28 | assertEqual(numMipLevels([1, 4]), 3); 29 | 30 | assertEqual(numMipLevels([1, 1, 1]), 1); 31 | assertEqual(numMipLevels([1, 1, 2]), 1); 32 | assertEqual(numMipLevels([1, 1, 3]), 1); 33 | assertEqual(numMipLevels([1, 1, 4]), 1); 34 | assertEqual(numMipLevels([1, 1, 4]), 1); 35 | 36 | assertEqual(numMipLevels([1, 1, 1], '3d'), 1); 37 | assertEqual(numMipLevels([1, 1, 2], '3d'), 2); 38 | assertEqual(numMipLevels([1, 1, 3], '3d'), 2); 39 | assertEqual(numMipLevels([1, 1, 4], '3d'), 3); 40 | assertEqual(numMipLevels([1, 1, 4], '3d'), 3); 41 | 42 | assertEqual(numMipLevels({width: 1}), 1); 43 | assertEqual(numMipLevels({width: 2}), 2); 44 | assertEqual(numMipLevels({width: 3}), 2); 45 | assertEqual(numMipLevels({width: 4}), 3); 46 | assertEqual(numMipLevels({width: 4}), 3); 47 | 48 | assertEqual(numMipLevels({width: 1, height: 1}), 1); 49 | assertEqual(numMipLevels({width: 1, height: 2}), 2); 50 | assertEqual(numMipLevels({width: 1, height: 3}), 2); 51 | assertEqual(numMipLevels({width: 1, height: 4}), 3); 52 | assertEqual(numMipLevels({width: 1, height: 4}), 3); 53 | 54 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 1}, '3d'), 1); 55 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 2}, '3d'), 2); 56 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 3}, '3d'), 2); 57 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 4}, '3d'), 3); 58 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 4}, '3d'), 3); 59 | 60 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 1}), 1); 61 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 2}), 1); 62 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 3}), 1); 63 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 4}), 1); 64 | assertEqual(numMipLevels({width: 1, depthOrArrayLayers: 4}), 1); 65 | 66 | }); 67 | 68 | function test(compatibilityMode) { 69 | const options = compatibilityMode ? { 70 | featureLevel: 'compatibility', 71 | } : {}; 72 | 73 | describe(compatibilityMode ? 'test compatibility mode' : 'test normal WebGPU', () => { 74 | 75 | const r = [255, 0, 0, 255]; 76 | const g = [0, 255, 0, 255]; 77 | const b = [0, 0, 255, 255]; 78 | const y = [255, 255, 0, 255]; 79 | const c = [0, 255, 255, 255]; 80 | const m = [255, 0, 255, 255]; 81 | 82 | const layerData = [ 83 | { 84 | src: new Uint8Array([ 85 | r, r, b, b, 86 | r, r, b, b, 87 | b, b, r, r, 88 | b, b, r, r, 89 | ].flat()), 90 | expected: [128, 0, 128, 255], 91 | }, 92 | { 93 | src: new Uint8Array([ 94 | g, g, b, b, 95 | g, g, b, b, 96 | b, b, g, g, 97 | b, b, g, g, 98 | ].flat()), 99 | expected: [0, 128, 128, 255], 100 | }, 101 | { 102 | src: new Uint8Array([ 103 | y, y, m, m, 104 | y, y, m, m, 105 | m, m, y, y, 106 | m, m, y, y, 107 | ].flat()), 108 | expected: [255, 128, 128, 255], 109 | }, 110 | { 111 | src: new Uint8Array([ 112 | c, c, m, m, 113 | c, c, m, m, 114 | m, m, c, c, 115 | m, m, c, c, 116 | ].flat()), 117 | expected: [128, 128, 255, 255], 118 | }, 119 | { 120 | src: new Uint8Array([ 121 | b, b, y, y, 122 | b, b, y, y, 123 | y, y, b, b, 124 | y, y, b, b, 125 | ].flat()), 126 | expected: [128, 128, 128, 255], 127 | }, 128 | { 129 | src: new Uint8Array([ 130 | g, g, r, r, 131 | g, g, r, r, 132 | r, r, g, g, 133 | r, r, g, g, 134 | ].flat()), 135 | expected: [128, 128, 0, 255], 136 | }, 137 | ]; 138 | 139 | async function testGenerateMipmap(device, textureData, textureOptions = {}) { 140 | const kTextureWidth = 4; 141 | const kTextureHeight = 4; 142 | const size = [kTextureWidth, kTextureHeight, textureData.length]; 143 | const texture = device.createTexture({ 144 | ...textureOptions, 145 | size, 146 | mipLevelCount: numMipLevels(size), 147 | format: 'rgba8unorm', 148 | usage: GPUTextureUsage.TEXTURE_BINDING | 149 | GPUTextureUsage.RENDER_ATTACHMENT | 150 | GPUTextureUsage.COPY_DST | 151 | GPUTextureUsage.COPY_SRC, 152 | }); 153 | 154 | textureData.forEach(({src}, layer) => { 155 | device.queue.writeTexture( 156 | { texture, origin: [0, 0, layer] }, 157 | src, 158 | { bytesPerRow: kTextureWidth * 4 }, 159 | { width: kTextureWidth, height: kTextureHeight }, 160 | ); 161 | }); 162 | generateMipmap(device, texture, textureOptions.textureBindingViewDimension === 'cube' ? 'cube' : undefined); 163 | 164 | const results = await Promise.all(textureData.map((_, layer) => readTextureUnpadded(device, texture, 2, layer))); 165 | 166 | textureData.forEach(({expected}, layer) => { 167 | assertArrayEqualApproximately(results[layer], expected, 1, `for layer: ${layer}`); 168 | }); 169 | 170 | } 171 | 172 | it('generates mipmaps 1 layer', testWithDeviceWithOptions(options, async device => { 173 | await testGenerateMipmap(device, layerData.slice(0, 1)); 174 | })); 175 | 176 | it('generates mipmaps 3 layers', testWithDeviceWithOptions(options, async device => { 177 | await testGenerateMipmap(device, layerData.slice(0, 3)); 178 | })); 179 | 180 | it('generates mipmaps 6 layers (cube)', testWithDeviceWithOptions(options, async device => { 181 | await testGenerateMipmap(device, layerData.slice(0, 6), compatibilityMode ? { textureBindingViewDimension: 'cube' } : {}); 182 | })); 183 | 184 | it('generates mipmaps 6 layers (2d-array)', testWithDeviceWithOptions(options, async device => { 185 | await testGenerateMipmap(device, layerData.slice(0, 6)); 186 | })); 187 | 188 | it('generates mipmaps 12 layers (cube-array)', testWithDeviceWithOptions(options, async device => { 189 | if (!device.features.has('core-features-and-limits')) { 190 | // no cube-array in compat 191 | return; 192 | } 193 | await testGenerateMipmap.call(this, 194 | device, 195 | [ 196 | ...layerData.slice(0, 6), 197 | ...layerData.slice(0, 6).reverse(), 198 | ], 199 | ); 200 | })); 201 | 202 | }); 203 | } 204 | 205 | test(false); 206 | test(true); 207 | 208 | }); 209 | 210 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | export const config = {}; 2 | 3 | export function setConfig(options) { 4 | Object.assign(config, options); 5 | } 6 | 7 | function formatMsg(msg) { 8 | return `${msg}${msg ? ': ' : ''}`; 9 | } 10 | 11 | export function assertTruthy(actual, msg = '') { 12 | if (!actual) { 13 | throw new Error(`${formatMsg(msg)}expected: truthy, actual: ${actual}`); 14 | } 15 | } 16 | 17 | export function assertFalsy(actual, msg = '') { 18 | if (actual) { 19 | throw new Error(`${formatMsg(msg)}expected: falsy, actual: ${actual}`); 20 | } 21 | } 22 | 23 | export function assertStringMatchesRegEx(actual, regex, msg = '') { 24 | if (!regex.test(actual)) { 25 | throw new Error(`${formatMsg(msg)}expected: ${regex}, actual: ${actual}`); 26 | } 27 | } 28 | 29 | export function assertLessThan(actual, expected, msg = '') { 30 | if (actual >= expected) { 31 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be less than: ${expected}`); 32 | } 33 | } 34 | 35 | export function assertEqualApproximately(actual, expected, range = 0.0000001, msg = '') { 36 | const diff = Math.abs(actual - expected); 37 | if (diff > range) { 38 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be less ${range} different than: ${expected}`); 39 | } 40 | } 41 | 42 | export function assertInstanceOf(actual, expectedType, msg = '') { 43 | if (!(actual instanceof expectedType)) { 44 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be of type: ${expectedType.constructor.name}`); 45 | } 46 | } 47 | 48 | export function assertIsArray(actual, msg = '') { 49 | if (!Array.isArray(actual)) { 50 | throw new Error(`${formatMsg(msg)}expected: ${actual} to be an Array`); 51 | } 52 | } 53 | 54 | export function assertEqual(actual, expected, msg = '') { 55 | // I'm sure this is not sufficient 56 | if (Array.isArray(actual) || isTypedArray(actual)) { 57 | assertArrayEqual(actual, expected); 58 | } else if (actual !== expected) { 59 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`); 60 | } 61 | } 62 | 63 | const isTypedArray = (arr) => 64 | arr && typeof arr.length === 'number' && arr.buffer instanceof ArrayBuffer && typeof arr.byteLength === 'number'; 65 | 66 | export function assertDeepEqual(actual, expected, msg = '') { 67 | if (Array.isArray(actual) || isTypedArray(actual)) { 68 | assertArrayEqual(actual, expected, msg); 69 | } else if (typeof actual === 'object') { 70 | const actualKeys = Object.keys(actual).sort(); 71 | const expectedKeys = Object.keys(expected).sort(); 72 | assertArrayEqual(actualKeys, expectedKeys, msg); 73 | for (const key of actualKeys) { 74 | assertDeepEqual(actual[key], expected[key], `${msg} .${key}`); 75 | } 76 | } else { 77 | assertEqual(actual, expected, msg); 78 | } 79 | } 80 | 81 | export function assertStrictEqual(actual, expected, msg = '') { 82 | if (actual !== expected) { 83 | throw new Error(`${formatMsg(msg)}expected: ${expected} to equal actual: ${actual}`); 84 | } 85 | } 86 | 87 | export function assertNotEqual(actual, expected, msg = '') { 88 | if (actual === expected) { 89 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`); 90 | } 91 | } 92 | 93 | export function assertStrictNotEqual(actual, expected, msg = '') { 94 | if (actual === expected) { 95 | throw new Error(`${formatMsg(msg)}expected: ${expected} to not equal actual: ${actual}`); 96 | } 97 | } 98 | 99 | let depth = 0; 100 | export function assertArrayEqual(actual, expected, msg = '') { 101 | depth++; 102 | if (depth > 10) { 103 | // eslint-disable-next-line no-debugger 104 | debugger; 105 | } 106 | assertTruthy(typeof actual.length === 'number'); 107 | if (actual.length !== expected.length) { 108 | throw new Error(`${formatMsg(msg)}expected: array.length ${expected.length} to equal actual.length: ${actual.length}`); 109 | } 110 | const errors = []; 111 | for (let i = 0; i < actual.length; ++i) { 112 | try { 113 | assertDeepEqual(actual[i], expected[i]); 114 | } catch (err) { 115 | errors.push(`${formatMsg(msg)}expected: expected[${i}] ${expected[i]} to equal actual[${i}]: ${actual[i]}, ${err}`); 116 | if (errors.length === 10) { 117 | break; 118 | } 119 | } 120 | } 121 | if (errors.length > 0) { 122 | throw new Error(errors.join('\n')); 123 | } 124 | --depth; 125 | } 126 | 127 | export function assertArrayEqualApproximately(actual, expected, range = 0.0000001, msg = '') { 128 | if (actual.length !== expected.length) { 129 | throw new Error(`${formatMsg(msg)}expected: array.length ${expected.length} to equal actual.length: ${actual.length}`); 130 | } 131 | const errors = []; 132 | for (let i = 0; i < actual.length; ++i) { 133 | if (Math.abs(actual[i] - expected[i]) > range) { 134 | errors.push(`${formatMsg(msg)}expected: expected[${i}] ${expected[i]} to equal actual[${i}]: ${actual[i]}`); 135 | if (errors.length === 10) { 136 | break; 137 | } 138 | } 139 | } 140 | if (errors.length > 0) { 141 | throw new Error(errors.join('\n')); 142 | } 143 | } 144 | 145 | export function assertThrowsWith(func, expectations, msg = '') { 146 | let error = ''; 147 | if (config.throwOnError === false) { 148 | const origFn = console.error; 149 | const errors = []; 150 | console.error = function (...args) { 151 | errors.push(args.join(' ')); 152 | }; 153 | func(); 154 | console.error = origFn; 155 | if (errors.length) { 156 | error = errors.join('\n'); 157 | console.error(error); 158 | } 159 | } else { 160 | try { 161 | func(); 162 | } catch (e) { 163 | console.error(e); // eslint-disable-line 164 | error = e; 165 | } 166 | 167 | } 168 | 169 | if (config.noLint) { 170 | return; 171 | } 172 | 173 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 174 | } 175 | 176 | // check if it throws it throws with x 177 | export function assertIfThrowsItThrowsWith(func, expectations, msg = '') { 178 | let error = ''; 179 | let threw = false; 180 | if (config.throwOnError === false) { 181 | const origFn = console.error; 182 | const errors = []; 183 | console.error = function (...args) { 184 | errors.push(args.join(' ')); 185 | }; 186 | func(); 187 | console.error = origFn; 188 | if (errors.length) { 189 | error = errors.join('\n'); 190 | console.error(error); 191 | } 192 | } else { 193 | try { 194 | func(); 195 | } catch (e) { 196 | console.error(e); // eslint-disable-line 197 | error = e; 198 | threw = true; 199 | } 200 | 201 | } 202 | 203 | if (config.noLint) { 204 | return; 205 | } 206 | 207 | if (!threw) { 208 | return; 209 | } 210 | 211 | assertStringMatchesREs(error.toString().replace(/\n/g, ' '), expectations, msg); 212 | } 213 | 214 | function assertStringMatchesREs(actual, expectations, msg) { 215 | for (const expectation of expectations) { 216 | if (expectation instanceof RegExp) { 217 | if (!expectation.test(actual)) { 218 | throw new Error(`${formatMsg(msg)}expected: ${expectation}, actual: ${actual}`); 219 | } 220 | } 221 | } 222 | 223 | } 224 | export function assertWarnsWith(func, expectations, msg = '') { 225 | const warnings = []; 226 | const origWarnFn = console.warn; 227 | console.warn = function (...args) { 228 | origWarnFn.call(this, ...args); 229 | warnings.push(args.join(' ')); 230 | }; 231 | 232 | let error; 233 | try { 234 | func(); 235 | } catch (e) { 236 | error = e; 237 | } 238 | 239 | console.warn = origWarnFn; 240 | 241 | if (error) { 242 | throw error; 243 | } 244 | 245 | if (config.noLint) { 246 | return; 247 | } 248 | 249 | assertStringMatchesREs(warnings.join(' '), expectations, msg); 250 | } 251 | 252 | export default { 253 | false: assertFalsy, 254 | equal: assertEqual, 255 | matchesRegEx: assertStringMatchesRegEx, 256 | notEqual: assertNotEqual, 257 | throwsWith: assertThrowsWith, 258 | true: assertTruthy, 259 | }; -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /src/generate-mipmap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isTypedArray, 3 | } from './typed-arrays.js'; 4 | 5 | export function guessTextureBindingViewDimensionForTexture( 6 | dimension: GPUTextureDimension | undefined, 7 | depthOrArrayLayers: number, 8 | ): GPUTextureViewDimension { 9 | switch (dimension) { 10 | case '1d': return '1d'; 11 | case '3d': return '3d'; 12 | default: return depthOrArrayLayers > 1 ? '2d-array' : '2d'; 13 | } 14 | } 15 | 16 | function normalizeGPUExtent3Dict(size: GPUExtent3DDict) { 17 | return [size.width, size.height || 1, size.depthOrArrayLayers || 1]; 18 | } 19 | 20 | /** 21 | * Converts a `GPUExtent3D` into an array of numbers 22 | * 23 | * `GPUExtent3D` has two forms `[width, height?, depth?]` or 24 | * `{width: number, height?: number, depthOrArrayLayers?: number}` 25 | * 26 | * You pass one of those in here and it returns an array of 3 numbers 27 | * so that your code doesn't have to deal with multiple forms. 28 | * 29 | * @param size 30 | * @returns an array of 3 numbers, [width, height, depthOrArrayLayers] 31 | */ 32 | export function normalizeGPUExtent3D(size: GPUExtent3D): number[] { 33 | return (Array.isArray(size) || isTypedArray(size)) 34 | ? [...(size as Iterable), 1, 1].slice(0, 3) 35 | : normalizeGPUExtent3Dict(size as GPUExtent3DDict); 36 | } 37 | 38 | /** 39 | * Given a GPUExtent3D returns the number of mip levels needed 40 | * 41 | * @param size 42 | * @returns number of mip levels needed for the given size 43 | */ 44 | export function numMipLevels(size: GPUExtent3D, dimension?: GPUTextureDimension): number { 45 | const sizes = normalizeGPUExtent3D(size); 46 | const maxSize = Math.max(...sizes.slice(0, dimension === '3d' ? 3 : 2)); 47 | return 1 + Math.log2(maxSize) | 0; 48 | } 49 | 50 | // Use a WeakMap so the device can be destroyed and/or lost 51 | const byDevice = new WeakMap(); 52 | 53 | /** 54 | * Generates mip levels from level 0 to the last mip for an existing texture 55 | * 56 | * The texture must have been created with TEXTURE_BINDING and RENDER_ATTACHMENT 57 | * and been created with mip levels 58 | * 59 | * @param device A GPUDevice 60 | * @param texture The texture to create mips for 61 | * @param textureBindingViewDimension This is only needed in compatibility mode 62 | * and it is only needed when the texture is going to be used as a cube map. 63 | */ 64 | export function generateMipmap( 65 | device: GPUDevice, 66 | texture: GPUTexture, 67 | textureBindingViewDimension?: GPUTextureViewDimension) { 68 | let perDeviceInfo = byDevice.get(device); 69 | if (!perDeviceInfo) { 70 | perDeviceInfo = { 71 | pipelineByFormatAndViewDimension: {}, 72 | moduleByViewDimension: {}, 73 | }; 74 | byDevice.set(device, perDeviceInfo); 75 | } 76 | let { 77 | sampler, 78 | module, 79 | } = perDeviceInfo; 80 | const { 81 | pipelineByFormatAndViewDimension, 82 | } = perDeviceInfo; 83 | textureBindingViewDimension = device.features.has('core-features-and-limits') 84 | ? '2d-array' 85 | : textureBindingViewDimension ?? guessTextureBindingViewDimensionForTexture( 86 | texture.dimension, texture.depthOrArrayLayers 87 | ); 88 | if (!module) { 89 | module = device.createShaderModule({ 90 | label: `mip level generation for ${textureBindingViewDimension}`, 91 | code: ` 92 | const faceMat = array( 93 | mat3x3f( 0, 0, -2, 0, -2, 0, 1, 1, 1), // pos-x 94 | mat3x3f( 0, 0, 2, 0, -2, 0, -1, 1, -1), // neg-x 95 | mat3x3f( 2, 0, 0, 0, 0, 2, -1, 1, -1), // pos-y 96 | mat3x3f( 2, 0, 0, 0, 0, -2, -1, -1, 1), // neg-y 97 | mat3x3f( 2, 0, 0, 0, -2, 0, -1, 1, 1), // pos-z 98 | mat3x3f(-2, 0, 0, 0, -2, 0, 1, 1, -1)); // neg-z 99 | 100 | struct VSOutput { 101 | @builtin(position) position: vec4f, 102 | @location(0) texcoord: vec2f, 103 | @location(1) @interpolate(flat, either) baseArrayLayer: u32, 104 | }; 105 | 106 | @vertex fn vs( 107 | @builtin(vertex_index) vertexIndex : u32, 108 | @builtin(instance_index) baseArrayLayer: u32, 109 | ) -> VSOutput { 110 | var pos = array( 111 | vec2f(-1.0, -1.0), 112 | vec2f(-1.0, 3.0), 113 | vec2f( 3.0, -1.0), 114 | ); 115 | 116 | var vsOutput: VSOutput; 117 | let xy = pos[vertexIndex]; 118 | vsOutput.position = vec4f(xy, 0.0, 1.0); 119 | vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5); 120 | vsOutput.baseArrayLayer = baseArrayLayer; 121 | return vsOutput; 122 | } 123 | 124 | @group(0) @binding(0) var ourSampler: sampler; 125 | 126 | @group(0) @binding(1) var ourTexture2d: texture_2d; 127 | @fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f { 128 | return textureSample(ourTexture2d, ourSampler, fsInput.texcoord); 129 | } 130 | 131 | @group(0) @binding(1) var ourTexture2dArray: texture_2d_array; 132 | @fragment fn fs2darray(fsInput: VSOutput) -> @location(0) vec4f { 133 | return textureSample( 134 | ourTexture2dArray, 135 | ourSampler, 136 | fsInput.texcoord, 137 | fsInput.baseArrayLayer); 138 | } 139 | 140 | @group(0) @binding(1) var ourTextureCube: texture_cube; 141 | @fragment fn fscube(fsInput: VSOutput) -> @location(0) vec4f { 142 | return textureSample( 143 | ourTextureCube, 144 | ourSampler, 145 | faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1)); 146 | } 147 | 148 | @group(0) @binding(1) var ourTextureCubeArray: texture_cube_array; 149 | @fragment fn fscubearray(fsInput: VSOutput) -> @location(0) vec4f { 150 | return textureSample( 151 | ourTextureCubeArray, 152 | ourSampler, 153 | faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1), fsInput.baseArrayLayer); 154 | } 155 | `, 156 | }); 157 | 158 | sampler = device.createSampler({ 159 | minFilter: 'linear', 160 | magFilter: 'linear', 161 | }); 162 | Object.assign(perDeviceInfo, { sampler, module }); 163 | } 164 | 165 | const id = `${texture.format}.${textureBindingViewDimension}`; 166 | 167 | if (!pipelineByFormatAndViewDimension[id]) { 168 | const entryPoint = `fs${textureBindingViewDimension.replace(/[\W]/, '')}`; 169 | pipelineByFormatAndViewDimension[id] = device.createRenderPipeline({ 170 | label: `mip level generator pipeline for ${textureBindingViewDimension}`, 171 | layout: 'auto', 172 | vertex: { 173 | module, 174 | }, 175 | fragment: { 176 | module, 177 | entryPoint, 178 | targets: [{ format: texture.format }], 179 | }, 180 | }); 181 | } 182 | const pipeline = pipelineByFormatAndViewDimension[id]; 183 | 184 | const encoder = device.createCommandEncoder({ 185 | label: 'mip gen encoder', 186 | }); 187 | 188 | for (let baseMipLevel = 1; baseMipLevel < texture.mipLevelCount; ++baseMipLevel) { 189 | for (let baseArrayLayer = 0; baseArrayLayer < texture.depthOrArrayLayers; ++baseArrayLayer) { 190 | const bindGroup = device.createBindGroup({ 191 | layout: pipeline.getBindGroupLayout(0), 192 | entries: [ 193 | { binding: 0, resource: sampler }, 194 | { 195 | binding: 1, 196 | resource: texture.createView({ 197 | dimension: textureBindingViewDimension, 198 | baseMipLevel: baseMipLevel - 1, 199 | mipLevelCount: 1, 200 | }), 201 | }, 202 | ], 203 | }); 204 | 205 | const renderPassDescriptor: GPURenderPassDescriptor = { 206 | label: 'mip gen renderPass', 207 | colorAttachments: [ 208 | { 209 | view: texture.createView({ 210 | dimension: '2d', 211 | baseMipLevel, 212 | mipLevelCount: 1, 213 | baseArrayLayer, 214 | arrayLayerCount: 1, 215 | }), 216 | loadOp: 'clear', 217 | storeOp: 'store', 218 | }, 219 | ], 220 | }; 221 | 222 | const pass = encoder.beginRenderPass(renderPassDescriptor); 223 | pass.setPipeline(pipeline); 224 | pass.setBindGroup(0, bindGroup); 225 | pass.draw(3, 1, 0, baseArrayLayer); 226 | pass.end(); 227 | } 228 | } 229 | 230 | const commandBuffer = encoder.finish(); 231 | device.queue.submit([commandBuffer]); 232 | } -------------------------------------------------------------------------------- /examples/cube.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | /* global GPUTextureUsage */ 3 | import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 4 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct VSUniforms { 27 | worldViewProjection: mat4x4f, 28 | worldInverseTranspose: mat4x4f, 29 | }; 30 | @group(0) @binding(0) var vsUniforms: VSUniforms; 31 | 32 | struct MyVSInput { 33 | @location(0) position: vec4f, 34 | @location(1) normal: vec3f, 35 | @location(2) texcoord: vec2f, 36 | }; 37 | 38 | struct MyVSOutput { 39 | @builtin(position) position: vec4f, 40 | @location(0) normal: vec3f, 41 | @location(1) texcoord: vec2f, 42 | }; 43 | 44 | @vertex 45 | fn myVSMain(v: MyVSInput) -> MyVSOutput { 46 | var vsOut: MyVSOutput; 47 | vsOut.position = vsUniforms.worldViewProjection * v.position; 48 | vsOut.normal = (vsUniforms.worldInverseTranspose * vec4f(v.normal, 0.0)).xyz; 49 | vsOut.texcoord = v.texcoord; 50 | return vsOut; 51 | } 52 | 53 | struct FSUniforms { 54 | lightDirection: vec3f, 55 | }; 56 | 57 | @group(0) @binding(1) var fsUniforms: FSUniforms; 58 | @group(0) @binding(2) var diffuseSampler: sampler; 59 | @group(0) @binding(3) var diffuseTexture: texture_2d; 60 | 61 | @fragment 62 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 63 | let diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.texcoord); 64 | let a_normal = normalize(v.normal); 65 | let l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5; 66 | return vec4f(diffuseColor.rgb * l, diffuseColor.a); 67 | } 68 | `; 69 | 70 | const { 71 | buffers, 72 | bufferLayouts, 73 | indexBuffer, 74 | indexFormat, 75 | numElements, 76 | } = wgh.createBuffersAndAttributesFromArrays(device, { 77 | position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 78 | normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 79 | texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 80 | indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23], 81 | }); 82 | 83 | const module = device.createShaderModule({code}); 84 | 85 | const pipeline = device.createRenderPipeline({ 86 | layout: 'auto', 87 | vertex: { 88 | module, 89 | entryPoint: 'myVSMain', 90 | buffers: bufferLayouts, 91 | }, 92 | fragment: { 93 | module, 94 | entryPoint: 'myFSMain', 95 | targets: [ 96 | {format: presentationFormat}, 97 | ], 98 | }, 99 | primitive: { 100 | topology: 'triangle-list', 101 | cullMode: 'back', 102 | }, 103 | depthStencil: { 104 | depthWriteEnabled: true, 105 | depthCompare: 'less', 106 | format: 'depth24plus', 107 | }, 108 | }); 109 | 110 | const sampler = device.createSampler({ 111 | magFilter: 'linear', 112 | minFilter: 'linear', 113 | mipmapFilter: 'linear', 114 | }); 115 | 116 | const texture = await wgh.createTextureFromImage(device, 'images/1024px-Mexican_Talavera_texture.png', { 117 | mips: true, 118 | flipY: true, 119 | }); 120 | 121 | const defs = wgh.makeShaderDataDefinitions(code); 122 | const vsUniformValues = wgh.makeStructuredView(defs.uniforms.vsUniforms); 123 | const fsUniformValues = wgh.makeStructuredView(defs.uniforms.fsUniforms); 124 | 125 | const vsUniformBuffer = device.createBuffer({ 126 | size: vsUniformValues.arrayBuffer.byteLength, 127 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 128 | }); 129 | const fsUniformBuffer = device.createBuffer({ 130 | size: fsUniformValues.arrayBuffer.byteLength, 131 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 132 | }); 133 | 134 | const bindGroup = device.createBindGroup({ 135 | layout: pipeline.getBindGroupLayout(0), 136 | entries: [ 137 | { binding: 0, resource: { buffer: vsUniformBuffer } }, 138 | { binding: 1, resource: { buffer: fsUniformBuffer } }, 139 | { binding: 2, resource: sampler }, 140 | { binding: 3, resource: texture.createView() }, 141 | ], 142 | }); 143 | 144 | const renderPassDescriptor = { 145 | colorAttachments: [ 146 | { 147 | // view: undefined, // Assigned later 148 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 149 | loadOp: 'clear', 150 | storeOp: 'store', 151 | }, 152 | ], 153 | depthStencilAttachment: { 154 | // view: undefined, // Assigned later 155 | depthClearValue: 1, 156 | depthLoadOp: 'clear', 157 | depthStoreOp: 'store', 158 | }, 159 | }; 160 | 161 | let depthTexture; 162 | 163 | function render(time) { 164 | time *= 0.001; 165 | 166 | const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 10); 167 | const eye = [1, 4, -6]; 168 | const target = [0, 0, 0]; 169 | const up = [0, 1, 0]; 170 | 171 | const view = mat4.lookAt(eye, target, up); 172 | const viewProjection = mat4.multiply(projection, view); 173 | const world = mat4.rotationY(time); 174 | mat4.transpose(mat4.inverse(world), vsUniformValues.views.worldInverseTranspose); 175 | mat4.multiply(viewProjection, world, vsUniformValues.views.worldViewProjection); 176 | 177 | fsUniformValues.set({ 178 | lightDirection: vec3.normalize([1, 8, -10]), 179 | }); 180 | 181 | device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues.arrayBuffer); 182 | device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues.arrayBuffer); 183 | 184 | const canvasTexture = context.getCurrentTexture(); 185 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 186 | 187 | // If we don't have a depth texture OR if its size is different 188 | // from the canvasTexture when make a new depth texture 189 | if (!depthTexture || 190 | depthTexture.width !== canvasTexture.width || 191 | depthTexture.height !== canvasTexture.height) { 192 | if (depthTexture) { 193 | depthTexture.destroy(); 194 | } 195 | depthTexture = device.createTexture({ 196 | size: [canvasTexture.width, canvasTexture.height], 197 | format: 'depth24plus', 198 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 199 | }); 200 | } 201 | renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); 202 | 203 | const commandEncoder = device.createCommandEncoder(); 204 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 205 | passEncoder.setPipeline(pipeline); 206 | passEncoder.setBindGroup(0, bindGroup); 207 | passEncoder.setVertexBuffer(0, buffers[0]); 208 | passEncoder.setIndexBuffer(indexBuffer, indexFormat); 209 | passEncoder.drawIndexed(numElements); 210 | passEncoder.end(); 211 | device.queue.submit([commandEncoder.finish()]); 212 | 213 | requestAnimationFrame(render); 214 | } 215 | requestAnimationFrame(render); 216 | 217 | const observer = new ResizeObserver(entries => { 218 | for (const entry of entries) { 219 | const canvas = entry.target; 220 | const width = entry.contentBoxSize[0].inlineSize; 221 | const height = entry.contentBoxSize[0].blockSize; 222 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 223 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 224 | } 225 | }); 226 | observer.observe(canvas); 227 | } 228 | 229 | function fail(msg) { 230 | const elem = document.querySelector('#fail'); 231 | elem.style.display = ''; 232 | elem.children[0].textContent = msg; 233 | } 234 | 235 | 236 | main(); 237 | -------------------------------------------------------------------------------- /dist/0.x/attribute-utils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { TypedArray, TypedArrayConstructor } from './typed-arrays.js'; 3 | /** 4 | * See {@link Arrays} for details 5 | */ 6 | export type FullArraySpec = { 7 | data: number | number[] | TypedArray; 8 | type?: TypedArrayConstructor; 9 | numComponents?: number; 10 | shaderLocation?: number; 11 | normalize?: boolean; 12 | }; 13 | export type ArrayUnion = number | number[] | TypedArray | FullArraySpec; 14 | /** 15 | * Named Arrays 16 | * 17 | * A set of named arrays are passed to various functions like 18 | * {@link createBufferLayoutsFromArrays} and {@link createBuffersAndAttributesFromArrays} 19 | * 20 | * Each array can be 1 of 4 things. A native JavaScript array, a TypedArray, a number, a {@link FullArraySpec} 21 | * 22 | * If it's a native array then, if the name of the array is `indices` the data will be converted 23 | * to a `Uint32Array`, otherwise a `Float32Array`. Use a TypedArray or a {@link FullArraySpec} to choose a different type. 24 | * The {@link FullArraySpec} `type` is only used if it's not already a TypedArray 25 | * 26 | * If it's a native array or a TypedArray or if `numComponents` in a {@link FullArraySpec} is not 27 | * specified it will be guessed. If the name contains 'coord', 'texture' or 'uv' then numComponents will be 2. 28 | * If the name contains 'color' or 'colour' then numComponents will be 4. Otherwise it's 3. 29 | * 30 | * For attribute formats, guesses are made based on type and number of components. The guess is 31 | * based on this table where (d) is the default for that type if `normalize` is not specified 32 | * 33 | * | Type | .. | normalize | 34 | * | ------------ | ----------- | ----------- | 35 | * | Int8Array | sint8 | snorm8 (d) | 36 | * | Uint8Array | uint8 | unorm8 (d) | 37 | * | Int16Array | sint16 | snorm16 (d) | 38 | * | Uint16Array | uint16 | unorm16 (d) | 39 | * | Int32Array | sint32 (d) | snorm32 | 40 | * | Uint32Array | uint32 (d) | unorm32 | 41 | * | Float32Array | float32 (d) | float32 | 42 | * 43 | */ 44 | export type Arrays = { 45 | [key: string]: ArrayUnion; 46 | }; 47 | export type ArraysOptions = { 48 | interleave?: boolean; 49 | stepMode?: GPUVertexStepMode; 50 | usage?: GPUBufferUsageFlags; 51 | shaderLocation?: number; 52 | }; 53 | /** 54 | * Returned by {@link createBuffersAndAttributesFromArrays} 55 | */ 56 | export type BuffersAndAttributes = { 57 | numElements: number; 58 | bufferLayouts: GPUVertexBufferLayout[]; 59 | buffers: GPUBuffer[]; 60 | indexBuffer?: GPUBuffer; 61 | indexFormat?: GPUIndexFormat; 62 | }; 63 | type TypedArrayWithOffsetAndStride = { 64 | data: TypedArray; 65 | offset: number; /** In elements not bytes */ 66 | stride: number; /** In elements not bytes */ 67 | }; 68 | /** 69 | * Given a set of named arrays, generates an array `GPUBufferLayout`s 70 | * 71 | * Examples: 72 | * 73 | * ```js 74 | * const arrays = { 75 | * position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 76 | * normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 77 | * texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 78 | * }; 79 | * 80 | * const { bufferLayouts, typedArrays } = createBufferLayoutsFromArrays(arrays); 81 | * ``` 82 | * 83 | * results in `bufferLayouts` being 84 | * 85 | * ```js 86 | * [ 87 | * { 88 | * stepMode: 'vertex', 89 | * arrayStride: 32, 90 | * attributes: [ 91 | * { shaderLocation: 0, offset: 0, format: 'float32x3' }, 92 | * { shaderLocation: 1, offset: 12, format: 'float32x3' }, 93 | * { shaderLocation: 2, offset: 24, format: 'float32x2' }, 94 | * ], 95 | * }, 96 | * ] 97 | * ``` 98 | * 99 | * and `typedArrays` being 100 | * 101 | * ``` 102 | * [ 103 | * someFloat32Array0, 104 | * someFloat32Array1, 105 | * someFloat32Array2, 106 | * ] 107 | * ``` 108 | * 109 | * See {@link Arrays} for details on the various types of arrays. 110 | * 111 | * Note: If typed arrays are passed in the same typed arrays will come out (copies will not be made) 112 | */ 113 | export declare function createBufferLayoutsFromArrays(arrays: Arrays, options?: ArraysOptions): { 114 | bufferLayouts: GPUVertexBufferLayout[]; 115 | typedArrays: TypedArrayWithOffsetAndStride[]; 116 | }; 117 | /** 118 | * Given an array of `GPUVertexAttribute`s and a corresponding array 119 | * of TypedArrays, interleaves the contents of the typed arrays 120 | * into the given ArrayBuffer 121 | * 122 | * example: 123 | * 124 | * ```js 125 | * const attributes: GPUVertexAttribute[] = [ 126 | * { shaderLocation: 0, offset: 0, format: 'float32x3' }, 127 | * { shaderLocation: 1, offset: 12, format: 'float32x3' }, 128 | * { shaderLocation: 2, offset: 24, format: 'float32x2' }, 129 | * ]; 130 | * const typedArrays = [ 131 | * new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]), 132 | * new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]), 133 | * new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]), 134 | * ]; 135 | * const arrayStride = (3 + 3 + 2) * 4; // pos + nrm + uv 136 | * const arrayBuffer = new ArrayBuffer(arrayStride * 24) 137 | * interleaveVertexData(attributes, typedArrays, arrayStride, arrayBuffer) 138 | * ``` 139 | * 140 | * results in the contents of `arrayBuffer` to be the 3 TypedArrays interleaved 141 | * 142 | * See {@link Arrays} for details on the various types of arrays. 143 | * 144 | * Note: You can generate `attributes` and `typedArrays` above by calling 145 | * {@link createBufferLayoutsFromArrays} 146 | */ 147 | export declare function interleaveVertexData(attributes: GPUVertexAttribute[], typedArrays: (TypedArray | TypedArrayWithOffsetAndStride)[], arrayStride: number, arrayBuffer: ArrayBuffer): void; 148 | /** 149 | * Given arrays, create buffers, fills the buffers with data if provided, optionally 150 | * interleaves the data (the default). 151 | * 152 | * Example: 153 | * 154 | * ```js 155 | * const { 156 | * buffers, 157 | * bufferLayouts, 158 | * indexBuffer, 159 | * indexFormat, 160 | * numElements, 161 | * } = createBuffersAndAttributesFromArrays(device, { 162 | * position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 163 | * normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 164 | * texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 165 | * indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23], 166 | * }); 167 | * ``` 168 | * 169 | * Where `bufferLayouts` will be 170 | * 171 | * ```js 172 | * [ 173 | * { 174 | * stepMode: 'vertex', 175 | * arrayStride: 32, 176 | * attributes: [ 177 | * { shaderLocation: 0, offset: 0, format: 'float32x3' }, 178 | * { shaderLocation: 1, offset: 12, format: 'float32x3' }, 179 | * { shaderLocation: 2, offset: 24, format: 'float32x2' }, 180 | * ], 181 | * }, 182 | * ] 183 | * ``` 184 | * 185 | * * `buffers` will have one `GPUBuffer` of usage `GPUBufferUsage.VERTEX` 186 | * * `indexBuffer` will be `GPUBuffer` of usage `GPUBufferUsage.INDEX` 187 | * * `indexFormat` will be `uint32` (use a full spec or a typedarray of `Uint16Array` if you want 16bit indices) 188 | * * `numElements` will be 36 (this is either the number entries in the array named `indices` or if no 189 | * indices are provided then it's the length of the first array divided by numComponents. See {@link Arrays}) 190 | * 191 | * See {@link Arrays} for details on the various types of arrays. 192 | * Also see the cube and instancing examples. 193 | */ 194 | export declare function createBuffersAndAttributesFromArrays(device: GPUDevice, arrays: Arrays, options?: ArraysOptions): BuffersAndAttributes; 195 | export {}; 196 | -------------------------------------------------------------------------------- /dist/0.x/primitives.d.ts: -------------------------------------------------------------------------------- 1 | import { TypedArray } from './typed-arrays.js'; 2 | import { Arrays } from './attribute-utils.js'; 3 | /** 4 | * A class to provide `push` on a typed array. 5 | * 6 | * example: 7 | * 8 | * ```js 9 | * const positions = new TypedArrayWrapper(new Float32Array(300), 3); 10 | * positions.push(1, 2, 3); // add a position 11 | * positions.push([4, 5, 6]); // add a position 12 | * positions.push(new Float32Array(6)); // add 2 positions 13 | * const data = positions.typedArray; 14 | * ``` 15 | */ 16 | export declare class TypedArrayWrapper { 17 | typedArray: T; 18 | cursor: number; 19 | numComponents: number; 20 | constructor(arr: T, numComponents: number); 21 | get numElements(): number; 22 | push(...data: (number | Iterable)[]): void; 23 | reset(index?: number): void; 24 | } 25 | /** 26 | * Creates XY quad vertices 27 | * 28 | * The default with no parameters will return a 2x2 quad with values from -1 to +1. 29 | * If you want a unit quad with that goes from 0 to 1 you'd call it with 30 | * 31 | * createXYQuadVertices(1, 0.5, 0.5); 32 | * 33 | * If you want a unit quad centered above 0,0 you'd call it with 34 | * 35 | * primitives.createXYQuadVertices(1, 0, 0.5); 36 | * 37 | * @param size the size across the quad. Defaults to 2 which means vertices will go from -1 to +1 38 | * @param xOffset the amount to offset the quad in X 39 | * @param yOffset the amount to offset the quad in Y 40 | * @return the created XY Quad vertices 41 | */ 42 | export declare function createXYQuadVertices(size?: number, xOffset?: number, yOffset?: number): Arrays; 43 | /** 44 | * Creates XZ plane vertices. 45 | * 46 | * The created plane has position, normal, and texcoord data 47 | * 48 | * @param width Width of the plane. Default = 1 49 | * @param depth Depth of the plane. Default = 1 50 | * @param subdivisionsWidth Number of steps across the plane. Default = 1 51 | * @param subdivisionsDepth Number of steps down the plane. Default = 1 52 | * @return The created plane vertices. 53 | */ 54 | export declare function createPlaneVertices(width?: number, depth?: number, subdivisionsWidth?: number, subdivisionsDepth?: number): { 55 | position: Float32Array; 56 | normal: Float32Array; 57 | texcoord: Float32Array; 58 | indices: Uint16Array; 59 | }; 60 | /** 61 | * Creates sphere vertices. 62 | * 63 | * The created sphere has position, normal, and texcoord data 64 | * 65 | * @param radius radius of the sphere. 66 | * @param subdivisionsAxis number of steps around the sphere. 67 | * @param subdivisionsHeight number of vertically on the sphere. 68 | * @param startLatitudeInRadians where to start the 69 | * top of the sphere. 70 | * @param endLatitudeInRadians Where to end the 71 | * bottom of the sphere. 72 | * @param startLongitudeInRadians where to start 73 | * wrapping the sphere. 74 | * @param endLongitudeInRadians where to end 75 | * wrapping the sphere. 76 | * @return The created sphere vertices. 77 | */ 78 | export declare function createSphereVertices(radius?: number, subdivisionsAxis?: number, subdivisionsHeight?: number, startLatitudeInRadians?: number, endLatitudeInRadians?: number, startLongitudeInRadians?: number, endLongitudeInRadians?: number): { 79 | position: Float32Array; 80 | normal: Float32Array; 81 | texcoord: Float32Array; 82 | indices: Uint16Array; 83 | }; 84 | /** 85 | * Creates the vertices and indices for a cube. 86 | * 87 | * The cube is created around the origin. (-size / 2, size / 2). 88 | * 89 | * @param size width, height and depth of the cube. 90 | * @return The created vertices. 91 | */ 92 | export declare function createCubeVertices(size?: number): { 93 | position: Float32Array; 94 | normal: Float32Array; 95 | texcoord: Float32Array; 96 | indices: Uint16Array; 97 | }; 98 | /** 99 | * Creates vertices for a truncated cone, which is like a cylinder 100 | * except that it has different top and bottom radii. A truncated cone 101 | * can also be used to create cylinders and regular cones. The 102 | * truncated cone will be created centered about the origin, with the 103 | * y axis as its vertical axis. . 104 | * 105 | * @param bottomRadius Bottom radius of truncated cone. 106 | * @param topRadius Top radius of truncated cone. 107 | * @param height Height of truncated cone. 108 | * @param radialSubdivisions The number of subdivisions around the 109 | * truncated cone. 110 | * @param verticalSubdivisions The number of subdivisions down the 111 | * truncated cone. 112 | * @param topCap Create top cap. Default = true. 113 | * @param bottomCap Create bottom cap. Default = true. 114 | * @return The created cone vertices. 115 | */ 116 | export declare function createTruncatedConeVertices(bottomRadius?: number, topRadius?: number, height?: number, radialSubdivisions?: number, verticalSubdivisions?: number, topCap?: boolean, bottomCap?: boolean): { 117 | position: Float32Array; 118 | normal: Float32Array; 119 | texcoord: Float32Array; 120 | indices: Uint16Array; 121 | }; 122 | /** 123 | * Creates 3D 'F' vertices. 124 | * An 'F' is useful because you can easily tell which way it is oriented. 125 | * The created 'F' has position, normal, texcoord, and color arrays. 126 | * 127 | * @return The created vertices. 128 | */ 129 | export declare function create3DFVertices(): { 130 | [k: string]: Uint8Array | Uint16Array | Float32Array; 131 | }; 132 | /** 133 | * Creates crescent vertices. 134 | * 135 | * @param verticalRadius The vertical radius of the crescent. 136 | * @param outerRadius The outer radius of the crescent. 137 | * @param innerRadius The inner radius of the crescent. 138 | * @param thickness The thickness of the crescent. 139 | * @param subdivisionsDown number of steps around the crescent. 140 | * @param startOffset Where to start arc. Default 0. 141 | * @param endOffset Where to end arg. Default 1. 142 | * @return The created vertices. 143 | */ 144 | export declare function createCrescentVertices(verticalRadius: 2, outerRadius: 1, innerRadius: 0, thickness: 1, subdivisionsDown: 12, startOffset: 0, endOffset: 1): { 145 | position: Float32Array; 146 | normal: Float32Array; 147 | texcoord: Float32Array; 148 | indices: Uint16Array; 149 | }; 150 | /** 151 | * Creates cylinder vertices. The cylinder will be created around the origin 152 | * along the y-axis. 153 | * 154 | * @param radius Radius of cylinder. 155 | * @param height Height of cylinder. 156 | * @param radialSubdivisions The number of subdivisions around the cylinder. 157 | * @param verticalSubdivisions The number of subdivisions down the cylinder. 158 | * @param topCap Create top cap. Default = true. 159 | * @param bottomCap Create bottom cap. Default = true. 160 | * @return The created vertices. 161 | */ 162 | export declare function createCylinderVertices(radius?: number, height?: number, radialSubdivisions?: number, verticalSubdivisions?: number, topCap?: boolean, bottomCap?: boolean): { 163 | position: Float32Array; 164 | normal: Float32Array; 165 | texcoord: Float32Array; 166 | indices: Uint16Array; 167 | }; 168 | /** 169 | * Creates vertices for a torus 170 | * 171 | * @param radius radius of center of torus circle. 172 | * @param thickness radius of torus ring. 173 | * @param radialSubdivisions The number of subdivisions around the torus. 174 | * @param bodySubdivisions The number of subdivisions around the body torus. 175 | * @param startAngle start angle in radians. Default = 0. 176 | * @param endAngle end angle in radians. Default = Math.PI * 2. 177 | * @return The created vertices. 178 | */ 179 | export declare function createTorusVertices(radius?: number, thickness?: number, radialSubdivisions?: number, bodySubdivisions?: number, startAngle?: number, endAngle?: number): { 180 | position: Float32Array; 181 | normal: Float32Array; 182 | texcoord: Float32Array; 183 | indices: Uint16Array; 184 | }; 185 | /** 186 | * Creates disc vertices. The disc will be in the xz plane, centered at 187 | * the origin. When creating, at least 3 divisions, or pie 188 | * pieces, need to be specified, otherwise the triangles making 189 | * up the disc will be degenerate. You can also specify the 190 | * number of radial pieces `stacks`. A value of 1 for 191 | * stacks will give you a simple disc of pie pieces. If you 192 | * want to create an annulus you can set `innerRadius` to a 193 | * value > 0. Finally, `stackPower` allows you to have the widths 194 | * increase or decrease as you move away from the center. This 195 | * is particularly useful when using the disc as a ground plane 196 | * with a fixed camera such that you don't need the resolution 197 | * of small triangles near the perimeter. For example, a value 198 | * of 2 will produce stacks whose outside radius increases with 199 | * the square of the stack index. A value of 1 will give uniform 200 | * stacks. 201 | * 202 | * @param radius Radius of the ground plane. 203 | * @param divisions Number of triangles in the ground plane (at least 3). 204 | * @param stacks Number of radial divisions (default=1). 205 | * @param innerRadius Default 0. 206 | * @param stackPower Power to raise stack size to for decreasing width. 207 | * @return The created vertices. 208 | */ 209 | export declare function createDiscVertices(radius?: number, divisions?: number, stacks?: number, innerRadius?: number, stackPower?: number): { 210 | position: Float32Array; 211 | normal: Float32Array; 212 | texcoord: Float32Array; 213 | indices: Uint16Array; 214 | }; 215 | -------------------------------------------------------------------------------- /examples/bind-group-layouts.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | /* global GPUTextureUsage */ 3 | import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 4 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct VSUniforms { 27 | worldViewProjection: mat4x4f, 28 | worldInverseTranspose: mat4x4f, 29 | }; 30 | @group(2) @binding(0) var vsUniforms: VSUniforms; 31 | 32 | struct MyVSInput { 33 | @location(0) position: vec4f, 34 | @location(1) normal: vec3f, 35 | @location(2) texcoord: vec2f, 36 | }; 37 | 38 | struct MyVSOutput { 39 | @builtin(position) position: vec4f, 40 | @location(0) normal: vec3f, 41 | @location(1) texcoord: vec2f, 42 | }; 43 | 44 | @vertex 45 | fn myVSMain(v: MyVSInput) -> MyVSOutput { 46 | var vsOut: MyVSOutput; 47 | vsOut.position = vsUniforms.worldViewProjection * v.position; 48 | vsOut.normal = (vsUniforms.worldInverseTranspose * vec4f(v.normal, 0.0)).xyz; 49 | vsOut.texcoord = v.texcoord; 50 | return vsOut; 51 | } 52 | 53 | struct FSUniforms { 54 | lightDirection: vec3f, 55 | }; 56 | 57 | @group(0) @binding(0) var fsUniforms: FSUniforms; 58 | @group(0) @binding(1) var diffuseSampler: sampler; 59 | @group(0) @binding(2) var diffuseTexture: texture_2d; 60 | 61 | @fragment 62 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 63 | let diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.texcoord); 64 | let a_normal = normalize(v.normal); 65 | let l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5; 66 | return vec4f(diffuseColor.rgb * l, diffuseColor.a); 67 | } 68 | `; 69 | 70 | const { 71 | buffers, 72 | bufferLayouts, 73 | indexBuffer, 74 | indexFormat, 75 | numElements, 76 | } = wgh.createBuffersAndAttributesFromArrays(device, { 77 | position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 78 | normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 79 | texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 80 | indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23], 81 | }); 82 | 83 | const module = device.createShaderModule({code}); 84 | 85 | const defs = wgh.makeShaderDataDefinitions(code); 86 | 87 | const pipelineDesc = { 88 | vertex: { 89 | module, 90 | entryPoint: 'myVSMain', 91 | buffers: bufferLayouts, 92 | }, 93 | fragment: { 94 | module, 95 | entryPoint: 'myFSMain', 96 | targets: [ 97 | {format: presentationFormat}, 98 | ], 99 | }, 100 | primitive: { 101 | topology: 'triangle-list', 102 | cullMode: 'back', 103 | }, 104 | depthStencil: { 105 | depthWriteEnabled: true, 106 | depthCompare: 'less', 107 | format: 'depth24plus', 108 | }, 109 | }; 110 | 111 | // NOTE: There really isn't much to see here. The point of being able 112 | // to create GPUBindGroupLayoutDescriptors from WGSL is that you don't 113 | // have to manually specify them. You need GPUBindGroupLayouts to be 114 | // able to use a GPUBindGroup with more than one pipeline. 115 | // This example though, doesn't use multiple pipelines. But it does 116 | // generate group layouts. 117 | 118 | const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, pipelineDesc); 119 | const bindGroupLayouts = descriptors.map(desc => device.createBindGroupLayout(desc)); 120 | const layout = device.createPipelineLayout({ bindGroupLayouts }); 121 | 122 | const pipeline = device.createRenderPipeline({ 123 | layout, 124 | ...pipelineDesc, 125 | }); 126 | 127 | const sampler = device.createSampler({ 128 | magFilter: 'linear', 129 | minFilter: 'linear', 130 | mipmapFilter: 'linear', 131 | }); 132 | 133 | const texture = await wgh.createTextureFromImage(device, 'images/1024px-Mexican_Talavera_texture.png', { 134 | mips: true, 135 | flipY: true, 136 | }); 137 | 138 | const vsUniformValues = wgh.makeStructuredView(defs.uniforms.vsUniforms); 139 | const fsUniformValues = wgh.makeStructuredView(defs.uniforms.fsUniforms); 140 | 141 | const vsUniformBuffer = device.createBuffer({ 142 | size: vsUniformValues.arrayBuffer.byteLength, 143 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 144 | }); 145 | const fsUniformBuffer = device.createBuffer({ 146 | size: fsUniformValues.arrayBuffer.byteLength, 147 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 148 | }); 149 | 150 | const bindGroup0 = device.createBindGroup({ 151 | layout: bindGroupLayouts[0], 152 | entries: [ 153 | { binding: 0, resource: { buffer: fsUniformBuffer } }, 154 | { binding: 1, resource: sampler }, 155 | { binding: 2, resource: texture.createView() }, 156 | ], 157 | }); 158 | 159 | const bindGroup2 = device.createBindGroup({ 160 | layout: bindGroupLayouts[2], 161 | entries: [ 162 | { binding: 0, resource: { buffer: vsUniformBuffer } }, 163 | ], 164 | }); 165 | 166 | const renderPassDescriptor = { 167 | colorAttachments: [ 168 | { 169 | // view: undefined, // Assigned later 170 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 171 | loadOp: 'clear', 172 | storeOp: 'store', 173 | }, 174 | ], 175 | depthStencilAttachment: { 176 | // view: undefined, // Assigned later 177 | depthClearValue: 1, 178 | depthLoadOp: 'clear', 179 | depthStoreOp: 'store', 180 | }, 181 | }; 182 | 183 | let depthTexture; 184 | 185 | function render(time) { 186 | time *= 0.001; 187 | 188 | const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 10); 189 | const eye = [1, 4, -6]; 190 | const target = [0, 0, 0]; 191 | const up = [0, 1, 0]; 192 | 193 | const view = mat4.lookAt(eye, target, up); 194 | const viewProjection = mat4.multiply(projection, view); 195 | const world = mat4.rotationY(time); 196 | mat4.transpose(mat4.inverse(world), vsUniformValues.views.worldInverseTranspose); 197 | mat4.multiply(viewProjection, world, vsUniformValues.views.worldViewProjection); 198 | 199 | fsUniformValues.set({ 200 | lightDirection: vec3.normalize([1, 8, -10]), 201 | }); 202 | 203 | device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues.arrayBuffer); 204 | device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues.arrayBuffer); 205 | 206 | const canvasTexture = context.getCurrentTexture(); 207 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 208 | 209 | // If we don't have a depth texture OR if its size is different 210 | // from the canvasTexture when make a new depth texture 211 | if (!depthTexture || 212 | depthTexture.width !== canvasTexture.width || 213 | depthTexture.height !== canvasTexture.height) { 214 | if (depthTexture) { 215 | depthTexture.destroy(); 216 | } 217 | depthTexture = device.createTexture({ 218 | size: [canvasTexture.width, canvasTexture.height], 219 | format: 'depth24plus', 220 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 221 | }); 222 | } 223 | renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); 224 | 225 | const commandEncoder = device.createCommandEncoder(); 226 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 227 | passEncoder.setPipeline(pipeline); 228 | passEncoder.setBindGroup(0, bindGroup0); 229 | passEncoder.setBindGroup(2, bindGroup2); 230 | passEncoder.setVertexBuffer(0, buffers[0]); 231 | passEncoder.setIndexBuffer(indexBuffer, indexFormat); 232 | passEncoder.drawIndexed(numElements); 233 | passEncoder.end(); 234 | device.queue.submit([commandEncoder.finish()]); 235 | 236 | requestAnimationFrame(render); 237 | } 238 | requestAnimationFrame(render); 239 | 240 | const observer = new ResizeObserver(entries => { 241 | for (const entry of entries) { 242 | const canvas = entry.target; 243 | const width = entry.contentBoxSize[0].inlineSize; 244 | const height = entry.contentBoxSize[0].blockSize; 245 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 246 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 247 | } 248 | }); 249 | observer.observe(canvas); 250 | } 251 | 252 | function fail(msg) { 253 | const elem = document.querySelector('#fail'); 254 | elem.style.display = ''; 255 | elem.children[0].textContent = msg; 256 | } 257 | 258 | 259 | main(); 260 | -------------------------------------------------------------------------------- /examples/instancing.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | /* global GPUTextureUsage */ 3 | import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 4 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct VSUniforms { 27 | viewProjection: mat4x4f, 28 | }; 29 | @group(0) @binding(0) var vsUniforms: VSUniforms; 30 | 31 | struct MyVSInput { 32 | @location(0) position: vec4f, 33 | @location(1) normal: vec3f, 34 | @location(2) texcoord: vec2f, 35 | @location(3) matrix0: vec4f, 36 | @location(4) matrix1: vec4f, 37 | @location(5) matrix2: vec4f, 38 | @location(6) matrix3: vec4f, 39 | @location(7) color: vec4f, 40 | }; 41 | 42 | struct MyVSOutput { 43 | @builtin(position) position: vec4f, 44 | @location(0) normal: vec3f, 45 | @location(1) texcoord: vec2f, 46 | @location(2) color: vec4f, 47 | }; 48 | 49 | @vertex 50 | fn myVSMain(v: MyVSInput) -> MyVSOutput { 51 | let mat = mat4x4f(v.matrix0, v.matrix1, v.matrix2, v.matrix3); 52 | var vsOut: MyVSOutput; 53 | vsOut.position = vsUniforms.viewProjection * mat * v.position; 54 | vsOut.normal = (mat * vec4f(v.normal, 0.0)).xyz; 55 | vsOut.texcoord = v.texcoord; 56 | vsOut.color = v.color; 57 | return vsOut; 58 | } 59 | 60 | struct FSUniforms { 61 | lightDirection: vec3f, 62 | }; 63 | 64 | @group(0) @binding(1) var fsUniforms: FSUniforms; 65 | 66 | @fragment 67 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 68 | let diffuseColor = v.color; 69 | let a_normal = normalize(v.normal); 70 | let l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5; 71 | return vec4f(diffuseColor.rgb * l, diffuseColor.a); 72 | } 73 | `; 74 | 75 | const numInstances = 1000; 76 | const nonInstancedVerts = wgh.createBuffersAndAttributesFromArrays(device, { 77 | position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 78 | normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 79 | texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 80 | indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23], 81 | }); 82 | 83 | function r(min, max) { 84 | if (typeof max === 'undefined') { 85 | max = min; 86 | min = 0; 87 | } 88 | return Math.random() * (max - min) + min; 89 | } 90 | 91 | const matrices = new Float32Array(numInstances * 16); 92 | const colors = new Uint8Array(numInstances * 4); 93 | for (let i = 0; i < numInstances; ++i) { 94 | const color = wgh.subarray(colors, i * 4, 4); 95 | color.set([r(256), r(256), r(256), 1]); 96 | 97 | const matrix = wgh.subarray(matrices, i * 16, 16); 98 | const t = vec3.mulScalar(vec3.normalize([r(-1, 1), r(-1, 1), r(-1, 1)]), 10); 99 | mat4.translation(t, matrix); 100 | mat4.rotateX(matrix, r(Math.PI * 2), matrix); 101 | mat4.rotateY(matrix, r(Math.PI), matrix); 102 | const s = r(0.25, 1); 103 | mat4.scale(matrix, [s, s, s], matrix); 104 | } 105 | 106 | const instancedVerts = wgh.createBuffersAndAttributesFromArrays(device, { 107 | matrix: { 108 | data: matrices, //numInstances * 16, 109 | type: Float32Array, 110 | numComponents: 16, 111 | }, 112 | color: { 113 | data: colors, // numInstances * 4, 114 | type: Uint8Array, 115 | }, 116 | }, { stepMode: 'instance', interleave: false, shaderLocation: 3 }); 117 | 118 | const module = device.createShaderModule({code}); 119 | 120 | const pipeline = device.createRenderPipeline({ 121 | layout: 'auto', 122 | vertex: { 123 | module, 124 | entryPoint: 'myVSMain', 125 | buffers: [ 126 | ...nonInstancedVerts.bufferLayouts, 127 | ...instancedVerts.bufferLayouts, 128 | ], 129 | }, 130 | fragment: { 131 | module, 132 | entryPoint: 'myFSMain', 133 | targets: [ 134 | {format: presentationFormat}, 135 | ], 136 | }, 137 | primitive: { 138 | topology: 'triangle-list', 139 | cullMode: 'back', 140 | }, 141 | depthStencil: { 142 | depthWriteEnabled: true, 143 | depthCompare: 'less', 144 | format: 'depth24plus', 145 | }, 146 | }); 147 | 148 | const defs = wgh.makeShaderDataDefinitions(code); 149 | const vsUniformValues = wgh.makeStructuredView(defs.uniforms.vsUniforms); 150 | const fsUniformValues = wgh.makeStructuredView(defs.uniforms.fsUniforms); 151 | 152 | const vsUniformBuffer = device.createBuffer({ 153 | size: vsUniformValues.arrayBuffer.byteLength, 154 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 155 | }); 156 | const fsUniformBuffer = device.createBuffer({ 157 | size: fsUniformValues.arrayBuffer.byteLength, 158 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 159 | }); 160 | 161 | const bindGroup = device.createBindGroup({ 162 | layout: pipeline.getBindGroupLayout(0), 163 | entries: [ 164 | { binding: 0, resource: { buffer: vsUniformBuffer } }, 165 | { binding: 1, resource: { buffer: fsUniformBuffer } }, 166 | ], 167 | }); 168 | 169 | const renderPassDescriptor = { 170 | colorAttachments: [ 171 | { 172 | // view: undefined, // Assigned later 173 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 174 | loadOp: 'clear', 175 | storeOp: 'store', 176 | }, 177 | ], 178 | depthStencilAttachment: { 179 | // view: undefined, // Assigned later 180 | depthClearValue: 1, 181 | depthLoadOp: 'clear', 182 | depthStoreOp: 'store', 183 | }, 184 | }; 185 | 186 | let depthTexture; 187 | 188 | function render(time) { 189 | time *= 0.001; 190 | 191 | const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); 192 | const radius = 35; 193 | const t = time * 0.1; 194 | const eye = [Math.cos(t) * radius, 4, Math.sin(t) * radius]; 195 | const target = [0, 0, 0]; 196 | const up = [0, 1, 0]; 197 | 198 | const view = mat4.lookAt(eye, target, up); 199 | mat4.multiply(projection, view, vsUniformValues.views.viewProjection); 200 | 201 | fsUniformValues.set({ 202 | lightDirection: vec3.normalize([1, 8, -10]), 203 | }); 204 | 205 | device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues.arrayBuffer); 206 | device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues.arrayBuffer); 207 | 208 | const canvasTexture = context.getCurrentTexture(); 209 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 210 | 211 | // If we don't have a depth texture OR if its size is different 212 | // from the canvasTexture when make a new depth texture 213 | if (!depthTexture || 214 | depthTexture.width !== canvasTexture.width || 215 | depthTexture.height !== canvasTexture.height) { 216 | if (depthTexture) { 217 | depthTexture.destroy(); 218 | } 219 | depthTexture = device.createTexture({ 220 | size: [canvasTexture.width, canvasTexture.height], 221 | format: 'depth24plus', 222 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 223 | }); 224 | } 225 | renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); 226 | 227 | const commandEncoder = device.createCommandEncoder(); 228 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 229 | passEncoder.setPipeline(pipeline); 230 | passEncoder.setBindGroup(0, bindGroup); 231 | passEncoder.setVertexBuffer(0, nonInstancedVerts.buffers[0]); 232 | passEncoder.setVertexBuffer(1, instancedVerts.buffers[0]); 233 | passEncoder.setVertexBuffer(2, instancedVerts.buffers[1]); 234 | passEncoder.setIndexBuffer(nonInstancedVerts.indexBuffer, nonInstancedVerts.indexFormat); 235 | passEncoder.drawIndexed(nonInstancedVerts.numElements, instancedVerts.numElements); 236 | passEncoder.end(); 237 | device.queue.submit([commandEncoder.finish()]); 238 | 239 | requestAnimationFrame(render); 240 | } 241 | requestAnimationFrame(render); 242 | 243 | const observer = new ResizeObserver(entries => { 244 | for (const entry of entries) { 245 | const canvas = entry.target; 246 | const width = entry.contentBoxSize[0].inlineSize; 247 | const height = entry.contentBoxSize[0].blockSize; 248 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 249 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 250 | } 251 | }); 252 | observer.observe(canvas); 253 | } 254 | 255 | function fail(msg) { 256 | const elem = document.querySelector('#fail'); 257 | elem.style.display = ''; 258 | elem.children[0].textContent = msg; 259 | } 260 | 261 | main(); 262 | -------------------------------------------------------------------------------- /examples/primitives.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | /* global GPUTextureUsage */ 3 | import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 4 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct Uniforms { 27 | world: mat4x4f, 28 | color: vec4f, 29 | }; 30 | 31 | struct SharedUniforms { 32 | viewProjection: mat4x4f, 33 | lightDirection: vec3f, 34 | }; 35 | 36 | @group(0) @binding(0) var uni: Uniforms; 37 | @group(0) @binding(1) var sharedUni: SharedUniforms; 38 | 39 | struct MyVSInput { 40 | @location(0) position: vec4f, 41 | @location(1) normal: vec3f, 42 | @location(2) texcoord: vec2f, 43 | }; 44 | 45 | struct MyVSOutput { 46 | @builtin(position) position: vec4f, 47 | @location(0) normal: vec3f, 48 | @location(1) texcoord: vec2f, 49 | }; 50 | 51 | @vertex 52 | fn myVSMain(v: MyVSInput) -> MyVSOutput { 53 | var vsOut: MyVSOutput; 54 | vsOut.position = sharedUni.viewProjection * uni.world * v.position; 55 | vsOut.normal = (uni.world * vec4f(v.normal, 0.0)).xyz; 56 | vsOut.texcoord = v.texcoord; 57 | return vsOut; 58 | } 59 | 60 | @fragment 61 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 62 | let diffuseColor = uni.color; 63 | let a_normal = normalize(v.normal); 64 | let l = dot(a_normal, sharedUni.lightDirection) * 0.5 + 0.5; 65 | return vec4f(diffuseColor.rgb * l, diffuseColor.a); 66 | } 67 | `; 68 | 69 | function facet(arrays) { 70 | const newArrays = wgh.primitives.deindex(arrays); 71 | newArrays.normal = wgh.primitives.generateTriangleNormals(wgh.makeTypedArrayFromArrayUnion(newArrays.position, 'position')); 72 | return newArrays; 73 | } 74 | 75 | const numInstances = 1000; 76 | const geometries = [ 77 | wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createSphereVertices()), 78 | wgh.createBuffersAndAttributesFromArrays(device, facet(wgh.primitives.createSphereVertices({subdivisionsAxis: 6, subdivisionsHeight: 5}))), 79 | wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createTorusVertices()), 80 | wgh.createBuffersAndAttributesFromArrays(device, facet(wgh.primitives.createTorusVertices({thickness: 0.5, radialSubdivisions: 8, bodySubdivisions: 8}))), 81 | wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCubeVertices()), 82 | wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCylinderVertices()), 83 | wgh.createBuffersAndAttributesFromArrays(device, facet(wgh.primitives.createCylinderVertices({radialSubdivisions: 7}))), 84 | /////wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createPlaneVertices()), 85 | /////wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createDiscVertices()), 86 | wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createTruncatedConeVertices()), 87 | ]; 88 | 89 | function r(min, max) { 90 | if (typeof max === 'undefined') { 91 | max = min; 92 | min = 0; 93 | } 94 | return Math.random() * (max - min) + min; 95 | } 96 | 97 | const randElem = arr => arr[r(arr.length) | 0]; 98 | 99 | const module = device.createShaderModule({code}); 100 | 101 | const pipeline = device.createRenderPipeline({ 102 | layout: 'auto', 103 | vertex: { 104 | module, 105 | entryPoint: 'myVSMain', 106 | buffers: [ 107 | ...geometries[0].bufferLayouts, 108 | ], 109 | }, 110 | fragment: { 111 | module, 112 | entryPoint: 'myFSMain', 113 | targets: [ 114 | {format: presentationFormat}, 115 | ], 116 | }, 117 | primitive: { 118 | topology: 'triangle-list', 119 | cullMode: 'back', 120 | }, 121 | depthStencil: { 122 | depthWriteEnabled: true, 123 | depthCompare: 'less', 124 | format: 'depth24plus', 125 | }, 126 | }); 127 | 128 | const defs = wgh.makeShaderDataDefinitions(code); 129 | const sharedUniformValues = wgh.makeStructuredView(defs.uniforms.sharedUni); 130 | 131 | const sharedUniformBuffer = device.createBuffer({ 132 | size: sharedUniformValues.arrayBuffer.byteLength, 133 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 134 | }); 135 | 136 | const objectInfos = []; 137 | for (let i = 0; i < numInstances; ++i) { 138 | const uniformView = wgh.makeStructuredView(defs.uniforms.uni); 139 | const uniformBuffer = device.createBuffer({ 140 | size: uniformView.arrayBuffer.byteLength, 141 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 142 | }); 143 | uniformView.views.color.set([r(1), r(1), r(1), 1]); 144 | 145 | device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); 146 | 147 | const bindGroup = device.createBindGroup({ 148 | layout: pipeline.getBindGroupLayout(0), 149 | entries: [ 150 | { binding: 0, resource: { buffer: uniformBuffer } }, 151 | { binding: 1, resource: { buffer: sharedUniformBuffer } }, 152 | ], 153 | }); 154 | 155 | objectInfos.push({ 156 | uniformView, 157 | uniformBuffer, 158 | bindGroup, 159 | geometry: randElem(geometries), 160 | }); 161 | } 162 | 163 | const renderPassDescriptor = { 164 | colorAttachments: [ 165 | { 166 | // view: undefined, // Assigned later 167 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 168 | loadOp: 'clear', 169 | storeOp: 'store', 170 | }, 171 | ], 172 | depthStencilAttachment: { 173 | // view: undefined, // Assigned later 174 | depthClearValue: 1, 175 | depthLoadOp: 'clear', 176 | depthStoreOp: 'store', 177 | }, 178 | }; 179 | 180 | let depthTexture; 181 | 182 | function render(time) { 183 | time *= 0.001; 184 | 185 | const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); 186 | const radius = 35; 187 | const t = time * 0.1; 188 | const eye = [Math.cos(t) * radius, 4, Math.sin(t) * radius]; 189 | const target = [0, 0, 0]; 190 | const up = [0, 1, 0]; 191 | 192 | const view = mat4.lookAt(eye, target, up); 193 | mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); 194 | 195 | sharedUniformValues.set({ 196 | lightDirection: vec3.normalize([1, 8, 10]), 197 | }); 198 | 199 | device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); 200 | 201 | const canvasTexture = context.getCurrentTexture(); 202 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 203 | 204 | // If we don't have a depth texture OR if its size is different 205 | // from the canvasTexture when make a new depth texture 206 | if (!depthTexture || 207 | depthTexture.width !== canvasTexture.width || 208 | depthTexture.height !== canvasTexture.height) { 209 | if (depthTexture) { 210 | depthTexture.destroy(); 211 | } 212 | depthTexture = device.createTexture({ 213 | size: [canvasTexture.width, canvasTexture.height], 214 | format: 'depth24plus', 215 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 216 | }); 217 | } 218 | renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); 219 | 220 | const commandEncoder = device.createCommandEncoder(); 221 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 222 | passEncoder.setPipeline(pipeline); 223 | objectInfos.forEach(({ 224 | bindGroup, 225 | geometry, 226 | uniformBuffer, 227 | uniformView, 228 | }, i) => { 229 | const world = uniformView.views.world; 230 | mat4.identity(world); 231 | mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * 0.1) * 10], world); 232 | mat4.rotateX(world, i * 4.567, world); 233 | mat4.rotateY(world, i * 2.967, world); 234 | mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * 10], world); 235 | mat4.rotateX(world, time * 0.53 + i, world); 236 | 237 | device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); 238 | 239 | passEncoder.setBindGroup(0, bindGroup); 240 | passEncoder.setVertexBuffer(0, geometry.buffers[0]); 241 | if (geometry.indexBuffer) { 242 | passEncoder.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat); 243 | passEncoder.drawIndexed(geometry.numElements); 244 | } else { 245 | passEncoder.draw(geometry.numElements); 246 | } 247 | }); 248 | passEncoder.end(); 249 | device.queue.submit([commandEncoder.finish()]); 250 | 251 | requestAnimationFrame(render); 252 | } 253 | requestAnimationFrame(render); 254 | 255 | const observer = new ResizeObserver(entries => { 256 | for (const entry of entries) { 257 | const canvas = entry.target; 258 | const width = entry.contentBoxSize[0].inlineSize; 259 | const height = entry.contentBoxSize[0].blockSize; 260 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 261 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 262 | } 263 | }); 264 | observer.observe(canvas); 265 | } 266 | 267 | function fail(msg) { 268 | const elem = document.querySelector('#fail'); 269 | elem.style.display = ''; 270 | elem.children[0].textContent = msg; 271 | } 272 | 273 | main(); 274 | -------------------------------------------------------------------------------- /examples/instancing-size-only.js: -------------------------------------------------------------------------------- 1 | /* global GPUBufferUsage */ 2 | /* global GPUTextureUsage */ 3 | import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; 4 | import * as wgh from '../dist/2.x/webgpu-utils.module.js'; 5 | 6 | async function main() { 7 | const adapter = await navigator.gpu?.requestAdapter({ 8 | featureLevel: 'compatibility', 9 | }); 10 | const device = await adapter?.requestDevice(); 11 | if (!device) { 12 | fail('need a browser that supports WebGPU'); 13 | return; 14 | } 15 | 16 | const canvas = document.querySelector('canvas'); 17 | const context = canvas.getContext('webgpu'); 18 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); 19 | context.configure({ 20 | device, 21 | format: presentationFormat, 22 | alphaMode: 'premultiplied', 23 | }); 24 | 25 | const code = ` 26 | struct VSUniforms { 27 | viewProjection: mat4x4f, 28 | }; 29 | @group(0) @binding(0) var vsUniforms: VSUniforms; 30 | 31 | struct MyVSInput { 32 | @location(0) position: vec4f, 33 | @location(1) normal: vec3f, 34 | @location(2) texcoord: vec2f, 35 | @location(3) matrix0: vec4f, 36 | @location(4) matrix1: vec4f, 37 | @location(5) matrix2: vec4f, 38 | @location(6) matrix3: vec4f, 39 | @location(7) color: vec4f, 40 | }; 41 | 42 | struct MyVSOutput { 43 | @builtin(position) position: vec4f, 44 | @location(0) normal: vec3f, 45 | @location(1) texcoord: vec2f, 46 | @location(2) color: vec4f, 47 | }; 48 | 49 | @vertex 50 | fn myVSMain(v: MyVSInput) -> MyVSOutput { 51 | let mat = mat4x4f(v.matrix0, v.matrix1, v.matrix2, v.matrix3); 52 | var vsOut: MyVSOutput; 53 | vsOut.position = vsUniforms.viewProjection * mat * v.position; 54 | vsOut.normal = (mat * vec4f(v.normal, 0.0)).xyz; 55 | vsOut.texcoord = v.texcoord; 56 | vsOut.color = v.color; 57 | return vsOut; 58 | } 59 | 60 | struct FSUniforms { 61 | lightDirection: vec3f, 62 | }; 63 | 64 | @group(0) @binding(1) var fsUniforms: FSUniforms; 65 | 66 | @fragment 67 | fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { 68 | let diffuseColor = v.color; 69 | let a_normal = normalize(v.normal); 70 | let l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5; 71 | return vec4f(diffuseColor.rgb * l, diffuseColor.a); 72 | } 73 | `; 74 | 75 | const numInstances = 1000; 76 | const nonInstancedVerts = wgh.createBuffersAndAttributesFromArrays(device, { 77 | position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1], 78 | normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1], 79 | texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 80 | indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23], 81 | }); 82 | 83 | function r(min, max) { 84 | if (typeof max === 'undefined') { 85 | max = min; 86 | min = 0; 87 | } 88 | return Math.random() * (max - min) + min; 89 | } 90 | 91 | const instancedVerts = wgh.createBuffersAndAttributesFromArrays(device, { 92 | matrix: { 93 | data: numInstances * 16, 94 | type: Float32Array, 95 | numComponents: 16, 96 | }, 97 | color: { 98 | data: numInstances * 4, 99 | type: Uint8Array, 100 | }, 101 | }, { stepMode: 'instance', interleave: false, shaderLocation: 3, usage: GPUBufferUsage.COPY_DST }); 102 | 103 | const matrices = new Float32Array(numInstances * 16); 104 | const colors = new Uint8Array(numInstances * 4); 105 | for (let i = 0; i < numInstances; ++i) { 106 | const color = wgh.subarray(colors, i * 4, 4); 107 | color.set([r(256), r(256), r(256), 1]); 108 | 109 | const matrix = wgh.subarray(matrices, i * 16, 16); 110 | const t = vec3.mulScalar(vec3.normalize([r(-1, 1), r(-1, 1), r(-1, 1)]), 10); 111 | mat4.translation(t, matrix); 112 | mat4.rotateX(matrix, r(Math.PI * 2), matrix); 113 | mat4.rotateY(matrix, r(Math.PI), matrix); 114 | const s = r(0.25, 1); 115 | mat4.scale(matrix, [s, s, s], matrix); 116 | } 117 | device.queue.writeBuffer(instancedVerts.buffers[0], 0, matrices); 118 | device.queue.writeBuffer(instancedVerts.buffers[1], 0, colors); 119 | 120 | const module = device.createShaderModule({code}); 121 | 122 | const pipeline = device.createRenderPipeline({ 123 | layout: 'auto', 124 | vertex: { 125 | module, 126 | entryPoint: 'myVSMain', 127 | buffers: [ 128 | ...nonInstancedVerts.bufferLayouts, 129 | ...instancedVerts.bufferLayouts, 130 | ], 131 | }, 132 | fragment: { 133 | module, 134 | entryPoint: 'myFSMain', 135 | targets: [ 136 | {format: presentationFormat}, 137 | ], 138 | }, 139 | primitive: { 140 | topology: 'triangle-list', 141 | cullMode: 'back', 142 | }, 143 | depthStencil: { 144 | depthWriteEnabled: true, 145 | depthCompare: 'less', 146 | format: 'depth24plus', 147 | }, 148 | }); 149 | 150 | const defs = wgh.makeShaderDataDefinitions(code); 151 | const vsUniformValues = wgh.makeStructuredView(defs.uniforms.vsUniforms); 152 | const fsUniformValues = wgh.makeStructuredView(defs.uniforms.fsUniforms); 153 | 154 | const vsUniformBuffer = device.createBuffer({ 155 | size: vsUniformValues.arrayBuffer.byteLength, 156 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 157 | }); 158 | const fsUniformBuffer = device.createBuffer({ 159 | size: fsUniformValues.arrayBuffer.byteLength, 160 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 161 | }); 162 | 163 | const bindGroup = device.createBindGroup({ 164 | layout: pipeline.getBindGroupLayout(0), 165 | entries: [ 166 | { binding: 0, resource: { buffer: vsUniformBuffer } }, 167 | { binding: 1, resource: { buffer: fsUniformBuffer } }, 168 | ], 169 | }); 170 | 171 | const renderPassDescriptor = { 172 | colorAttachments: [ 173 | { 174 | // view: undefined, // Assigned later 175 | clearValue: [ 0.2, 0.2, 0.2, 1.0 ], 176 | loadOp: 'clear', 177 | storeOp: 'store', 178 | }, 179 | ], 180 | depthStencilAttachment: { 181 | // view: undefined, // Assigned later 182 | depthClearValue: 1, 183 | depthLoadOp: 'clear', 184 | depthStoreOp: 'store', 185 | }, 186 | }; 187 | 188 | let depthTexture; 189 | 190 | function render(time) { 191 | time *= 0.001; 192 | 193 | const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); 194 | const radius = 35; 195 | const t = time * 0.1; 196 | const eye = [Math.cos(t) * radius, 4, Math.sin(t) * radius]; 197 | const target = [0, 0, 0]; 198 | const up = [0, 1, 0]; 199 | 200 | const view = mat4.lookAt(eye, target, up); 201 | mat4.multiply(projection, view, vsUniformValues.views.viewProjection); 202 | 203 | fsUniformValues.set({ 204 | lightDirection: vec3.normalize([1, 8, -10]), 205 | }); 206 | 207 | device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues.arrayBuffer); 208 | device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues.arrayBuffer); 209 | 210 | const canvasTexture = context.getCurrentTexture(); 211 | renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); 212 | 213 | // If we don't have a depth texture OR if its size is different 214 | // from the canvasTexture when make a new depth texture 215 | if (!depthTexture || 216 | depthTexture.width !== canvasTexture.width || 217 | depthTexture.height !== canvasTexture.height) { 218 | if (depthTexture) { 219 | depthTexture.destroy(); 220 | } 221 | depthTexture = device.createTexture({ 222 | size: [canvasTexture.width, canvasTexture.height], 223 | format: 'depth24plus', 224 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 225 | }); 226 | } 227 | renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); 228 | 229 | const commandEncoder = device.createCommandEncoder(); 230 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 231 | passEncoder.setPipeline(pipeline); 232 | passEncoder.setBindGroup(0, bindGroup); 233 | passEncoder.setVertexBuffer(0, nonInstancedVerts.buffers[0]); 234 | passEncoder.setVertexBuffer(1, instancedVerts.buffers[0]); 235 | passEncoder.setVertexBuffer(2, instancedVerts.buffers[1]); 236 | passEncoder.setIndexBuffer(nonInstancedVerts.indexBuffer, nonInstancedVerts.indexFormat); 237 | passEncoder.drawIndexed(nonInstancedVerts.numElements, instancedVerts.numElements); 238 | passEncoder.end(); 239 | device.queue.submit([commandEncoder.finish()]); 240 | 241 | requestAnimationFrame(render); 242 | } 243 | requestAnimationFrame(render); 244 | 245 | const observer = new ResizeObserver(entries => { 246 | for (const entry of entries) { 247 | const canvas = entry.target; 248 | const width = entry.contentBoxSize[0].inlineSize; 249 | const height = entry.contentBoxSize[0].blockSize; 250 | canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); 251 | canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); 252 | } 253 | }); 254 | observer.observe(canvas); 255 | } 256 | 257 | function fail(msg) { 258 | const elem = document.querySelector('#fail'); 259 | elem.style.display = ''; 260 | elem.children[0].textContent = msg; 261 | } 262 | 263 | 264 | main(); 265 | --------------------------------------------------------------------------------