This commit is contained in:
Andras Schmelczer 2023-04-15 14:13:54 +01:00
commit 9349a57781
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
14 changed files with 3973 additions and 0 deletions

45
.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# Dependency directory
node_modules
modules/
ts-node--*/
rss.xml
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.ssh
*.ppk
v8-compile-cache-0/
Thumbs.db
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
bin
ts-node
# Personal Scripts
*.bat
*.ssh
*.sh
!system.min.js
# Editors
.vscode
.markdownlint.json
# Build Files
temp
*.js
*.map
!webpack.*
dist

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "none",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

55
README.md Normal file
View file

@ -0,0 +1,55 @@
# 🔺 WebGPU Seed
[![License][license-img]][license-url]
A WebGPU repo you can use to get started with your own renderer.
- [🔳 Codepen Example](https://codepen.io/alaingalvan/pen/GRgvLGw)
- [💬 Blog Post](https://alain.xyz/blog/raw-webgpu)
## Setup
First install:
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org/en/)
- A Text Editor such as [Visual Studio Code](https://code.visualstudio.com/).
Then type the following in any terminal your such as [VS Code's Integrated Terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).
```bash
# 🐑 Clone the repo
git clone https://github.com/alaingalvan/webgpu-seed
# 💿 go inside the folder
cd webgpu-seed
# 🔨 Start installing dependencies, building, and running at localhost:8080
npm start
```
> Refer to [this blog post on designing web libraries and apps](https://alain.xyz/blog/designing-a-web-app) for more details on Node.js, packages, etc.
## Project Layout
As your project becomes more complex, you'll want to separate files and organize your application to something more akin to a game or renderer, check out this post on [game engine architecture](https://alain.xyz/blog/game-engine-architecture) and this one on [real time renderer architecture](https://alain.xyz/blog/realtime-renderer-architectures) for more details.
```bash
├─ 📂 node_modules/ # 👶 Dependencies
│ ├─ 📁 gl-matrix # Linear Algebra
│ └─ 📁 ... # 🕚 Other Dependencies (TypeScript, Webpack, etc.)
├─ 📂 src/ # 🌟 Source Files
│ ├─ 📄 index.html # 📇 Main HTML file
│ └─ 📄 renderer.ts # 🔺 Triangle Renderer
├─ 📄 .gitignore # 👁️ Ignore certain files in git repo
├─ 📄 package.json # 📦 Node Package File
├─ 📄 license.md # ⚖️ Your License (Unlicense)
└─ 📃readme.md # 📖 Read Me!
```
[license-img]: https://img.shields.io/:license-unlicense-blue.svg?style=flat-square
[license-url]: https://unlicense.org/

1
definitions.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.wgsl';

28
index.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>WebGPU Hello Triangle</title>
<style>
html,
body {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
background: #000;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<canvas id="gfx"></canvas>
<script type="text/javascript" src="dist/main.js"></script>
</body>
</html>

9
license.md Normal file
View file

@ -0,0 +1,9 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

3357
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

43
package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "webgpu-seed",
"version": "0.1.0",
"description": "🔺 A simple hello triangle example introducing WebGPU.",
"main": "dist/main.js",
"scripts": {
"start": "npm i && npm run build && npm run dev",
"dev": "http-server",
"build": "cross-env NODE_ENV=production ts-node webpack.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/alaingalvan/webgpu-seed.git"
},
"keywords": [
"webgpu",
"webgl",
"example",
"seed",
"types",
"typescript"
],
"author": "Alain Galvan",
"license": "Unlicense",
"bugs": {
"url": "https://github.com/alaingalvan/webgpu-seed/issues"
},
"homepage": "https://github.com/alaingalvan/webgpu-seed#readme",
"devDependencies": {
"@types/node": "^18.11.x",
"@webgpu/types": "^0.1.26",
"clean-webpack-plugin": "^4.0.x",
"cross-env": "^7.0.x",
"http-server": "^14.1.x",
"ts-loader": "^9.4.x",
"ts-node": "^10.9.x",
"typescript": "^4.9.x",
"webpack": "^5.75.x"
},
"dependencies": {
"gl-matrix": "^3.4.3"
}
}

6
src/main.ts Normal file
View file

@ -0,0 +1,6 @@
import Renderer from './renderer';
const canvas = document.getElementById('gfx') as HTMLCanvasElement;
canvas.width = canvas.height = 640;
const renderer = new Renderer(canvas);
renderer.start();

257
src/renderer.ts Normal file
View file

@ -0,0 +1,257 @@
import vertShaderCode from './shaders/triangle.vert.wgsl';
import fragShaderCode from './shaders/triangle.frag.wgsl';
const positions = new Float32Array([
1.0, -1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0, 0.0
]);
const colors = new Float32Array([
1.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
1.0
]);
const indices = new Uint16Array([0, 1, 2]);
export default class Renderer {
canvas: HTMLCanvasElement;
adapter: GPUAdapter;
device: GPUDevice;
queue: GPUQueue;
context: GPUCanvasContext;
colorTexture: GPUTexture;
colorTextureView: GPUTextureView;
depthTexture: GPUTexture;
depthTextureView: GPUTextureView;
positionBuffer: GPUBuffer;
colorBuffer: GPUBuffer;
indexBuffer: GPUBuffer;
vertModule: GPUShaderModule;
fragModule: GPUShaderModule;
pipeline: GPURenderPipeline;
commandEncoder: GPUCommandEncoder;
passEncoder: GPURenderPassEncoder;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
}
async start() {
if (await this.initializeAPI()) {
this.resizeBackings();
await this.initializeResources();
this.render();
}
}
async initializeAPI(): Promise<boolean> {
try {
const entry: GPU = navigator.gpu;
if (!entry) {
return false;
}
this.adapter = await entry.requestAdapter();
this.device = await this.adapter.requestDevice();
this.queue = this.device.queue;
} catch (e) {
console.error(e);
return false;
}
return true;
}
async initializeResources() {
const createBuffer = (
arr: Float32Array | Uint16Array,
usage: number
) => {
// 📏 Align to 4 bytes (thanks @chrimsonite)
let desc = {
size: (arr.byteLength + 3) & ~3,
usage,
mappedAtCreation: true
};
let buffer = this.device.createBuffer(desc);
const writeArray =
arr instanceof Uint16Array
? new Uint16Array(buffer.getMappedRange())
: new Float32Array(buffer.getMappedRange());
writeArray.set(arr);
buffer.unmap();
return buffer;
};
this.positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
this.colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
this.indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
const vsmDesc = {
code: vertShaderCode
};
this.vertModule = this.device.createShaderModule(vsmDesc);
const fsmDesc = {
code: fragShaderCode
};
this.fragModule = this.device.createShaderModule(fsmDesc);
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0, // [[location(0)]]
offset: 0,
format: 'float32x3'
};
const colorAttribDesc: GPUVertexAttribute = {
shaderLocation: 1, // [[location(1)]]
offset: 0,
format: 'float32x3'
};
const positionBufferDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const colorBufferDesc: GPUVertexBufferLayout = {
attributes: [colorAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const depthStencil: GPUDepthStencilState = {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8'
};
const pipelineLayoutDesc = { bindGroupLayouts: [] };
const layout = this.device.createPipelineLayout(pipelineLayoutDesc);
const vertex: GPUVertexState = {
module: this.vertModule,
entryPoint: 'main',
buffers: [positionBufferDesc, colorBufferDesc]
};
const colorState: GPUColorTargetState = {
format: 'bgra8unorm'
};
const fragment: GPUFragmentState = {
module: this.fragModule,
entryPoint: 'main',
targets: [colorState]
};
const primitive: GPUPrimitiveState = {
frontFace: 'cw',
cullMode: 'none',
topology: 'triangle-list'
};
const pipelineDesc: GPURenderPipelineDescriptor = {
layout,
vertex,
fragment,
primitive,
depthStencil
};
this.pipeline = this.device.createRenderPipeline(pipelineDesc);
}
resizeBackings() {
if (!this.context) {
this.context = this.canvas.getContext('webgpu');
const canvasConfig: GPUCanvasConfiguration = {
device: this.device,
format: 'bgra8unorm',
usage:
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.COPY_SRC,
alphaMode: 'opaque'
};
this.context.configure(canvasConfig);
}
const depthTextureDesc: GPUTextureDescriptor = {
size: [this.canvas.width, this.canvas.height, 1],
dimension: '2d',
format: 'depth24plus-stencil8',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
};
this.depthTexture = this.device.createTexture(depthTextureDesc);
this.depthTextureView = this.depthTexture.createView();
}
encodeCommands() {
let colorAttachment: GPURenderPassColorAttachment = {
view: this.colorTextureView,
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store'
};
const depthAttachment: GPURenderPassDepthStencilAttachment = {
view: this.depthTextureView,
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store'
};
const renderPassDesc: GPURenderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthAttachment
};
this.commandEncoder = this.device.createCommandEncoder();
// 🖌️ Encode drawing commands
this.passEncoder = this.commandEncoder.beginRenderPass(renderPassDesc);
this.passEncoder.setPipeline(this.pipeline);
this.passEncoder.setViewport(
0,
0,
this.canvas.width,
this.canvas.height,
0,
1
);
this.passEncoder.setScissorRect(
0,
0,
this.canvas.width,
this.canvas.height
);
this.passEncoder.setVertexBuffer(0, this.positionBuffer);
this.passEncoder.setVertexBuffer(1, this.colorBuffer);
this.passEncoder.setIndexBuffer(this.indexBuffer, 'uint16');
this.passEncoder.drawIndexed(3, 1);
this.passEncoder.end();
this.queue.submit([this.commandEncoder.finish()]);
}
render = () => {
this.colorTexture = this.context.getCurrentTexture();
this.colorTextureView = this.colorTexture.createView();
this.encodeCommands();
requestAnimationFrame(this.render);
};
}

View file

@ -0,0 +1,4 @@
@fragment
fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(inColor, 1.0);
}

View file

@ -0,0 +1,13 @@
struct VSOut {
@builtin(position) Position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn main(@location(0) inPos: vec3<f32>,
@location(1) inColor: vec3<f32>) -> VSOut {
var vsOut: VSOut;
vsOut.Position = vec4<f32>(inPos, 1.0);
vsOut.color = inColor;
return vsOut;
}

37
tsconfig.json Normal file
View file

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES6",
"lib": [
"es2017",
"es2017.object",
"es2017.sharedmemory",
"es2016",
"es2016.array.include",
"es2015",
"es2015.core",
"es2015.promise",
"es2015.collection",
"es5",
"dom"
],
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noEmitHelpers": false,
"sourceMap": false,
"strictNullChecks": false,
"jsx": "react",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": [
"definitions.d.ts",
"src/**/*",
"node_modules/@webgpu/types/**/*"
],
"compileOnSave": false,
"buildOnSave": false
}

112
webpack.ts Normal file
View file

@ -0,0 +1,112 @@
import webpack from 'webpack';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import path from 'path';
import { argv } from 'process';
let env = process.env['NODE_ENV'];
let isProduction =
(env && env.match(/production/)) ||
argv.reduce((prev, cur) => prev || cur === '--production', false);
let config: webpack.Configuration = {
context: path.join(__dirname, 'src'),
entry: {
app: './main.ts'
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.ts', '.tsx', 'js'],
modules: [path.resolve(__dirname, 'src'), 'node_modules']
},
module: {
rules: [
{
test: /\.ts/,
exclude: /node_modules/,
loader: 'ts-loader',
options: {
transpileOnly: true,
compilerOptions: {
isolatedModules: true
}
}
},
{
test: /\.wgsl/,
type: 'asset/source'
}
]
},
node: false,
plugins: [
new CleanWebpackPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(
isProduction ? 'production' : 'development'
)
}
})
],
optimization: {
minimize: isProduction ? true : false
}
};
/**
* Start Build
*/
const compiler: webpack.Compiler = webpack(config);
if (!argv.reduce((prev, cur) => prev || cur === '--watch', false)) {
compiler.run((err, stats) => {
if (err) return console.error(err);
if (stats.hasErrors()) {
let statsJson = stats.toJson();
console.log(
'❌' + ' · Error · ' + 'webgpu-seed failed to compile:'
);
for (let error of statsJson.errors) {
console.log(error.message);
}
return;
}
console.log(
'✔️️' +
' · Success · ' +
'webgpu-seed' +
(isProduction ? ' (production) ' : ' (development) ') +
'built in ' +
(+stats.endTime - +stats.startTime + ' ms.')
);
});
} else {
compiler.watch({}, (err, stats) => {
if (err) return console.error(err);
if (stats.hasErrors()) {
let statsJson = stats.toJson();
console.log(
'❌' + ' · Error · ' + 'webgpu-seed failed to compile:'
);
for (let error of statsJson.errors) {
console.log(error.message);
}
console.log('\n👀 · Watching for changes... · \n');
return;
}
console.log(
'✔️️' +
' · Success · ' +
'webgpu-seed' +
(isProduction ? ' (production) ' : ' (development) ') +
'built in ' +
(+stats.endTime - +stats.startTime + ' ms.') +
'\n👀 · Watching for changes... · \n'
);
});
}