initial commit
This commit is contained in:
commit
c039544ec6
47 changed files with 2512 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules/
|
||||||
|
http/
|
||||||
|
serve
|
||||||
|
|
11
.gitmodules
vendored
Normal file
11
.gitmodules
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[submodule "public/vendor/bitecs"]
|
||||||
|
path = public/vendor/bitecs
|
||||||
|
url = https://github.com/NateTheGreatt/bitECS.git
|
||||||
|
branch = rc-0-4-0
|
||||||
|
[submodule "public/vendor/gl-matrix"]
|
||||||
|
path = public/vendor/gl-matrix
|
||||||
|
url = https://github.com/toji/gl-matrix.git
|
||||||
|
branch = v3.4.x
|
||||||
|
[submodule "public/vendor/kojima"]
|
||||||
|
path = public/vendor/kojima
|
||||||
|
url = https://git.kitsu.cafe/rowan/kojima.git
|
17
examples/cube/index.html
Normal file
17
examples/cube/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>HTML 5 Boilerplate</title>
|
||||||
|
<link rel="stylesheet" href="/public/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<canvas></canvas>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
205
examples/cube/index.js
Normal file
205
examples/cube/index.js
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import { addComponent, addEntity, createWorld, query } from '/src/ecs.js'
|
||||||
|
import { Engine, Startup, Update, World } from '/src/core/engine.js'
|
||||||
|
import { DefaultPlugins } from '/src/plugins/index.js'
|
||||||
|
import { WebGLContext } from '/src/plugins/renderer/webgl-context.js'
|
||||||
|
import { Camera, Mesh, Renderable, Transform } from '/src/components/index.js'
|
||||||
|
import * as twgl from '/public/vendor/twgl/twgl-full.module.js'
|
||||||
|
import { Assets, AssetType } from '/src/core/assets.js'
|
||||||
|
import { Geometry, Material, Materials, Meshes } from '/src/plugins/renderer/index.js'
|
||||||
|
|
||||||
|
/** @param {number} degrees */
|
||||||
|
const deg2rad = degrees => degrees * Math.PI / 180
|
||||||
|
|
||||||
|
// INFO: does a camera need a Transform, specifically
|
||||||
|
// a scale??
|
||||||
|
|
||||||
|
/** @param {World} world */
|
||||||
|
const createCamera = world => {
|
||||||
|
const entity = addEntity(world)
|
||||||
|
addComponent(world, entity, [Camera, Transform])
|
||||||
|
const transform = twgl.m4.identity()
|
||||||
|
twgl.m4.translate(transform, [0, 0, 10], transform)
|
||||||
|
Transform.matrix[entity] = transform
|
||||||
|
|
||||||
|
Camera.projectionMatrix[entity] = []
|
||||||
|
Camera.viewMatrix[entity] = []
|
||||||
|
Camera.up[entity] = new Float32Array([0, 1, 0])
|
||||||
|
Camera.fov[entity] = deg2rad(30)
|
||||||
|
Camera.zNear[entity] = 0.1
|
||||||
|
Camera.zFar[entity] = 1000
|
||||||
|
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cube = () => {
|
||||||
|
const position = [
|
||||||
|
// Front face
|
||||||
|
-0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
|
||||||
|
|
||||||
|
// Back face
|
||||||
|
-0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5,
|
||||||
|
|
||||||
|
// Top face
|
||||||
|
-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5,
|
||||||
|
|
||||||
|
// Bottom face
|
||||||
|
-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5,
|
||||||
|
|
||||||
|
// Right face
|
||||||
|
0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5,
|
||||||
|
|
||||||
|
// Left face
|
||||||
|
-0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5,
|
||||||
|
]
|
||||||
|
|
||||||
|
const indices = [
|
||||||
|
// Front face
|
||||||
|
0, 1, 2, 0, 2, 3,
|
||||||
|
|
||||||
|
// Back face
|
||||||
|
4, 5, 6, 4, 6, 7,
|
||||||
|
|
||||||
|
// Top face
|
||||||
|
8, 9, 10, 8, 10, 11,
|
||||||
|
|
||||||
|
// Bottom face
|
||||||
|
12, 13, 14, 12, 14, 15,
|
||||||
|
|
||||||
|
// Right face
|
||||||
|
16, 17, 18, 16, 18, 19,
|
||||||
|
|
||||||
|
// Left face
|
||||||
|
20, 21, 22, 20, 22, 23,
|
||||||
|
]
|
||||||
|
|
||||||
|
const color = [
|
||||||
|
// Front face (red)
|
||||||
|
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
|
||||||
|
|
||||||
|
// Back face (green)
|
||||||
|
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
|
||||||
|
|
||||||
|
// Top face (blue)
|
||||||
|
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
|
||||||
|
|
||||||
|
// Bottom face (yellow)
|
||||||
|
1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
|
||||||
|
|
||||||
|
// Right face (magenta)
|
||||||
|
1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1,
|
||||||
|
|
||||||
|
// Left face (cyan)
|
||||||
|
0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1,
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
position,
|
||||||
|
indices,
|
||||||
|
color: { numComponents: 3, data: color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quad = () => {
|
||||||
|
const position = [
|
||||||
|
-0.5, -0.5, 0,
|
||||||
|
0.5, -0.5, 0,
|
||||||
|
0.5, 0.5, 0,
|
||||||
|
-0.5, 0.5, 0,
|
||||||
|
]
|
||||||
|
|
||||||
|
const indices = [
|
||||||
|
0, 1, 2,
|
||||||
|
2, 3, 0,
|
||||||
|
]
|
||||||
|
|
||||||
|
const color = [
|
||||||
|
1, 0, 0,
|
||||||
|
0, 1, 0,
|
||||||
|
0, 0, 1,
|
||||||
|
1, 1, 0,
|
||||||
|
]
|
||||||
|
|
||||||
|
return { position, indices, color: { numComponents: 3, data: color } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const Triangle = () => {
|
||||||
|
return {
|
||||||
|
position: [-0.5, -0.5, 0, 0.5, -0.5, 0, 0, 0.5, 0],
|
||||||
|
color: { numComponents: 3, data: [1, 0, 0, 0, 1, 0, 0, 0, 1] },
|
||||||
|
indices: [0, 1, 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const createObject = (world, mesh, material, position) => {
|
||||||
|
const entity = addEntity(world)
|
||||||
|
addComponent(world, entity, [Renderable, Mesh, Transform])
|
||||||
|
const transform = twgl.m4.identity()
|
||||||
|
twgl.m4.translate(transform, position, transform)
|
||||||
|
Transform.matrix[entity] = transform
|
||||||
|
|
||||||
|
Mesh.geometry[entity] = mesh
|
||||||
|
Mesh.material[entity] = material
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
const canvas = document.querySelector('main canvas')
|
||||||
|
if (canvas.getContext) {
|
||||||
|
const world = createWorld(new World())
|
||||||
|
|
||||||
|
let app = new Engine(world, canvas)
|
||||||
|
.addResource(new WebGLContext(canvas))
|
||||||
|
.addResource(new Assets())
|
||||||
|
.addPlugin(DefaultPlugins)
|
||||||
|
.addSystem(Startup, async world => {
|
||||||
|
const gl = world.getResource(WebGLContext).context
|
||||||
|
const meshes = world.getResource(Meshes)
|
||||||
|
const materials = world.getResource(Materials)
|
||||||
|
const assets = world.getResource(Assets)
|
||||||
|
|
||||||
|
const camera = createCamera(world)
|
||||||
|
|
||||||
|
let sources = ['simple-vert.wgsl', 'simple-frag.wgsl']
|
||||||
|
.map(url => `/public/shaders/${url}`)
|
||||||
|
.map(import.meta.resolve)
|
||||||
|
.map(url => assets.load(url, AssetType.Text))
|
||||||
|
|
||||||
|
sources = await Promise.all(sources)
|
||||||
|
|
||||||
|
const mesh = Cube()
|
||||||
|
const geometry = new Geometry(mesh.position, mesh.indices).createBuffers()
|
||||||
|
|
||||||
|
const meshHandle = meshes.add(buffer)
|
||||||
|
const materialHandle = materials.add(new Material(
|
||||||
|
programInfo,
|
||||||
|
{ u_color: [1, 1, 1, 1] }
|
||||||
|
))
|
||||||
|
const one = createObject(world, meshHandle, materialHandle, [0, -1.5, 0])
|
||||||
|
let transform = Transform.matrix[one]
|
||||||
|
twgl.m4.rotateX(transform, deg2rad(45), transform)
|
||||||
|
twgl.m4.rotateY(transform, deg2rad(45), transform)
|
||||||
|
|
||||||
|
const two = createObject(world, meshHandle, materialHandle, [2, 1, 0])
|
||||||
|
transform = Transform.matrix[two]
|
||||||
|
twgl.m4.rotateX(transform, deg2rad(135), transform)
|
||||||
|
twgl.m4.rotateY(transform, deg2rad(135), transform)
|
||||||
|
|
||||||
|
const three = createObject(world, meshHandle, materialHandle, [-2, 1, 0])
|
||||||
|
transform = Transform.matrix[three]
|
||||||
|
twgl.m4.rotateX(transform, deg2rad(225), transform)
|
||||||
|
twgl.m4.rotateY(transform, deg2rad(225), transform)
|
||||||
|
|
||||||
|
})
|
||||||
|
.addSystem(Update, world => {
|
||||||
|
const meshes = query(world, [Mesh, Transform])
|
||||||
|
for (const mesh of meshes) {
|
||||||
|
const transform = Transform.matrix[mesh]
|
||||||
|
twgl.m4.rotateZ(transform, 0.01, transform)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
} else {
|
||||||
|
// canvas not supported
|
||||||
|
}
|
||||||
|
|
17
index.html
Normal file
17
index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>HTML 5 Boilerplate</title>
|
||||||
|
<link rel="stylesheet" href="public/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<canvas></canvas>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="src/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
16
jsconfig.json
Normal file
16
jsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "es2020",
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["es2019", "dom"],
|
||||||
|
"types": ["@webgpu/types"],
|
||||||
|
"checkJs": true,
|
||||||
|
"paths": {
|
||||||
|
"/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
19
package-lock.json
generated
Normal file
19
package-lock.json
generated
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "canvas",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"@webgpu/types": "^0.1.58"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.58",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.58.tgz",
|
||||||
|
"integrity": "sha512-+8+NBE17zrc1wS4FvZmmuGTpog5C2H6QC46RY2TTWNpnt15Xvp7dZoUHZQXvb8l5ddKwO8l1pDJa6XTnR4Al1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
package.json
Normal file
5
package.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@webgpu/types": "^0.1.58"
|
||||||
|
}
|
||||||
|
}
|
13
public/css/style.css
Normal file
13
public/css/style.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main canvas {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
width: min(100%, 100vh * 16 / 9);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
17
public/shaders/simple-frag.wgsl
Normal file
17
public/shaders/simple-frag.wgsl
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
struct Uniforms {
|
||||||
|
u_color: vec4f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> uniforms: Uniforms;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4f,
|
||||||
|
@location(0) v_color: vec4f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn main(input: VertexOutput) -> @location(0) vec4f {
|
||||||
|
return input.v_color * uniforms.u_color;
|
||||||
|
}
|
||||||
|
|
27
public/shaders/simple-vert.wgsl
Normal file
27
public/shaders/simple-vert.wgsl
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec4f,
|
||||||
|
@location(1) color: vec4f,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
u_projection: mat4x4f,
|
||||||
|
u_view: mat4x4f,
|
||||||
|
u_world: mat4x4f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> uniforms: Uniforms;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4f,
|
||||||
|
@location(0) v_color: vec4f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn main(input: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = uniforms.u_projection * uniforms.u_view * uniforms.u_world * input.position;
|
||||||
|
out.v_color = input.color;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
1
public/vendor/bitecs
vendored
Submodule
1
public/vendor/bitecs
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit caa1f58be2ccc304c1f0a085de34ca5904b3b80f
|
1
public/vendor/gl-matrix
vendored
Submodule
1
public/vendor/gl-matrix
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4e312f346713acd87dcb8aef9016121108eac07a
|
1
public/vendor/kojima
vendored
Submodule
1
public/vendor/kojima
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1224a8382cf7ec0e9409430f7185d0f857a2ec26
|
9
src/components/color.js
Normal file
9
src/components/color.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { DefaultSize } from './constants.js'
|
||||||
|
|
||||||
|
export const Rgba8 = {
|
||||||
|
r: new Uint8Array(DefaultSize),
|
||||||
|
g: new Uint8Array(DefaultSize),
|
||||||
|
b: new Uint8Array(DefaultSize),
|
||||||
|
a: new Uint8Array(DefaultSize),
|
||||||
|
}
|
||||||
|
|
2
src/components/constants.js
Normal file
2
src/components/constants.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const DefaultSize = 512
|
||||||
|
|
6
src/components/index.js
Normal file
6
src/components/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './math.js'
|
||||||
|
export * from './color.js'
|
||||||
|
export * from '../plugins/camera/components/index.js'
|
||||||
|
export * from '../plugins/renderer/components/index.js'
|
||||||
|
export * from '../plugins/transform/components/index.js'
|
||||||
|
|
105
src/components/math.js
Normal file
105
src/components/math.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import * as twgl from '../../public/vendor/twgl/twgl-full.module.js'
|
||||||
|
|
||||||
|
const identity = twgl.m4.identity()
|
||||||
|
const id = (size, m) => {
|
||||||
|
const len = m.length
|
||||||
|
for (let i = 0; i < len; i += size) {
|
||||||
|
m.set(identity, i)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
export const Range = size => ({
|
||||||
|
min: new Float32Array(size),
|
||||||
|
max: new Float32Array(size)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UVector2 = size => []
|
||||||
|
export const UVector3 = size => []
|
||||||
|
|
||||||
|
export const Vector2 = size => []
|
||||||
|
export const Vector3 = size => []
|
||||||
|
|
||||||
|
export const Matrix3x3 = size => []
|
||||||
|
export const Matrix4x4 = size => []
|
||||||
|
|
||||||
|
export class ComponentProxy {
|
||||||
|
id
|
||||||
|
_component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {import('../ecs.js').Component} component
|
||||||
|
*/
|
||||||
|
constructor(id, component) {
|
||||||
|
this.id = id
|
||||||
|
this._component = component
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOf() {
|
||||||
|
return this._component[this.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArrayComponentProxy extends ComponentProxy {
|
||||||
|
_length
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @param {import('../ecs.js').Component} component
|
||||||
|
* @param {number} length
|
||||||
|
*/
|
||||||
|
constructor(id, component, length) {
|
||||||
|
super(id, component)
|
||||||
|
this._length = length
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOf() {
|
||||||
|
return this._component.subarray(this.id, this.id + this._length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Vector2Proxy extends ArrayComponentProxy {
|
||||||
|
/*
|
||||||
|
* @param {number} id
|
||||||
|
* @param {import('../ecs.js').Component} [component=Vector2]
|
||||||
|
* @param {number} [length=2]
|
||||||
|
*/
|
||||||
|
constructor(id, component = Vector2, length = 2) {
|
||||||
|
super(id, component, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
get x() {
|
||||||
|
return this._component[this.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this._component[this.id + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Vector3Proxy extends Vector2Proxy {
|
||||||
|
/*
|
||||||
|
* @param {number} id
|
||||||
|
* @param {import('../ecs.js').Component} [component=Vector3]
|
||||||
|
* @param {number} [length=3]
|
||||||
|
*/
|
||||||
|
constructor(id, component = Vector3, length = 3) {
|
||||||
|
super(id, component, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
get z() {
|
||||||
|
return this._component[this.id + 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Matrix3x3Proxy extends ArrayComponentProxy {
|
||||||
|
constructor(id, component = Matrix3x3) {
|
||||||
|
super(id, component, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Matrix4x4Proxy extends ArrayComponentProxy {
|
||||||
|
constructor(id, component = Matrix4x4) {
|
||||||
|
super(id, component, 16)
|
||||||
|
}
|
||||||
|
}
|
112
src/core/assets.js
Normal file
112
src/core/assets.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { NotImplementedError } from '../utils/error.js'
|
||||||
|
|
||||||
|
export const AssetType = Object.freeze({
|
||||||
|
Text: url => fetch(url).then(res => res.text()),
|
||||||
|
ArrayBuffer: url => fetch(url).then(res => res.arrayBuffer()),
|
||||||
|
Image: url => new Promise((resolve, reject) => {
|
||||||
|
const image = new Image()
|
||||||
|
image.onload = () => resolve(image)
|
||||||
|
image.onerror = err => reject(err)
|
||||||
|
image.src = url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
class StorageLayer {
|
||||||
|
has(_key) {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
get(_key) {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
set(_key, _value) {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryStorage extends StorageLayer {
|
||||||
|
#memory = new Map()
|
||||||
|
|
||||||
|
has(key) {
|
||||||
|
return this.#memory.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return this.#memory.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
this.#memory.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalStorage extends StorageLayer {
|
||||||
|
#cache = new MemoryStorage()
|
||||||
|
|
||||||
|
has(key) {
|
||||||
|
if (this.#cache.has(key)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
const item = localStorage.getItem(key)
|
||||||
|
if (item != null) {
|
||||||
|
this.#cache.set(key, item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
if (this.#cache.has(key)) {
|
||||||
|
return this.#cache.get(key)
|
||||||
|
} else {
|
||||||
|
const item = localStorage.getItem(key)
|
||||||
|
if (item != null) {
|
||||||
|
this.#cache.set(key, item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
this.#cache.set(key, value)
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Assets {
|
||||||
|
#storage = new LocalStorage()
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UrlLike} url
|
||||||
|
* @param {(url: UrlLike) => Promise<any>} handler
|
||||||
|
*/
|
||||||
|
async load(url, handler = AssetType.ArrayBuffer) {
|
||||||
|
if (this.#storage.has(url)) {
|
||||||
|
return this.#storage.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await handler(url)
|
||||||
|
this.#storage.set(url, data)
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UrlLike} url
|
||||||
|
*/
|
||||||
|
get(url) {
|
||||||
|
return this.#storage.get(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
24
src/core/ecs.js
Normal file
24
src/core/ecs.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { isObject } from './utils/index.js'
|
||||||
|
import { addComponent as add } from '../public/vendor/bitecs/index.js'
|
||||||
|
import { World } from './core/engine.js'
|
||||||
|
export * from '../public/vendor/bitecs/index.js'
|
||||||
|
|
||||||
|
/** @typedef {import('./core/engine.js').Plugin} Plugin */
|
||||||
|
/** @typedef {any} Component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {World} world
|
||||||
|
* @param {number} entity
|
||||||
|
* @param {Component} component
|
||||||
|
*/
|
||||||
|
export const addComponent = (world, entity, component) => {
|
||||||
|
if (Array.isArray(component)) {
|
||||||
|
const len = component.length
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
add(world, entity, component[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add(world, entity, component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
217
src/core/engine.js
Normal file
217
src/core/engine.js
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import { addComponent, addEntity } from '../ecs.js'
|
||||||
|
import { PrioritySchedule, SystemSchedule, Schedules } from './schedule.js'
|
||||||
|
|
||||||
|
/** @typedef {(engine: Engine) => void} Plugin */
|
||||||
|
/** @typedef {number} Entity */
|
||||||
|
|
||||||
|
export class Startup { }
|
||||||
|
export class Update { }
|
||||||
|
export class FixedUpdate { }
|
||||||
|
export class Render { }
|
||||||
|
|
||||||
|
export class Time {
|
||||||
|
/** @type {DOMHighResTimeStamp} */
|
||||||
|
currentTime
|
||||||
|
|
||||||
|
/** @type {DOMHighResTimeStamp} */
|
||||||
|
deltaTime
|
||||||
|
}
|
||||||
|
|
||||||
|
class Resources {
|
||||||
|
#inner = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} resource
|
||||||
|
*/
|
||||||
|
add(resource) {
|
||||||
|
this.#inner.set(resource.constructor, resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {new(...args: any[]) => T} resource
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
get(resource) {
|
||||||
|
return this.#inner.get(resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class World {
|
||||||
|
#resources = new Resources()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T} resource
|
||||||
|
*/
|
||||||
|
addResource(resource) {
|
||||||
|
this.#resources.add(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {new(...args: any[]) => T} resource
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
getResource(resource) {
|
||||||
|
return this.#resources.get(resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Canvas {
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
#element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} element
|
||||||
|
*/
|
||||||
|
constructor(element) {
|
||||||
|
this.#element = element
|
||||||
|
}
|
||||||
|
|
||||||
|
get element() {
|
||||||
|
return this.#element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add analog to Commands/EntityCommands
|
||||||
|
export class Engine {
|
||||||
|
/** @type {World} */
|
||||||
|
#world
|
||||||
|
|
||||||
|
/** @type {Plugin[]} */
|
||||||
|
#plugins = []
|
||||||
|
|
||||||
|
#schedules = new Schedules()
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
#loopId
|
||||||
|
|
||||||
|
#initialized = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {World} world
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
*/
|
||||||
|
constructor(world, canvas) {
|
||||||
|
this.#world = world
|
||||||
|
|
||||||
|
this.addResource(new Time())
|
||||||
|
this.addResource(new Canvas(canvas))
|
||||||
|
|
||||||
|
this.#schedules.add(
|
||||||
|
Startup,
|
||||||
|
new PrioritySchedule(0, new SystemSchedule())
|
||||||
|
)
|
||||||
|
|
||||||
|
this.#schedules.add(
|
||||||
|
Update,
|
||||||
|
new PrioritySchedule(1, new SystemSchedule())
|
||||||
|
)
|
||||||
|
|
||||||
|
this.#schedules.addBefore(
|
||||||
|
Update,
|
||||||
|
FixedUpdate,
|
||||||
|
new SystemSchedule()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.#schedules.addAfter(
|
||||||
|
Update,
|
||||||
|
Render,
|
||||||
|
new SystemSchedule()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get world() {
|
||||||
|
return this.#world
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} resource
|
||||||
|
* @returns {Engine}
|
||||||
|
*/
|
||||||
|
addResource(resource) {
|
||||||
|
this.#world.addResource(resource)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {new(...args: any[]) => T} resource
|
||||||
|
* @returns {T}
|
||||||
|
*/
|
||||||
|
getResource(resource) {
|
||||||
|
return this.#world.getResource(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./schedule.js').ScheduleLabel} schedule
|
||||||
|
* @param {import('./schedule').System} system
|
||||||
|
* @param {import('./schedule').RegisterSystemOptions} [options={}]
|
||||||
|
* @returns {Engine}
|
||||||
|
*/
|
||||||
|
addSystem(schedule, system, options) {
|
||||||
|
this.#schedules.get(schedule).register(system, options)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Plugin | Plugin[]} plugin
|
||||||
|
* @returns {Engine}
|
||||||
|
*/
|
||||||
|
addPlugin(plugin) {
|
||||||
|
if (Array.isArray(plugin)) {
|
||||||
|
plugin.forEach(this.addPlugin.bind(this))
|
||||||
|
} else {
|
||||||
|
plugin(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
cancelAnimationFrame(this.#loopId)
|
||||||
|
this.#loopId = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
// FIXME: modularize this
|
||||||
|
const startup = this.#schedules.get(Startup).schedule
|
||||||
|
const main = this.#schedules.get(Update).schedule
|
||||||
|
const fixed = this.#schedules.get(FixedUpdate).schedule
|
||||||
|
const render = this.#schedules.get(Render).schedule
|
||||||
|
|
||||||
|
const fixedTimeStep = 1000 / 60
|
||||||
|
|
||||||
|
let lastTime = performance.now()
|
||||||
|
let accumulator = 0
|
||||||
|
|
||||||
|
/** @type (time: DOMHighResTimeStamp) => void */
|
||||||
|
const loop = currentTime => {
|
||||||
|
const deltaTime = currentTime - lastTime
|
||||||
|
lastTime = currentTime
|
||||||
|
accumulator += deltaTime
|
||||||
|
|
||||||
|
const time = this.getResource(Time)
|
||||||
|
|
||||||
|
time.currentTime = currentTime
|
||||||
|
time.deltaTime = deltaTime
|
||||||
|
|
||||||
|
while (accumulator >= fixedTimeStep) {
|
||||||
|
fixed.execute(this.#world)
|
||||||
|
accumulator -= fixedTimeStep
|
||||||
|
}
|
||||||
|
|
||||||
|
main.execute(this.#world)
|
||||||
|
render.execute(this.#world)
|
||||||
|
|
||||||
|
this.#loopId = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
await startup.execute(this.#world)
|
||||||
|
this.#loopId = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
329
src/core/schedule.js
Normal file
329
src/core/schedule.js
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
import { PriorityMap } from '../utils/ordered-map.js'
|
||||||
|
|
||||||
|
/** @typedef {Function} ScheduleLabel */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template Dependency
|
||||||
|
*/
|
||||||
|
export class CyclicalDependencyError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {Dependency} dependency
|
||||||
|
*/
|
||||||
|
constructor(dependency) {
|
||||||
|
super(`Cyclical dependency: ${dependency}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template Dependency
|
||||||
|
*/
|
||||||
|
export class UnknownDependencyError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {Dependency} dependency
|
||||||
|
*/
|
||||||
|
constructor(dependency) {
|
||||||
|
super(`Dependency ${dependency} not found`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template Node
|
||||||
|
*/
|
||||||
|
export class Visitor {
|
||||||
|
#nodes
|
||||||
|
#recursion
|
||||||
|
#visited
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Visitor
|
||||||
|
* @param {Iterable.<Node>} nodes
|
||||||
|
*/
|
||||||
|
constructor(nodes) {
|
||||||
|
this.#nodes = Array.from(nodes)
|
||||||
|
this.#visited = new Set()
|
||||||
|
this.#recursion = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {(node: Node) => void} VisitNode */
|
||||||
|
/** @typedef {(node: Node, visit: VisitNode) => void} HandleNode */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
* @param {HandleNode} onVisit
|
||||||
|
*
|
||||||
|
* @throws {CircularDependencyError}
|
||||||
|
* Thrown if two nodes rely on one another
|
||||||
|
*/
|
||||||
|
_visitNode(node, onVisit) {
|
||||||
|
if (this.#recursion.has(node)) {
|
||||||
|
throw new CyclicalDependencyError(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#visited.has(node)) return
|
||||||
|
|
||||||
|
this.#visited.add(node)
|
||||||
|
this.#recursion.add(node)
|
||||||
|
|
||||||
|
onVisit(node, node => this._visitNode(node, onVisit))
|
||||||
|
|
||||||
|
this.#recursion.delete(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit all nodes with the onVisit callback
|
||||||
|
*
|
||||||
|
* @param {HandleNode} onVisit
|
||||||
|
* @returns {Node[]}
|
||||||
|
*/
|
||||||
|
visit(onVisit) {
|
||||||
|
const len = this.#nodes.length
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
this._visitNode(this.#nodes[i], onVisit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.#visited.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the visitor
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.#visited.clear()
|
||||||
|
this.#recursion.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Schedule {
|
||||||
|
execute(world) {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {(world: any) => void} System */
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SystemSchema
|
||||||
|
* @property {System} system
|
||||||
|
* @property {Set<System>} dependencies
|
||||||
|
* @property {Set<System>} before
|
||||||
|
* @property {Set<System>} after
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} RegisterSystemOptions
|
||||||
|
* @property {System[]} [options.dependencies = []]
|
||||||
|
* @property {System} [options.before]
|
||||||
|
* @property {System} [options.after]
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SystemSchedule {
|
||||||
|
/** @type {Map<System, SystemSchema>} */
|
||||||
|
_graph = new Map()
|
||||||
|
|
||||||
|
/** @type {System[]?} */
|
||||||
|
_cache = undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {System} node
|
||||||
|
* @returns {SystemSchema}
|
||||||
|
*
|
||||||
|
* @throws {UnknownDependencyError}
|
||||||
|
* Thrown if the requested node is not in the dependency graph
|
||||||
|
*/
|
||||||
|
_tryGet(node) {
|
||||||
|
if (!this._graph.has(node)) {
|
||||||
|
throw new UnknownDependencyError(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._graph.get(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SystemSchema} beforeNode
|
||||||
|
* @param {SystemSchema} afterNode
|
||||||
|
*/
|
||||||
|
_insert(beforeNode, afterNode) {
|
||||||
|
afterNode.before.add(beforeNode.system)
|
||||||
|
beforeNode.after.add(afterNode.system)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new system with this schedule
|
||||||
|
*
|
||||||
|
* @param {System} system
|
||||||
|
* @param {RegisterSystemOptions} [options={}]
|
||||||
|
*/
|
||||||
|
register(system, { dependencies = [], before, after } = {}) {
|
||||||
|
this._cache = undefined
|
||||||
|
|
||||||
|
this._graph.set(system, { system, dependencies: new Set(dependencies), before: new Set(), after: new Set() })
|
||||||
|
|
||||||
|
const systemNode = this._graph.get(system)
|
||||||
|
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
const depNode = this._tryGet(dep)
|
||||||
|
depNode.after.add(system)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
const beforeNode = this._tryGet(before)
|
||||||
|
this._insert(systemNode, beforeNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after) {
|
||||||
|
const afterNode = this._tryGet(after)
|
||||||
|
this._insert(afterNode, systemNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {System[]}
|
||||||
|
*
|
||||||
|
* @throws {UnknownDependencyError}
|
||||||
|
* Thrown if a dependency was not registered before resolution
|
||||||
|
*
|
||||||
|
* @throws {CircularDependencyError}
|
||||||
|
* Thrown if two nodes rely on one another
|
||||||
|
*/
|
||||||
|
resolve() {
|
||||||
|
if (this._cache) {
|
||||||
|
return this._cache
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitor = new Visitor(this._graph.keys())
|
||||||
|
|
||||||
|
this._cache = visitor.visit(
|
||||||
|
(next, visit) => {
|
||||||
|
const node = this._graph.get(next)
|
||||||
|
|
||||||
|
for (const dep of node.dependencies) {
|
||||||
|
visit(dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const before of node.before) {
|
||||||
|
visit(before)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return this._cache
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} world
|
||||||
|
*
|
||||||
|
* @throws {UnknownDependencyError}
|
||||||
|
* Thrown if a dependency was not registered before resolution
|
||||||
|
*
|
||||||
|
* @throws {CircularDependencyError}
|
||||||
|
* Thrown if two nodes rely on one another
|
||||||
|
*/
|
||||||
|
async execute(world) {
|
||||||
|
const systems = this.resolve()
|
||||||
|
const len = systems.length
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
await systems[i](world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrioritySchedule {
|
||||||
|
/** @type {Number} */
|
||||||
|
priority
|
||||||
|
|
||||||
|
/** @type {SystemSchedule} */
|
||||||
|
schedule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Number} priority
|
||||||
|
* @param {SystemSchedule} schedule
|
||||||
|
*/
|
||||||
|
constructor(priority, schedule) {
|
||||||
|
this.priority = priority
|
||||||
|
this.schedule = schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
register(system, options) {
|
||||||
|
this.schedule.register(system, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(world) {
|
||||||
|
await this.schedule.execute(world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[ScheduleLabel, PrioritySchedule]} value
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
const getPriority = value => value[1].priority
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends PriorityMap<ScheduleLabel, PrioritySchedule>
|
||||||
|
*/
|
||||||
|
export class Schedules extends PriorityMap {
|
||||||
|
/** @type {Number} */
|
||||||
|
#lowest
|
||||||
|
|
||||||
|
/** @type {Number} */
|
||||||
|
#highest
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(getPriority)
|
||||||
|
|
||||||
|
this.#lowest = -Infinity
|
||||||
|
this.#highest = Infinity
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ScheduleLabel} target
|
||||||
|
* @param {ScheduleLabel} label
|
||||||
|
* @param {SystemSchedule} schedule
|
||||||
|
*/
|
||||||
|
addBefore(target, label, schedule) {
|
||||||
|
const targetSchedule = this.get(target)
|
||||||
|
let priority = this.#lowest
|
||||||
|
|
||||||
|
if (targetSchedule) {
|
||||||
|
priority = targetSchedule.priority - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.add(label, new PrioritySchedule(priority, schedule))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ScheduleLabel} target
|
||||||
|
* @param {ScheduleLabel} label
|
||||||
|
* @param {SystemSchedule} schedule
|
||||||
|
*/
|
||||||
|
addAfter(target, label, schedule) {
|
||||||
|
const targetSchedule = this.get(target)
|
||||||
|
let priority = this.#lowest
|
||||||
|
|
||||||
|
if (targetSchedule) {
|
||||||
|
priority = targetSchedule.priority + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.add(label, new PrioritySchedule(priority, schedule))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ScheduleLabel} label
|
||||||
|
* @param {PrioritySchedule} prioritySchedule
|
||||||
|
*/
|
||||||
|
add(label, prioritySchedule) {
|
||||||
|
const priority = prioritySchedule.priority
|
||||||
|
|
||||||
|
if (priority > this.#lowest) {
|
||||||
|
this.#lowest = priority
|
||||||
|
} else if (priority < this.#highest) {
|
||||||
|
this.#highest = priority
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set(label, prioritySchedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
4
src/mixins/index.js
Normal file
4
src/mixins/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const mixin = (target, mixin) => {
|
||||||
|
Object.assign(target, mixin)
|
||||||
|
}
|
||||||
|
|
78
src/plugins/camera/components/index.js
Normal file
78
src/plugins/camera/components/index.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { DefaultSize } from '../../../components/constants.js'
|
||||||
|
import { Vector3, Matrix4x4 } from '../../../components/math.js'
|
||||||
|
|
||||||
|
class RenderGraph {
|
||||||
|
#nodes = new Map()
|
||||||
|
#edges = new Map()
|
||||||
|
|
||||||
|
add(name, render, dependencies = []) {
|
||||||
|
this.node.set(name, render)
|
||||||
|
this.edges.set(name, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(world) {
|
||||||
|
const executed = new Set()
|
||||||
|
const order = this.sort()
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of order) {
|
||||||
|
const render = this.nodes.get(node)
|
||||||
|
render(world)
|
||||||
|
executed.add(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort() {
|
||||||
|
const visited = new Set()
|
||||||
|
const stack = []
|
||||||
|
const cycles = new Set()
|
||||||
|
|
||||||
|
const visit = node => {
|
||||||
|
if (cycles.has(node)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visited.has(node)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(node)
|
||||||
|
cycles.add(node)
|
||||||
|
|
||||||
|
for (const dep of this.dependentsFor(name)) {
|
||||||
|
if (!visit(dep)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cycles.delete(name)
|
||||||
|
stack.push(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node in this.nodes) {
|
||||||
|
if (!visited.has(node) && !visit(node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependentsFor(name) {
|
||||||
|
return this.edges.values()
|
||||||
|
.filter(deps => deps.includes(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Camera = {
|
||||||
|
projectionMatrix: Matrix4x4(),
|
||||||
|
viewMatrix: Matrix4x4(),
|
||||||
|
fov: new Float32Array(DefaultSize),
|
||||||
|
zNear: new Float32Array(DefaultSize),
|
||||||
|
zFar: new Float32Array(DefaultSize),
|
||||||
|
up: Vector3(),
|
||||||
|
}
|
||||||
|
|
9
src/plugins/camera/index.js
Normal file
9
src/plugins/camera/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Engine, Update } from '../../core/engine.js'
|
||||||
|
import { updateCamera } from './systems/index.js'
|
||||||
|
|
||||||
|
/** @param {Engine} engine */
|
||||||
|
export const CameraPlugin = engine => {
|
||||||
|
engine.addSystem(Update, updateCamera(engine.world))
|
||||||
|
console.log('hi from camera plugin!')
|
||||||
|
}
|
||||||
|
|
3
src/plugins/camera/render-graph.js
Normal file
3
src/plugins/camera/render-graph.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class RenderGraph {
|
||||||
|
|
||||||
|
}
|
39
src/plugins/camera/systems/index.js
Normal file
39
src/plugins/camera/systems/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { World } from '../../../core/engine.js'
|
||||||
|
import { query } from '../../../ecs.js'
|
||||||
|
import { Transform } from '../../transform/components/index.js'
|
||||||
|
import { Camera } from '../components/index.js'
|
||||||
|
import { WebGLContext } from '../../renderer/webgl-context.js'
|
||||||
|
import * as twgl from '/public/vendor/twgl/twgl-full.module.js'
|
||||||
|
|
||||||
|
/** @param {World} world */
|
||||||
|
export const updateCamera = world => {
|
||||||
|
/** @type {WebGLContext} */
|
||||||
|
const glContext = world.getResource(WebGLContext)
|
||||||
|
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const canvas = glContext.context.canvas
|
||||||
|
|
||||||
|
/** @param {World} world */
|
||||||
|
return world => {
|
||||||
|
const cameras = query(world, [Camera, Transform])
|
||||||
|
const len = cameras.length
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const entity = cameras[i]
|
||||||
|
|
||||||
|
const projection = Camera.projectionMatrix[entity]
|
||||||
|
const transform = Transform[entity]
|
||||||
|
const view = Camera.viewMatrix[entity]
|
||||||
|
|
||||||
|
const fov = Camera.fov[entity]
|
||||||
|
const aspect = canvas.clientWidth / canvas.clientHeight
|
||||||
|
const zNear = Camera.zNear[entity]
|
||||||
|
const zFar = Camera.zFar[entity]
|
||||||
|
|
||||||
|
twgl.m4.perspective(fov, aspect, zNear, zFar, projection)
|
||||||
|
twgl.m4.inverse(transform, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
src/plugins/index.js
Normal file
16
src/plugins/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Engine } from '../core/engine.js'
|
||||||
|
import { CameraPlugin } from './camera/index.js'
|
||||||
|
import { RendererPlugin } from './renderer/index.js'
|
||||||
|
import { TransformPlugin } from './transform/index.js'
|
||||||
|
import { WindowPlugin } from './window/index.js'
|
||||||
|
|
||||||
|
/** @param {Engine} engine */
|
||||||
|
export const DefaultPlugins = engine => {
|
||||||
|
engine.addPlugin([
|
||||||
|
CameraPlugin,
|
||||||
|
RendererPlugin,
|
||||||
|
TransformPlugin,
|
||||||
|
WindowPlugin
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
8
src/plugins/renderer/components/index.js
Normal file
8
src/plugins/renderer/components/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { DefaultSize } from '/src/components/constants.js'
|
||||||
|
|
||||||
|
export const Renderable = {}
|
||||||
|
export const Mesh = {
|
||||||
|
geometry: new Uint8Array(DefaultSize),
|
||||||
|
material: new Uint8Array(DefaultSize),
|
||||||
|
}
|
||||||
|
|
10
src/plugins/renderer/index.js
Normal file
10
src/plugins/renderer/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Engine, Render } from '../../core/engine.js'
|
||||||
|
import { render } from './systems/index.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Engine} engine
|
||||||
|
*/
|
||||||
|
export const RendererPlugin = engine => {
|
||||||
|
engine.addSystem(Render, render(engine.world))
|
||||||
|
}
|
||||||
|
|
4
src/plugins/renderer/mesh/index.js
Normal file
4
src/plugins/renderer/mesh/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { Union } from '/public/vendor/kojima/union.js'
|
||||||
|
|
||||||
|
export const Indices = Union(['U16', 'U32'])
|
||||||
|
|
65
src/plugins/renderer/mesh/mesh.js
Normal file
65
src/plugins/renderer/mesh/mesh.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { MeshAttributeData } from './vertex.js'
|
||||||
|
import { Indices } from './index.js'
|
||||||
|
import { assignOptions } from '/src/utils/index.js'
|
||||||
|
|
||||||
|
export const PrimitiveTopology = Object.freeze({
|
||||||
|
PointList: 'point-list',
|
||||||
|
LineList: 'line-list',
|
||||||
|
LineStrip: 'line-strip',
|
||||||
|
TriangleList: 'triangle-list',
|
||||||
|
TriangleStrip: 'triangle-strip'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const IndexFormat = Object.freeze({
|
||||||
|
Uint16: 'uint16',
|
||||||
|
Uint32: 'uint32'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const FrontFace = Object.freeze({
|
||||||
|
Ccw: 'ccw',
|
||||||
|
Cw: 'cw'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CullMode = Object.freeze({
|
||||||
|
None: 'none',
|
||||||
|
Front: 'front',
|
||||||
|
Back: 'back'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class PrimitiveState {
|
||||||
|
#topology = PrimitiveTopology.TriangleList
|
||||||
|
#stripIndexFormat
|
||||||
|
#frontFace = FrontFace.Ccw
|
||||||
|
#cullMode = CullMode.None
|
||||||
|
|
||||||
|
#unclippedDepth = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUIndexFormat} stripIndexFormat
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {GPUPrimitiveTopology} [options.topology]
|
||||||
|
* @param {GPUFrontFace} [options.frontFace]
|
||||||
|
* @param {GPUCullMode} [options.cullMode]
|
||||||
|
* @param {boolean} [options.unclippedDepth]
|
||||||
|
*/
|
||||||
|
constructor(stripIndexFormat, options) {
|
||||||
|
this.#stripIndexFormat = stripIndexFormat
|
||||||
|
assignOptions(this, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
get topology() { return this.#topology }
|
||||||
|
get stripIndexFormat() { return this.#stripIndexFormat }
|
||||||
|
get frontFace() { return this.#frontFace }
|
||||||
|
get cullMode() { return this.#cullMode }
|
||||||
|
get unclippedDepth() { return this.#unclippedDepth }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Mesh {
|
||||||
|
/** @type {PrimitiveTopology} */
|
||||||
|
topology
|
||||||
|
/** @type {Map<VertexAttributeId, MeshAttributeData>} */
|
||||||
|
attributes = new Map()
|
||||||
|
/** @type {Indices} */
|
||||||
|
indices
|
||||||
|
}
|
||||||
|
|
116
src/plugins/renderer/mesh/vertex-format.js
Normal file
116
src/plugins/renderer/mesh/vertex-format.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// https://www.w3.org/TR/webgpu/#enumdef-gpuvertexformat
|
||||||
|
|
||||||
|
import { Union } from '/public/vendor/kojima/union.js'
|
||||||
|
import { capitalizen } from '/src/utils/index'
|
||||||
|
|
||||||
|
/** @typedef {Uint8ArrayConstructor | Int8ArrayConstructor | Uint16ArrayConstructor | Int16ArrayConstructor | Uint32ArrayConstructor | Int32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
|
||||||
|
|
||||||
|
/** @type {GPUVertexFormat[]} */
|
||||||
|
export const GPUVertexFormats = ['uint8', 'uint8x2', 'uint8x4', 'sint8', 'sint8x2', 'sint8x4', 'unorm8', 'unorm8x2', 'unorm8x4', 'snorm8', 'snorm8x2', 'snorm8x4', 'uint16', 'uint16x2', 'uint16x4', 'sint16', 'sint16x2', 'sint16x4', 'unorm16', 'unorm16x2', 'unorm16x4', 'snorm16', 'snorm16x2', 'snorm16x4', 'float16', 'float16x2', 'float16x4', 'float32', 'float32x2', 'float32x3', 'float32x4', 'uint32', 'uint32x2', 'uint32x3', 'uint32x4', 'sint32', 'sint32x2', 'sint32x3', 'sint32x4', 'unorm10-10-10-2', 'unorm8x4-bgra']
|
||||||
|
|
||||||
|
export class MeshAttributeData {
|
||||||
|
#format
|
||||||
|
#buffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {VertexFormat} format
|
||||||
|
* @param {ArrayBufferLike} buffer
|
||||||
|
*/
|
||||||
|
constructor(format, buffer) {
|
||||||
|
this.#format = format
|
||||||
|
this.#buffer = buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() { return this.#format }
|
||||||
|
get buffer() { return this.#buffer }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VertexFormat {
|
||||||
|
#name
|
||||||
|
#components
|
||||||
|
#size
|
||||||
|
#bufferType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUVertexFormat} name
|
||||||
|
* @param {number} components
|
||||||
|
* @param {number} size
|
||||||
|
* @param {TypedArrayConstructor} bufferType
|
||||||
|
*/
|
||||||
|
constructor(name, components, size, bufferType) {
|
||||||
|
this.#name = name
|
||||||
|
this.#components = components
|
||||||
|
this.#size = size
|
||||||
|
this.#bufferType = bufferType
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this.#name }
|
||||||
|
get components() { return this.#components }
|
||||||
|
get size() { return this.#size }
|
||||||
|
get bufferType() { return this.#bufferType }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// INFO: ugly initialization code for VertexFormat statics
|
||||||
|
// this is for doing VertexFormat.Uint8 to get that format
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
/** @type {[GPUVertexFormat, number, number, TypedArrayConstructor][]} */
|
||||||
|
const formats = [
|
||||||
|
['uint8', 1, 1, Uint8Array],
|
||||||
|
['uint8x2', 2, 2, Uint8Array],
|
||||||
|
['uint8x4', 4, 4, Uint8Array],
|
||||||
|
['sint8', 1, 1, Int8Array],
|
||||||
|
['sint8x2', 2, 2, Int8Array],
|
||||||
|
['sint8x4', 4, 4, Int8Array],
|
||||||
|
['unorm8', 1, 1, Uint8Array],
|
||||||
|
['unorm8x2', 2, 2, Uint8Array],
|
||||||
|
['unorm8x4', 4, 4, Uint8Array],
|
||||||
|
['snorm8', 1, 1, Int8Array],
|
||||||
|
['snorm8x2', 2, 2, Int8Array],
|
||||||
|
['snorm8x4', 4, 4, Int8Array],
|
||||||
|
['uint16', 1, 2, Uint16Array],
|
||||||
|
['uint16x2', 2, 4, Uint16Array],
|
||||||
|
['uint16x4', 4, 8, Uint16Array],
|
||||||
|
['sint16', 1, 2, Int16Array],
|
||||||
|
['sint16x2', 2, 4, Int16Array],
|
||||||
|
['sint16x4', 4, 8, Int16Array],
|
||||||
|
['unorm16', 1, 2, Uint16Array],
|
||||||
|
['unorm16x2', 2, 4, Uint16Array],
|
||||||
|
['unorm16x4', 4, 8, Uint16Array],
|
||||||
|
['snorm16', 1, 2, Int16Array],
|
||||||
|
['snorm16x2', 2, 4, Int16Array],
|
||||||
|
['snorm16x4', 4, 8, Int16Array],
|
||||||
|
['float16', 1, 2, Uint16Array],
|
||||||
|
['float16x2', 2, 4, Uint16Array],
|
||||||
|
['float16x4', 4, 8, Uint16Array],
|
||||||
|
['float32', 1, 4, Float32Array],
|
||||||
|
['float32x2', 2, 8, Float32Array],
|
||||||
|
['float32x3', 3, 12, Float32Array],
|
||||||
|
['float32x4', 4, 16, Float32Array],
|
||||||
|
['uint32', 1, 4, Uint32Array],
|
||||||
|
['uint32x2', 2, 8, Uint32Array],
|
||||||
|
['uint32x3', 3, 12, Uint32Array],
|
||||||
|
['uint32x4', 4, 16, Uint32Array],
|
||||||
|
['sint32', 1, 4, Int32Array],
|
||||||
|
['sint32x2', 2, 8, Int32Array],
|
||||||
|
['sint32x3', 3, 12, Int32Array],
|
||||||
|
['sint32x4', 4, 16, Int32Array],
|
||||||
|
['unorm10-10-10-2', 4, 4, Uint32Array],
|
||||||
|
['unorm8x4-bgra', 4, 4, Uint8Array]
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
const capitalize = name => name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
|
||||||
|
formats.forEach(([name, components, size]) => {
|
||||||
|
Object.defineProperty(VertexFormat, capitalize(value.name), {
|
||||||
|
value: new VertexFormat(name, components, size),
|
||||||
|
writable: false,
|
||||||
|
configurable: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
213
src/plugins/renderer/mesh/vertex.js
Normal file
213
src/plugins/renderer/mesh/vertex.js
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
import { VertexFormat } from './vertex-format.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {number} MeshVertexAttributeId
|
||||||
|
* @typedef {number} BufferAddress
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class VertexAttribute {
|
||||||
|
#format
|
||||||
|
#offset
|
||||||
|
#shaderLocation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {VertexFormat} format
|
||||||
|
* @param {number} offset
|
||||||
|
* @param {number} shaderLocation
|
||||||
|
*/
|
||||||
|
constructor(format, offset, shaderLocation) {
|
||||||
|
this.#format = format
|
||||||
|
this.#offset = offset
|
||||||
|
this.#shaderLocation = shaderLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() { return this.#format }
|
||||||
|
get offset() { return this.#offset }
|
||||||
|
get shaderLocation() { return this.#shaderLocation }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MeshVertexAttribute {
|
||||||
|
#name
|
||||||
|
#id
|
||||||
|
#format
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {MeshVertexAttributeId} id
|
||||||
|
* @param {VertexFormat} format
|
||||||
|
*/
|
||||||
|
constructor(name, id, format) {
|
||||||
|
this.#name = name
|
||||||
|
this.#id = id
|
||||||
|
this.#format = format
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this.#name }
|
||||||
|
get id() { return this.#id }
|
||||||
|
get format() { return this.#format }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VertexAttributeDescriptor {
|
||||||
|
#id
|
||||||
|
#name
|
||||||
|
#shaderLocation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MeshVertexAttributeId} id
|
||||||
|
* @param {string} name
|
||||||
|
* @param {number} location
|
||||||
|
*/
|
||||||
|
constructor(id, name, location) {
|
||||||
|
this.#id = id
|
||||||
|
this.#name = name
|
||||||
|
this.#shaderLocation = location
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() { return this.#id }
|
||||||
|
get name() { return this.#name }
|
||||||
|
get shaderLocation() { return this.#shaderLocation }
|
||||||
|
}
|
||||||
|
|
||||||
|
class VertexAttributeError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {MeshVertexAttributeId} id
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(id, name, message) {
|
||||||
|
super(message)
|
||||||
|
this.id = id
|
||||||
|
this.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MeshVertexAttributeId} id
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
static missing(id, name) {
|
||||||
|
return new VertexAttributeError(id, name, `Missing vertex attribute ${name} (id: ${id})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VertexBufferLayout {
|
||||||
|
#stride
|
||||||
|
#stepMode
|
||||||
|
#attributes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BufferAddress} stride
|
||||||
|
* @param {GPUVertexStepMode} stepMode
|
||||||
|
* @param {VertexAttribute[]} attributes
|
||||||
|
*/
|
||||||
|
constructor(stride, stepMode, attributes) {
|
||||||
|
this.#stride = stride
|
||||||
|
this.#stepMode = stepMode
|
||||||
|
this.#attributes = attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
get stride() { return this.#stride }
|
||||||
|
get stepMode() { return this.#stepMode }
|
||||||
|
get attributes() { return this.#attributes }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUVertexStepMode} mode
|
||||||
|
* @param {VertexFormat[]} formats
|
||||||
|
*/
|
||||||
|
static fromVertexFormats(mode, formats) {
|
||||||
|
let offset = 0
|
||||||
|
const attributes = []
|
||||||
|
|
||||||
|
const len = formats.length
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const format = formats[i]
|
||||||
|
|
||||||
|
attributes.push(new VertexAttribute(
|
||||||
|
format,
|
||||||
|
offset,
|
||||||
|
i
|
||||||
|
))
|
||||||
|
|
||||||
|
offset += format.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VertexBufferLayout(
|
||||||
|
offset,
|
||||||
|
mode,
|
||||||
|
attributes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MeshVertexBufferLayout {
|
||||||
|
#ids
|
||||||
|
#layout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Set<MeshVertexAttributeId>} ids
|
||||||
|
* @param {VertexBufferLayout} layout
|
||||||
|
*/
|
||||||
|
constructor(ids, layout) {
|
||||||
|
this.#ids = ids
|
||||||
|
this.#layout = layout
|
||||||
|
}
|
||||||
|
|
||||||
|
get attributeIds() { return this.#ids }
|
||||||
|
get layout() { return this.#layout }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MeshVertexAttributeId} id
|
||||||
|
*/
|
||||||
|
has(id) {
|
||||||
|
return this.#ids.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {VertexAttributeDescriptor[]} descriptors
|
||||||
|
*/
|
||||||
|
getLayout(descriptors) {
|
||||||
|
let attributes = []
|
||||||
|
|
||||||
|
const len = descriptors.length
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const desc = descriptors[i]
|
||||||
|
const index = descriptors.indexOf(desc)
|
||||||
|
|
||||||
|
if (index <= -1) {
|
||||||
|
throw VertexAttributeError.missing(desc.id, desc.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attr = this.#layout.attributes[index]
|
||||||
|
attributes.push(new VertexAttribute(
|
||||||
|
attr.format,
|
||||||
|
attr.offset,
|
||||||
|
desc.shaderLocation
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VertexBufferLayout(
|
||||||
|
this.#layout.stride,
|
||||||
|
this.#layout.stepMode,
|
||||||
|
attributes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: revisit this to either optimize the sparse array,
|
||||||
|
// model it more like Bevy's tagged union, or refactor
|
||||||
|
// it entirely to better suit JS
|
||||||
|
export class MeshAttributeData {
|
||||||
|
/** @type {MeshVertexAttribute} */
|
||||||
|
#attribute
|
||||||
|
/** @type {number[]} */
|
||||||
|
#values
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MeshVertexAttribute} attribute
|
||||||
|
* @param {number[]} values
|
||||||
|
*/
|
||||||
|
constructor(attribute, values) {
|
||||||
|
this.#attribute = attribute
|
||||||
|
this.#values = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
9
src/plugins/renderer/systems/index.js
Normal file
9
src/plugins/renderer/systems/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* @param {World} world
|
||||||
|
*/
|
||||||
|
export const render = world => {
|
||||||
|
/** @param {World} world */
|
||||||
|
return world => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
36
src/plugins/renderer/webgl-context.js
Normal file
36
src/plugins/renderer/webgl-context.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
export class WebGLContext {
|
||||||
|
/** @type {WebGL2RenderingContext} */
|
||||||
|
#context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
*/
|
||||||
|
constructor(canvas) {
|
||||||
|
this.#context = canvas.getContext('webgl2')
|
||||||
|
|
||||||
|
if (!this.#context) {
|
||||||
|
throw new Error('WebGL2 is unsupported')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get context() {
|
||||||
|
return this.#context
|
||||||
|
}
|
||||||
|
|
||||||
|
get clearColor() {
|
||||||
|
return this.#context.getParameter(this.#context.COLOR_CLEAR_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[number, number, number, number]} value
|
||||||
|
*/
|
||||||
|
set clearColor(value) {
|
||||||
|
this.#context.clearColor(...value)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
427
src/plugins/renderer/webgpu-context.js
Normal file
427
src/plugins/renderer/webgpu-context.js
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
export class WebGPUError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsupported() {
|
||||||
|
return new WebGPUError("WebGPU is unsupported in this browser")
|
||||||
|
}
|
||||||
|
|
||||||
|
static adapterUnavailable() {
|
||||||
|
return new WebGPUError("Could not request a WebGPU adapter.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGPUBuilderError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
static missing(property) {
|
||||||
|
return new WebGPUBuilderError(`Missing required property: ${property}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GPUCanvasConfigurationBuilder {
|
||||||
|
/** @type {WebGPUContextBuilder} */
|
||||||
|
#contextBuilder
|
||||||
|
/** @type {GPUTextureUsageFlags} */
|
||||||
|
#usage = 0x10
|
||||||
|
/** @type {GPUTextureFormat[]} */
|
||||||
|
#viewFormats = []
|
||||||
|
/** @type {PredefinedColorSpace} */
|
||||||
|
#colorSpace = 'srgb'
|
||||||
|
/** @type {GPUCanvasToneMapping} */
|
||||||
|
#toneMapping = {}
|
||||||
|
/** @type {GPUCanvasAlphaMode} */
|
||||||
|
#alphaMode = 'opaque'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WebGPUContextBuilder} builder
|
||||||
|
*/
|
||||||
|
constructor(builder) {
|
||||||
|
this.#contextBuilder = builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureUsageFlags} usage
|
||||||
|
*/
|
||||||
|
usage(usage) {
|
||||||
|
this.#usage = usage
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureUsageFlags} usage
|
||||||
|
*/
|
||||||
|
addUsage(usage) {
|
||||||
|
this.#usage = this.#usage | usage
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureFormat[]} formats
|
||||||
|
*/
|
||||||
|
viewFormats(formats) {
|
||||||
|
this.#viewFormats = formats
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureFormat} format
|
||||||
|
*/
|
||||||
|
addViewFormat(format) {
|
||||||
|
this.#viewFormats.push(format)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PredefinedColorSpace} space
|
||||||
|
*/
|
||||||
|
colorSpace(space) {
|
||||||
|
this.#colorSpace = space
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUCanvasToneMappingMode} mode
|
||||||
|
*/
|
||||||
|
toneMappingMode(mode) {
|
||||||
|
this.#toneMapping = { mode }
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUCanvasAlphaMode} mode
|
||||||
|
*/
|
||||||
|
alphaMode(mode) {
|
||||||
|
this.#alphaMode = mode
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {GPUCanvasBuilderConfiguration}
|
||||||
|
*/
|
||||||
|
build() {
|
||||||
|
return {
|
||||||
|
usage: this.#usage,
|
||||||
|
viewFormats: this.#viewFormats,
|
||||||
|
colorSpace: this.#colorSpace,
|
||||||
|
toneMapping: this.#toneMapping,
|
||||||
|
alphaMode: this.#alphaMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {WebGPUContextBuilder}
|
||||||
|
*/
|
||||||
|
apply() {
|
||||||
|
return this.#contextBuilder.configureCanvas(this.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {'core' | 'compatibility'} GPUFeatureLevel */
|
||||||
|
|
||||||
|
class GPUAdapterOptionsBuilder {
|
||||||
|
#contextBuilder
|
||||||
|
/** @type {GPUFeatureLevel} */
|
||||||
|
#featureLevel = 'core'
|
||||||
|
/** @type {GPUPowerPreference} */
|
||||||
|
#powerPreference
|
||||||
|
/** @type {boolean} */
|
||||||
|
#forceFallbackAdapter = false
|
||||||
|
/** @type {boolean} */
|
||||||
|
#xrCompatible = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WebGPUContextBuilder} builder
|
||||||
|
*/
|
||||||
|
constructor(builder) {
|
||||||
|
this.#contextBuilder = builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUFeatureLevel} featureLevel
|
||||||
|
*/
|
||||||
|
featureLevel(featureLevel) {
|
||||||
|
this.#featureLevel = featureLevel
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUPowerPreference?} preference
|
||||||
|
*/
|
||||||
|
powerPreference(preference) {
|
||||||
|
this.#powerPreference = preference
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} force
|
||||||
|
*/
|
||||||
|
forceFallbackAdapter(force) {
|
||||||
|
this.#forceFallbackAdapter = force
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} compatible
|
||||||
|
*/
|
||||||
|
xrCompatible(compatible) {
|
||||||
|
this.#xrCompatible = compatible
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {GPURequestAdapterOptions}
|
||||||
|
*/
|
||||||
|
build() {
|
||||||
|
return {
|
||||||
|
featureLevel: this.#featureLevel,
|
||||||
|
powerPreference: this.#powerPreference,
|
||||||
|
forceFallbackAdapter: this.#forceFallbackAdapter,
|
||||||
|
xrCompatible: this.#xrCompatible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
return this.#contextBuilder.adapter(this.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GPUDeviceDescriptorBuilder {
|
||||||
|
/** @type {WebGPUContextBuilder} */
|
||||||
|
#contextBuilder
|
||||||
|
|
||||||
|
/** @type {GPUFeatureName[]} */
|
||||||
|
#features = []
|
||||||
|
|
||||||
|
/** @type {Record<string, GPUSize64?>} */
|
||||||
|
#limits = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WebGPUContextBuilder} builder
|
||||||
|
*/
|
||||||
|
constructor(builder) {
|
||||||
|
this.#contextBuilder = builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUFeatureName[]} features
|
||||||
|
*/
|
||||||
|
requiredFeatures(features) {
|
||||||
|
this.#features = features
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUFeatureName} feature
|
||||||
|
*/
|
||||||
|
addRequiredFeature(feature) {
|
||||||
|
this.#features.push(feature)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, GPUSize64?>} limits
|
||||||
|
*/
|
||||||
|
requiredLimits(limits) {
|
||||||
|
this.#limits = limits
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {GPUSize64?} value
|
||||||
|
*/
|
||||||
|
setRequiredLimit(key, value) {
|
||||||
|
this.#limits[key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {GPUDeviceDescriptor}
|
||||||
|
*/
|
||||||
|
build() {
|
||||||
|
return {
|
||||||
|
requiredFeatures: this.#features,
|
||||||
|
requiredLimits: this.#limits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
return this.#contextBuilder.device(this.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} GPUCanvasBuilderConfiguration
|
||||||
|
* @property {GPUTextureUsageFlags} [usage=0x10]
|
||||||
|
* @property {GPUTextureFormat[]} [viewFormats=[]]
|
||||||
|
* @property {PredefinedColorSpace} [colorSpace='srgb']
|
||||||
|
* @property {GPUCanvasToneMapping} [toneMapping = {}]
|
||||||
|
* @property {GPUCanvasAlphaMode} [alphaMode='opaque']
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class WebGPUContextBuilder {
|
||||||
|
/** @type {Promise<GPUAdapter>} */
|
||||||
|
#adapter
|
||||||
|
|
||||||
|
/** @type {GPUCanvasBuilderConfiguration} */
|
||||||
|
#canvasConfig
|
||||||
|
|
||||||
|
/** @type {GPUCanvasContext} */
|
||||||
|
#context
|
||||||
|
|
||||||
|
/** @type {Promise<GPUDevice>} */
|
||||||
|
#device
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
#dpr = window.devicePixelRatio || 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
#warnDefault(type) {
|
||||||
|
console.warn(`WARN: Requesting WebGPU ${type} with default options.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPURequestAdapterOptions} [descriptor]
|
||||||
|
* @returns {WebGPUContextBuilder}
|
||||||
|
* @throws {WebGPUError} Throws if WebGPU is not supported
|
||||||
|
*/
|
||||||
|
adapter(descriptor) {
|
||||||
|
if (!navigator.gpu) {
|
||||||
|
throw WebGPUError.unsupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#adapter = navigator.gpu.requestAdapter(descriptor)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAdapterConfiguration() {
|
||||||
|
return new GPUAdapterOptionsBuilder(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @returns {WebGPUContextBuilder}
|
||||||
|
*/
|
||||||
|
context(canvas) {
|
||||||
|
this.#context = canvas.getContext('webgpu')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCanvasConfiguration() {
|
||||||
|
return new GPUCanvasConfigurationBuilder(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDeviceDescriptor} [descriptor]
|
||||||
|
* @returns {WebGPUContextBuilder}
|
||||||
|
* @throws {WebGPUError} Throws if WebGPU is not supported
|
||||||
|
*/
|
||||||
|
device(descriptor) {
|
||||||
|
if (!this.#adapter) {
|
||||||
|
this.#warnDefault('adapter')
|
||||||
|
this.adapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#device = this.#adapter.then(adapter => adapter.requestDevice(descriptor))
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDeviceDescriptor() {
|
||||||
|
return new GPUDeviceDescriptorBuilder(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUCanvasBuilderConfiguration} [configuration]
|
||||||
|
* @returns {WebGPUContextBuilder}
|
||||||
|
*/
|
||||||
|
configureCanvas(configuration) {
|
||||||
|
this.#canvasConfig = configuration
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<WebGPUContext>}
|
||||||
|
* @throws {WebGPUBuilderError} Throws if context is undefined or if WebGPU is unsupported
|
||||||
|
*/
|
||||||
|
async build() {
|
||||||
|
if (!this.#context) {
|
||||||
|
throw WebGPUBuilderError.missing('context')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#device) {
|
||||||
|
this.#warnDefault('device')
|
||||||
|
this.device()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [adapter, device] = await Promise.all([this.#adapter, this.#device])
|
||||||
|
|
||||||
|
const format = navigator.gpu.getPreferredCanvasFormat()
|
||||||
|
|
||||||
|
if (this.#canvasConfig) {
|
||||||
|
this.#context.configure({
|
||||||
|
device,
|
||||||
|
format,
|
||||||
|
...this.#canvasConfig
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WebGPUContext(
|
||||||
|
this.#context,
|
||||||
|
adapter,
|
||||||
|
device,
|
||||||
|
format,
|
||||||
|
this.#dpr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGPUContext {
|
||||||
|
#adapter
|
||||||
|
#context
|
||||||
|
#device
|
||||||
|
#format
|
||||||
|
#dpr
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUCanvasContext} context
|
||||||
|
* @param {GPUAdapter} adapter
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUTextureFormat} format
|
||||||
|
* @param {number} [dpr]
|
||||||
|
*/
|
||||||
|
constructor(context, adapter, device, format, dpr) {
|
||||||
|
this.#context = context
|
||||||
|
this.#adapter = adapter
|
||||||
|
this.#device = device
|
||||||
|
this.#format = format
|
||||||
|
this.#dpr = dpr || window.devicePixelRatio || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get adapter() { return this.#adapter }
|
||||||
|
get context() { return this.#context }
|
||||||
|
get device() { return this.#device }
|
||||||
|
get queue() { return this.#device.queue }
|
||||||
|
get format() { return this.#format }
|
||||||
|
get devicePixelRatio() { return this.#dpr }
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new WebGPUContextBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
src/plugins/transform/components/index.js
Normal file
15
src/plugins/transform/components/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Matrix4x4 } from '../../../components/math.js'
|
||||||
|
import { createRelation } from '/src/ecs.js'
|
||||||
|
|
||||||
|
// Local transform, relative to parent
|
||||||
|
export const Transform = {
|
||||||
|
matrix: Matrix4x4()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalTransform = {
|
||||||
|
matrix: Matrix4x4()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChildOf = createRelation({ autoRemoveSubject: true, exclusive: true })
|
||||||
|
|
8
src/plugins/transform/index.js
Normal file
8
src/plugins/transform/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Dirty, Transform } from './components/index.js'
|
||||||
|
import { updateTransform } from './systems/index.js'
|
||||||
|
import { addComponent, observe, onSet } from '/src/ecs.js'
|
||||||
|
|
||||||
|
export const TransformPlugin = engine => {
|
||||||
|
console.log('hi from transform plugin!')
|
||||||
|
}
|
||||||
|
|
13
src/plugins/transform/systems/index.js
Normal file
13
src/plugins/transform/systems/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ChildOf, GlobalTransform, Transform } from '../components/index.js'
|
||||||
|
import { getRelationTargets, Not, query, Wildcard } from '/src/ecs.js'
|
||||||
|
import * as twgl from '/public/vendor/twgl/twgl-full.module.js'
|
||||||
|
import { World } from '/src/core/engine.js'
|
||||||
|
|
||||||
|
/** @param {World} _world */
|
||||||
|
export const updateTransform = _world => {
|
||||||
|
/** @param {World} world */
|
||||||
|
return world => {
|
||||||
|
// TODO: implement transform propagation with GlobalTransform and Transform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
src/plugins/window/index.js
Normal file
10
src/plugins/window/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Engine, Startup } from '/src/core/engine.js'
|
||||||
|
import { resize } from './systems/index.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Engine} engine
|
||||||
|
*/
|
||||||
|
export const WindowPlugin = engine => {
|
||||||
|
engine.addSystem(Startup, resize)
|
||||||
|
}
|
||||||
|
|
99
src/plugins/window/resize.js
Normal file
99
src/plugins/window/resize.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
const ResizeObserverBox = Object.freeze({
|
||||||
|
ContentBox: 'content-box',
|
||||||
|
BorderBox: 'border-box',
|
||||||
|
DevicePixelContentBox: 'device-pixel-content-box',
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ResizeHandler {
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
#canvas
|
||||||
|
|
||||||
|
/** @type {ResizeObserver} */
|
||||||
|
#observer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {(width: number, height: number) => void} Subscriber
|
||||||
|
* @type {Set<Subscriber>}
|
||||||
|
*/
|
||||||
|
#subscribers = new Set()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @param {number} dpr
|
||||||
|
* @param {number} limit
|
||||||
|
*/
|
||||||
|
constructor(canvas, dpr, limit) {
|
||||||
|
this.#canvas = canvas
|
||||||
|
|
||||||
|
this.#observer = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const canvas = entry.target
|
||||||
|
|
||||||
|
const size = this._getSize(entry)[0]
|
||||||
|
let width = size.inlineSize * dpr
|
||||||
|
let height = size.blockSize * dpr
|
||||||
|
width = this._clamp(width, 1, limit)
|
||||||
|
height = this._clamp(height, 1, limit)
|
||||||
|
|
||||||
|
if (width !== canvas.width || height !== canvas.height) {
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
this.#notify(width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Subscriber} fn
|
||||||
|
*/
|
||||||
|
subscribe(fn) {
|
||||||
|
this.#subscribers.add(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} width
|
||||||
|
* @param {number} height
|
||||||
|
*/
|
||||||
|
#notify(width, height) {
|
||||||
|
this.#subscribers
|
||||||
|
.forEach(fn => fn(width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Subscriber} fn
|
||||||
|
*/
|
||||||
|
unsubscribe(fn) {
|
||||||
|
this.#subscribers.delete(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.#observer.observe(
|
||||||
|
this.#canvas,
|
||||||
|
{ box: ResizeObserverBox.DevicePixelContentBox }
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.#observer.unobserve(this.#canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ResizeObserverEntry} entry
|
||||||
|
*/
|
||||||
|
_getSize(entry) {
|
||||||
|
return entry.devicePixelContentBoxSize || entry.contentBoxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} size
|
||||||
|
* @param {number} min
|
||||||
|
* @param {number} max
|
||||||
|
*/
|
||||||
|
_clamp(size, min, max) {
|
||||||
|
return Math.max(min, Math.min(size, max))
|
||||||
|
}
|
||||||
|
}
|
21
src/plugins/window/systems/index.js
Normal file
21
src/plugins/window/systems/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { WebGPUContext } from '../../renderer/webgpu-context.js'
|
||||||
|
import { ResizeHandler } from '../resize.js'
|
||||||
|
import { World } from '/src/core/engine.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {World} world
|
||||||
|
*/
|
||||||
|
export const resize = world => {
|
||||||
|
const webgpu = world.getResource(WebGPUContext)
|
||||||
|
const resizer = new ResizeHandler(
|
||||||
|
// @ts-ignore
|
||||||
|
webgpu.context.canvas,
|
||||||
|
webgpu.devicePixelRatio,
|
||||||
|
webgpu.device.limits.maxTextureDimension2D
|
||||||
|
)
|
||||||
|
|
||||||
|
resizer.start()
|
||||||
|
|
||||||
|
world.addResource(resizer)
|
||||||
|
}
|
||||||
|
|
9
src/utils/error.js
Normal file
9
src/utils/error.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export class NotImplementedError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
28
src/utils/index.js
Normal file
28
src/utils/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
export const capitalizen = (n, str) => {
|
||||||
|
str.slice(0, n).toUpperCase() + str.slice(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} a
|
||||||
|
* @param {Object} b
|
||||||
|
*/
|
||||||
|
export const assignOptions = (a, b) => {
|
||||||
|
const props = Object.entries(b)
|
||||||
|
.map(([key, value]) => [`#${key}`, value])
|
||||||
|
|
||||||
|
Object.assign(a, Object.fromEntries(props))
|
||||||
|
}
|
||||||
|
export const trampoline = (f, ...args) => {
|
||||||
|
let x = f(...args)
|
||||||
|
|
||||||
|
while (typeof x === 'function') {
|
||||||
|
x = x()
|
||||||
|
}
|
||||||
|
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isObject = value => {
|
||||||
|
return Object.getPrototypeOf(value) === Object.prototype
|
||||||
|
}
|
||||||
|
|
105
src/utils/ordered-map.js
Normal file
105
src/utils/ordered-map.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/** @typedef {-1 | 0 | 1} Ternary */
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {(a: T, b: T) => Ternary | NaN} Comparer
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template X, Y
|
||||||
|
* @param {X | Y} a
|
||||||
|
* @param {Y} b
|
||||||
|
* @returns [X | undefined, Y | undefined]
|
||||||
|
*/
|
||||||
|
const optionalSecond = (a, b) => {
|
||||||
|
let x = a
|
||||||
|
let y = b
|
||||||
|
|
||||||
|
if (!b) {
|
||||||
|
x = undefined
|
||||||
|
y = b
|
||||||
|
}
|
||||||
|
|
||||||
|
return [x, y]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template K, V
|
||||||
|
*/
|
||||||
|
export class OrderedMap extends Map {
|
||||||
|
_sort
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overload
|
||||||
|
* @param {Iterable.<[K, V]>} iterable
|
||||||
|
* @param {Comparer<[K, V]>} comparer
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @overload
|
||||||
|
* @param {Comparer<[K, V]>} comparer
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {Iterable.<[K, V]> | Comparer<[K, V]>} iterable
|
||||||
|
* @param {Comparer<[K, V]>} [comparer]
|
||||||
|
*/
|
||||||
|
constructor(iterable, comparer) {
|
||||||
|
const [iter, sort] = optionalSecond(iterable, comparer)
|
||||||
|
|
||||||
|
/** @ts-ignore */
|
||||||
|
super(iter)
|
||||||
|
this._sort = sort
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Comparer<[K, V]>} comparer
|
||||||
|
*/
|
||||||
|
sortWith(comparer) {
|
||||||
|
this._sort = comparer
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
/** @ts-ignore */
|
||||||
|
yield* [...this.entries()].sort(this._sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {(value: T) => number} get
|
||||||
|
* @returns {Comparer<T>}
|
||||||
|
*/
|
||||||
|
const prioritySort = get => (a, b) => get(a) - get(b)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template K, V
|
||||||
|
*/
|
||||||
|
export class PriorityMap extends OrderedMap {
|
||||||
|
/**
|
||||||
|
* @typedef {(value: [K, V]) => number} PriorityGetter
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type PriorityGetter */
|
||||||
|
_getter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overload
|
||||||
|
* @param {Iterable.<[K, V]>} iterable
|
||||||
|
* @param {PriorityGetter} getter
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @overload
|
||||||
|
* @param {PriorityGetter} getter
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {Iterable.<[K, V]> | PriorityGetter} iterable
|
||||||
|
* @param {PriorityGetter} [getter]
|
||||||
|
*/
|
||||||
|
constructor(iterable, getter) {
|
||||||
|
const [iter, get] = optionalSecond(iterable, getter)
|
||||||
|
|
||||||
|
/** @ts-ignore */
|
||||||
|
super(iter, prioritySort(get))
|
||||||
|
|
||||||
|
/** @ts-ignore */
|
||||||
|
this._getter = get
|
||||||
|
}
|
||||||
|
}
|
9
src/utils/webgpu.js
Normal file
9
src/utils/webgpu.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export class Attribute {
|
||||||
|
#name
|
||||||
|
#format
|
||||||
|
#location
|
||||||
|
#buffer
|
||||||
|
#count
|
||||||
|
#components
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue