Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 501949828d | |||
|
|
d28d7896de | ||
| 78683f6115 |
@@ -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
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
2
AUTHORS
2
AUTHORS
@@ -1 +1 @@
|
||||
Szymon Nowakowski <smnbdg13@gmail.com> (https://renati.me)
|
||||
Szymon Nowakowski <renati@renati.me> (https://renati.me)
|
||||
|
||||
@@ -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
22
bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
4
example/types.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.png" {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
BIN
example/uvmap.png
(Stored with Git LFS)
Normal file
BIN
example/uvmap.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
oktaeder.webm
(Stored with Git LFS)
Normal file
BIN
oktaeder.webm
(Stored with Git LFS)
Normal file
Binary file not shown.
16
package.json
16
package.json
@@ -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": {
|
||||
".": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
13
src/gltf.ts
13
src/gltf.ts
@@ -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 -----------------------------------------------------------
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user