Compare commits

3 Commits
gltf ... main

Author SHA1 Message Date
501949828d Modernize project 2025-11-06 16:32:41 +01:00
Szymon Nowakowski
d28d7896de Update deps, texture demo, fix textures and normals in shader 2024-03-17 20:22:03 +01:00
78683f6115 Include readme video 2023-11-28 22:27:41 +01:00
19 changed files with 144 additions and 72 deletions

View File

@@ -6,8 +6,11 @@ end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
tab_width = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[bun.lock]
indent_size = 2
indent_style = space

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.webm filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

View File

@@ -1 +1 @@
Szymon Nowakowski <smnbdg13@gmail.com> (https://renati.me)
Szymon Nowakowski <renati@renati.me> (https://renati.me)

View File

@@ -1,11 +1,12 @@
# oktaeder
3D rendering library for WebGPU
[oktaeder.webm](https://github.com/iszn11/oktaeder/assets/7891270/5dbcb03a-608f-41b8-860e-8e9c8e09e242)
<video src="https://gitea.renati.me/renati/oktaeder/media/branch/main/oktaeder.webm" autoplay controls loop>
</video>
This project ships with [bun.lockb](https://bun.sh/docs/install/lockfile)
This project ships with [bun.lock](https://bun.sh/docs/install/lockfile)
lockfile for the [Bun](https://bun.sh/) JavaScript runtime. You should be able
to install the dependencies with any JavaScript package manager, though.
To run the example, run `start:example` script with your JavaScript package
manager and visit [localhost:8000](http://localhost:8000).
To run the example, run `bun run start:example`. The example relies on the Bun
JavaScript runtime to bundle and serve the example.

22
bun.lock Normal file
View File

@@ -0,0 +1,22 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "oktaeder",
"dependencies": {
"tslib": "^2.6.2",
},
"devDependencies": {
"@webgpu/types": "^0.1.66",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8">
<title>oktaeder example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bundle.css">
<script type="module" src="bundle.js"></script>
<link rel="stylesheet" href="style.css">
<script type="module" src="script.ts"></script>
</head>
<body></body>
</html>

View File

@@ -1,8 +1,10 @@
import { Color, DirectionalLight, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index";
/// <reference types="../node_modules/@webgpu/types" />
/// <reference path="types.d.ts" />
import { Color, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index";
import { Renderer, degToRad } from "../src/oktaeder";
import "./style.css";
new EventSource("/esbuild").addEventListener("change", () => location.reload());
import uvmapUrl from "./uvmap.png";
const canvas = document.createElement("canvas");
window.addEventListener("resize", onResize);
@@ -16,38 +18,74 @@ const camera = new PerspectiveCamera({
farPlane: Infinity,
});
const vertexBuffer = renderer.createVertexBuffer({ vertexCount: 6 });
const vertexBuffer = renderer.createVertexBuffer({ vertexCount: 12, texCoord: true });
vertexBuffer.writeTypedArray(0, {
position: new Float32Array([
0, 0, 1,
1, 0, 0,
0, 1, 0,
-1, 0, 0,
0, 0, -1,
0, 0, -1,
0, 0, -1,
1, 0, 0,
0, -1, 0,
0, 1, 0,
-1, 0, 0,
0, 0, -1,
0, 0, 1,
]),
texCoord: new Float32Array([
0.5, 0.7113,
0.333333, 1,
0.166666, 0.7113,
0.333333, 0.4226,
0, 0.4226,
0, 1,
1, 1,
0.666666, 1,
0.833333, 0.7113,
0.666666, 0.4226,
1, 0.4226,
0.5, 0.7113,
]),
});
const indexBuffer = renderer.createIndexBuffer({ indexCount: 24, indexFormat: "uint16" });
indexBuffer.writeArray(0, [
0, 4, 3,
4, 1, 3,
1, 5, 3,
5, 0, 3,
4, 0, 2,
1, 4, 2,
0, 2, 1,
3, 4, 2,
5, 1, 2,
0, 5, 2,
2, 0, 3,
6, 8, 7,
9, 8, 10,
7, 8, 11,
11, 8, 9,
]);
const submesh: Submesh = { start: 0, length: 24 };
const mesh = new Mesh({ vertexBuffer, indexBuffer, submeshes: [submesh] });
const imageBitmap = await loadImageBitmap(uvmapUrl);
const texture = renderer.createTexture({
format: "srgb",
width: imageBitmap.width,
height: imageBitmap.height,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
});
renderer._device.queue.copyExternalImageToTexture(
{ source: imageBitmap, flipY: false },
{ texture: texture._texture },
{ width: imageBitmap.width, height: imageBitmap.height },
);
const material = renderer.createMaterial({
baseColor: Color.white(),
baseColorPartialCoverageTexture: texture,
roughness: 0.5,
metallic: 1,
metallic: 0,
});
const node = new Node({ mesh, materials: [material] });
@@ -55,25 +93,13 @@ const node = new Node({ mesh, materials: [material] });
const scene = new Scene({
nodes: [
node,
new Node({
translation: new Vector3(-1, 1, 0),
light: new PointLight({ color: new Color(1, 0, 0) }),
}),
new Node({
translation: new Vector3(0, 1, -1),
light: new PointLight({ color: new Color(0, 1, 0) }),
light: new PointLight({ color: new Color(1, 1, 1) }),
}),
new Node({
translation: new Vector3(1, 1, 0),
light: new PointLight({ color: new Color(0, 0, 1) }),
}),
new Node({
translation: new Vector3(0, 1, 1),
light: new PointLight({ color: new Color(1, 1, 0) }),
}),
new Node({
rotation: Quaternion.fromRotationYZ(degToRad(-90)),
light: new DirectionalLight({ color: new Color(0.5, 0.5, 0.5) }),
translation: new Vector3(0, -1, -1),
light: new PointLight({ color: new Color(1, 1, 1) }),
}),
new Node({
translation: new Vector3(0, 0.8, -3),
@@ -91,6 +117,14 @@ function onResize(this: Window) {
const _quaternion = Quaternion.identity();
async function loadImageBitmap(url: string) {
const res = await fetch(url);
const blob = await res.blob();
const imageBitmap = await createImageBitmap(blob, { colorSpaceConversion: "none" });
return imageBitmap;
}
function draw(timeMs: number) {
const time = 0.001 * timeMs;
node.setRotation(_quaternion.setRotationZX(-0.5 * time));

4
example/types.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.png" {
const url: string;
export default url;
}

BIN
example/uvmap.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
oktaeder.webm (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -7,27 +7,23 @@
"gltf",
"wegbpu"
],
"homepage": "https://github.com/iszn11/oktaeder",
"bugs": {
"url": "https://github.com/iszn11/oktaeder/issues"
},
"homepage": "https://gitea.renati.me/renati/oktaeder",
"license": "MPL-2.0",
"browser": "./dist/oktaeder.js",
"repository": {
"type": "git",
"url": "https://github.com/iszn11/oktaeder.git"
"url": "https://gitea.renati.me/renati/oktaeder.git"
},
"scripts": {
"start:example": "esbuild example/script.ts --bundle --outfile=example/bundle.js --watch --servedir=example --format=esm --sourcemap",
"start:example": "bun run example/index.html",
"build": "tsc --build"
},
"dependencies": {
"tslib": "^2.6.1"
"tslib": "^2.8.1"
},
"devDependencies": {
"@webgpu/types": "^0.1.34",
"esbuild": "^0.19.2",
"typescript": "5.1.6"
"@webgpu/types": "^0.1.66",
"typescript": "^5.9.3"
},
"exports": {
".": {

View File

@@ -12,10 +12,10 @@ export class _BinaryWriter {
_buffer: ArrayBuffer;
_dataView: DataView;
_typedArray: Uint8Array;
_typedArray: Uint8Array<ArrayBuffer>;
_length: number;
get subarray(): Uint8Array { return new Uint8Array(this._buffer, 0, this._length); }
get subarray(): Uint8Array<ArrayBuffer> { return new Uint8Array(this._buffer, 0, this._length); }
constructor(capacity = _BinaryWriter.DEFAULT_CAPACITY) {
capacity = Math.max(capacity, 1);

View File

@@ -5,6 +5,7 @@
*/
import * as data from "./data";
import * as resources from "./resources";
/* INITIAL SUPPORT PLAN
*
@@ -68,7 +69,7 @@ import * as data from "./data";
* - issues error
* - BLEND: partial support
* - decoded, but not implemented
* - doubleSided: prtial support
* - doubleSided: partial support
* - decoded, but not implemented
*
* Extensions:
@@ -94,7 +95,7 @@ import * as data from "./data";
export interface ParseResult {
readonly cameras: readonly data.Camera[];
readonly materials: readonly data.Material[];
readonly materials: readonly resources.Material[];
readonly lights: readonly data.Light[];
readonly scenes: readonly data.Scene[];
readonly scene: data.Scene | null;
@@ -181,7 +182,7 @@ export async function parse(gltf: ArrayBufferView, {
}: ParseOptions = {}): Promise<ParseResult> {
const cameras: data.Camera[] = [];
const materials: data.Material[] = [];
const materials: resources.Material[] = [];
const lights: data.Light[] = [];
const scenes: data.Scene[] = [];
const scene: data.Scene | null = null;
@@ -239,7 +240,7 @@ export async function parse(gltf: ArrayBufferView, {
}
if (version !== 2) {
const message = `Unsupported binary glTF container format. The bytes 4-8 define the binary glTF conatiner format version when read as little endian unsigned integer. Only version 2 is supported, but in the provided buffer they have the value of ${version}`;
const message = `Unsupported binary glTF container format. The bytes 4-8 define the binary glTF container format version when read as little endian unsigned integer. Only version 2 is supported, but in the provided buffer they have the value of ${version}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
@@ -267,6 +268,10 @@ export async function parse(gltf: ArrayBufferView, {
// --- JSON CHUNK ----------------------------------------------------------
void(stopOnFirstError);
void(treatWarningsAsErrors);
void(rest);
throw new Error("TODO");
// --- BIN CHUNK -----------------------------------------------------------

View File

@@ -49,7 +49,7 @@ export class Renderer {
_textureWhite: Texture2D;
/** 1×1 rgba8unorm texture of [0, 0, 0, 255] */
_textureBlack: Texture2D;
/** 1×1 rgba8unorm texture of [128, 128, 128, 255] */
/** 1×1 rgba8unorm texture of [128, 128, 255, 255] */
_textureNormal: Texture2D;
_depthBuffer: Texture2D;
@@ -110,7 +110,7 @@ export class Renderer {
height: 1,
format: "linear",
});
this._textureNormal.writeFull(new Uint8Array([128, 128, 128, 255]));
this._textureNormal.writeFull(new Uint8Array([128, 128, 255, 255]));
const framebufferTexture = this._context.getCurrentTexture();
this._depthBuffer = new Texture2D(this, {

View File

@@ -64,7 +64,7 @@ export class IndexBuffer {
return this.writeTypedArray(offset, array);
}
writeTypedArray(offset: number, indices: Uint16Array | Uint32Array): IndexBuffer {
writeTypedArray(offset: number, indices: Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer>): IndexBuffer {
if (
this._indexFormat === "uint16" && !(indices instanceof Uint16Array)
|| this._indexFormat === "uint32" && !(indices instanceof Uint32Array)

View File

@@ -42,11 +42,11 @@ export interface VertexBufferWriteArrayProps {
}
export interface VertexBufferWriteTypedArrayProps {
readonly position?: Float32Array;
readonly texCoord?: Float32Array;
readonly lightTexCoord?: Float32Array;
readonly normal?: Float32Array;
readonly tangent?: Float32Array;
readonly position?: Float32Array<ArrayBuffer>;
readonly texCoord?: Float32Array<ArrayBuffer>;
readonly lightTexCoord?: Float32Array<ArrayBuffer>;
readonly normal?: Float32Array<ArrayBuffer>;
readonly tangent?: Float32Array<ArrayBuffer>;
}
export class VertexBuffer {

View File

@@ -46,10 +46,10 @@ export function _createPipeline(renderer: Renderer, {
const shaderModule = renderer._device.createShaderModule({
code: shaderCode,
hints: {
"vert": { layout: renderer._pipelineLayout },
"frag": { layout: renderer._pipelineLayout },
},
compilationHints: [
{ entryPoint: "vert", layout: renderer._pipelineLayout },
{ entryPoint: "frag", layout: renderer._pipelineLayout },
],
});
let vertexLocation = 0;
@@ -318,17 +318,17 @@ fn frag(fragment: Varyings) -> @location(0) vec4<f32> {
var emissive = _Material.emissive;
var ior = _Material.ior;
${texCoord ? `
let baseColorPartialCoverageTexel = texture(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord);
let baseColorPartialCoverageTexel = textureSample(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord);
baseColor *= baseColorPartialCoverageTexel.rgb;
partialCoverage *= baseColorPartialCoverageTexel.a;
let roughnessMetallicTexel = texture(_RoughnessMetallicTexture, _Sampler, fragment.texCoord);
let roughnessMetallicTexel = textureSample(_RoughnessMetallicTexture, _Sampler, fragment.texCoord);
roughness *= roughnessMetallicTexel.g;
metallic *= roughnessMetallicTexel.b;
let emissiveTexel = texture(_EmissiveTexture, _Sampler, fragment.texCoord);
let emissiveTexel = textureSample(_EmissiveTexture, _Sampler, fragment.texCoord);
emissive *= emissiveTexel.rgb;
` : ""}
${lightTexCoord ? `
let occlusionTexel = texture(_OcclusionTexture, _Sampler, fragment.lightTexCoord);
let occlusionTexel = textureSample(_OcclusionTexture, _Sampler, fragment.lightTexCoord);
occlusion += _Material.occlusionTextureStrength * (occlusionTexel.r - 1.0);
` : ""}
@@ -348,10 +348,10 @@ fn frag(fragment: Varyings) -> @location(0) vec4<f32> {
` : `
let matrixTStoVS = screenSpaceMatrixTStoVS(positionVS, geometricNormalVS, fragment.texCoord);
`}
let normalTextureTexel = texture(_NormalTexture, _Sampler, fragment.texCoord);
let normalTextureTexel = textureSample(_NormalTexture, _Sampler, fragment.texCoord);
var normalTS = normalTextureTexel.xyz * 2.0 - 1.0;
normalTS.xy *= _Material.normalScale;
let actualNormalVS = normalize(matrixTStoVS * geometricNormalVS);
normalTS = vec3(normalTS.xy * _Material.normalScale, normalTS.z);
let actualNormalVS = normalize(matrixTStoVS * normalTS);
` : `
let actualNormalVS = geometricNormalVS;
`}

File diff suppressed because one or more lines are too long