added a shitty ecs that I'm confident is way worse than just looping … (#4)

* added a shitty ecs that I'm confident is way worse than just looping objects lol

* mader a rendering system based on ecs
This commit is contained in:
Heaust Azure
2025-05-06 18:53:21 +05:30
committed by GitHub
parent 5ecaf14b3d
commit 9d2bf4055d
17 changed files with 549 additions and 233 deletions
+5 -10
View File
@@ -58,11 +58,11 @@ const connectWorker = (worker: Worker) => {
document.addEventListener(
"pointermove",
eventPassers[EventTypes.pointermove]
eventPassers[EventTypes.pointermove],
);
document.addEventListener(
"pointerdown",
eventPassers[EventTypes.pointerdown]
eventPassers[EventTypes.pointerdown],
);
document.addEventListener("pointerup", eventPassers[EventTypes.pointerup]);
//
@@ -72,15 +72,15 @@ const connectWorker = (worker: Worker) => {
const disconnectWorker = () => {
document.removeEventListener(
"pointermove",
eventPassers[EventTypes.pointermove]
eventPassers[EventTypes.pointermove],
);
document.removeEventListener(
"pointerdown",
eventPassers[EventTypes.pointerdown]
eventPassers[EventTypes.pointerdown],
);
document.removeEventListener(
"pointerup",
eventPassers[EventTypes.pointerup]
eventPassers[EventTypes.pointerup],
);
//
document.removeEventListener("keydown", eventPassers[EventTypes.keydown]);
@@ -97,11 +97,6 @@ const attachCanvas = (worker: Worker, canvas: HTMLCanvasElement | string) => {
throw new Error("Fatal: Canvas Not Found!");
}
// set canvas dimensions
// if this is not done, the canvas passed to the web worker
// will have the default size of 300x150
// this will cause the viewport to be set to the default size
// and eveything will be messed up
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
+20 -13
View File
@@ -1,7 +1,14 @@
import { Attribute, draw, ProgramData } from "../gl";
import { cubeVertices } from "../gl/vertices";
import { Engine } from "../renderer/engine";
import { MessageType } from "./mainThread";
const cubeVertices = [
1, -1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1,
-1, -1, -1, -1, 1, 1, -1, -1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, -1, 1, -1,
1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1,
-1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1,
1, -1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1,
];
const handleConnection = (msg: MessageEvent<any>) => {
const { data } = msg;
@@ -12,19 +19,19 @@ const handleConnection = (msg: MessageEvent<any>) => {
case MessageType.attachCanvas:
const canvas = data[1];
const ctxWorker = canvas.getContext("webgl2");
var p: ProgramData<Attribute<Float32Array>> = {
vertexShaderSource: "/shaders/triangle/vertex.glsl",
fragmentShaderSource: "/shaders/triangle/frag.glsl",
attributes: [
{
name: "position",
data: new Float32Array(cubeVertices),
},
],
const engine = new Engine(canvas);
const scene = engine.createScene();
scene.addMesh("box", cubeVertices);
scene.addCamera("cam", true);
const raf = () => {
requestAnimationFrame(() => {
scene.runSystems();
raf();
});
};
requestAnimationFrame(() => draw(ctxWorker, p));
raf();
break;
}
+110
View File
@@ -0,0 +1,110 @@
type ComponentMap<CT> = {
uid: (number | null)[];
} & {
[K in keyof CT]?: Array<CT[K] | null>;
};
namespace ECS {
export type System<ComponentsType extends Record<string | number, any>> = (
arg: ComponentsType & { uid: (number | null)[] },
) => Partial<ComponentsType> | null;
}
class ECS<ComponentsType extends Record<string | number, any>> {
private components: ComponentMap<ComponentsType> = { uid: [] };
private componentDefaults: Record<
string,
null | ComponentsType[keyof ComponentsType]
> = { uid: null };
private systems: Record<string, ECS.System<ComponentsType>> = {};
/**
* System
*/
runSystems() {
const limit = this.components.uid.length;
for (let i = 0; i < limit; i++) {
const obj = {} as Record<string, unknown>;
for (const key in this.components) {
obj[key] = this.components[key]![i];
}
const systems = Object.values(this.systems);
for (let j = 0; j < systems.length; j++) {
const res = systems[j](
obj as ComponentsType & { uid: (number | null)[] },
);
if (!res) continue;
for (const key in res) {
if (!this.components[key]) continue;
this.components[key][i] = res[key]!;
}
}
}
}
addSystem(system: ECS.System<ComponentsType>) {
const id = crypto.randomUUID();
this.systems[id] = system;
return id;
}
removeSystem(id: string) {
delete this.systems[id];
}
getById(uid: number) {
if (this.components.uid.length <= uid) return {};
const obj = {} as Record<string, unknown>;
for (const key in this.components) {
obj[key] = this.components[key]![uid];
}
return obj;
}
/**
* Entity
*/
addEntity(components: ComponentsType) {
for (const key in this.components) {
if (key === "uid") {
this.components[key].push(this.components[key].length);
continue;
}
this.components[key]!.push(
components[key] ?? this.componentDefaults[key],
);
}
return this.components.uid.length - 1;
}
/**
* Component
*/
addComponent(
name: keyof ComponentsType,
fillValue = null as null | ComponentsType[keyof ComponentsType],
defaultValue = null as null | ComponentsType[keyof ComponentsType],
) {
if (this.components[name]) throw new Error("Component already exists");
this.components[name] = new Array(this.components.uid.length).fill(
fillValue,
) as ComponentMap<ComponentsType>[keyof ComponentsType];
this.componentDefaults[name as string] = defaultValue;
}
removeComponent(name: keyof ComponentsType) {
delete this.components[name];
}
}
export { ECS };
-156
View File
@@ -1,156 +0,0 @@
import { mat4 } from "gl-matrix";
import { cubeVertices } from "./vertices";
type AttributeDataTypes = Float32Array | Int32Array;
export interface Attribute<T> {
name: string;
data: T;
}
export interface Dimensions {
width: number;
height: number;
}
export interface ProgramData<T extends Attribute<AttributeDataTypes>> {
vertexShaderSource: string;
fragmentShaderSource: string;
attributes: T[];
}
async function fetchShader(path: string) {
const response = await fetch(path);
return response.text();
}
async function createShader(
gl: WebGL2RenderingContext,
type: number,
sourceFile: string
) {
const source = await fetchShader(sourceFile);
var shader = gl.createShader(type);
if (!shader) return;
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
console.error(gl.getShaderInfoLog(shader)); // eslint-disable-line
gl.deleteShader(shader);
}
return shader;
}
async function setupProgram<T extends Attribute<AttributeDataTypes>>(
gl: WebGL2RenderingContext,
programData: ProgramData<T>
) {
var program = gl.createProgram();
if (!program) return;
var vertexShader = await createShader(
gl,
gl.VERTEX_SHADER,
programData.vertexShaderSource
);
if (!vertexShader) return;
var fragmentShader = await createShader(
gl,
gl.FRAGMENT_SHADER,
programData.fragmentShaderSource
);
if (!fragmentShader) return;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
return program;
}
function setupAttribute<T extends Attribute<AttributeDataTypes>>(
gl: WebGL2RenderingContext,
program: WebGLProgram,
attribute: T
) {
const location = gl.getAttribLocation(program, attribute.name);
if (location === -1) return;
// setup a buffer for the attribute data
const buffer = gl.createBuffer();
if (!buffer) return;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, attribute.data, gl.STATIC_DRAW);
const vao = gl.createVertexArray();
if (!vao) return;
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 5 * 4, 0);
}
export async function draw<T extends Attribute<AttributeDataTypes>>(
gl: WebGL2RenderingContext,
programData: ProgramData<T>
) {
gl.enable(gl.DEPTH_TEST);
const program = await setupProgram(gl, programData);
if (!program) {
console.error("failed to setup program");
return;
}
gl.useProgram(program);
for (const attribute of programData.attributes) {
setupAttribute(gl, program, attribute);
}
let view = mat4.create();
view = mat4.translate(view, view, [0, 0, -5]);
let model = mat4.create();
const angle = Date.now() * 0.001;
model = mat4.rotate(model, model, angle, [0.5, 1, 0]);
const projection = mat4.create();
mat4.perspective(
projection,
45,
gl.canvas.width / gl.canvas.height,
0.1,
100.0
);
const viewLoc = gl.getUniformLocation(program, "view");
const modelLoc = gl.getUniformLocation(program, "model");
const projectionLoc = gl.getUniformLocation(program, "projection");
gl.uniformMatrix4fv(viewLoc, false, view);
gl.uniformMatrix4fv(modelLoc, false, model);
gl.uniformMatrix4fv(projectionLoc, false, projection);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 36);
requestAnimationFrame(() => draw(gl, programData));
}
-22
View File
@@ -1,22 +0,0 @@
export const cubeVertices = [
-0.5, -0.5, -0.5, 0.0, 0.0, 0.5, -0.5, -0.5, 1.0, 0.0, 0.5, 0.5, -0.5, 1.0,
1.0, 0.5, 0.5, -0.5, 1.0, 1.0, -0.5, 0.5, -0.5, 0.0, 1.0, -0.5, -0.5, -0.5,
0.0, 0.0,
-0.5, -0.5, 0.5, 0.0, 0.0, 0.5, -0.5, 0.5, 1.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0, 1.0, -0.5, 0.5, 0.5, 0.0, 1.0, -0.5, -0.5, 0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 1.0, 0.0, -0.5, 0.5, -0.5, 1.0, 1.0, -0.5, -0.5, -0.5, 0.0,
1.0, -0.5, -0.5, -0.5, 0.0, 1.0, -0.5, -0.5, 0.5, 0.0, 0.0, -0.5, 0.5, 0.5,
1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 0.0, 0.5, 0.5, -0.5, 1.0, 1.0, 0.5, -0.5, -0.5, 0.0, 1.0,
0.5, -0.5, -0.5, 0.0, 1.0, 0.5, -0.5, 0.5, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 0.0,
-0.5, -0.5, -0.5, 0.0, 1.0, 0.5, -0.5, -0.5, 1.0, 1.0, 0.5, -0.5, 0.5, 1.0,
0.0, 0.5, -0.5, 0.5, 1.0, 0.0, -0.5, -0.5, 0.5, 0.0, 0.0, -0.5, -0.5, -0.5,
0.0, 1.0,
-0.5, 0.5, -0.5, 0.0, 1.0, 0.5, 0.5, -0.5, 1.0, 1.0, 0.5, 0.5, 0.5, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 0.0, -0.5, 0.5, 0.5, 0.0, 0.0, -0.5, 0.5, -0.5, 0.0, 1.0,
];
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite-plugin-glsl/ext" />
+31
View File
@@ -0,0 +1,31 @@
import { Scene } from "./scene";
class Engine {
canvas: HTMLCanvasElement;
ctx: WebGL2RenderingContext;
scenes = [] as Scene[];
constructor(canvas: string | HTMLCanvasElement) {
/** get canvas by id */
if (typeof canvas === "string") {
canvas = document.getElementById(canvas)! as HTMLCanvasElement;
}
if (!canvas) {
throw new Error("canvas not found");
}
this.canvas = canvas;
this.ctx = canvas.getContext("webgl2")!;
if (!this.ctx) {
throw new Error("can't get webgl2 context");
}
}
createScene() {
const scene = new Scene(this.canvas, this.ctx);
this.scenes.push(scene);
return scene;
}
}
export { Engine };
+107
View File
@@ -0,0 +1,107 @@
import { mat4 } from "gl-matrix";
export const compileShader = (
gl: WebGL2RenderingContext,
type: number,
source: string,
) => {
const shader = gl.createShader(type);
if (!shader) {
throw new Error(`couldn't make the shader ${source}`);
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(
`Error in ${source}: ${gl.getShaderInfoLog(shader) ?? "no error returned"}`,
);
}
return shader;
};
export const createProgram = (
gl: WebGL2RenderingContext,
vs: string,
fs: string,
): WebGLProgram => {
const program = gl.createProgram()!;
const vShader = compileShader(gl, gl.VERTEX_SHADER, vs);
const fShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || "");
}
return program;
};
export type AttributeCollection = {
name: string;
data: Float32Array;
size: number;
};
export const setupVAO = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
attributeCollections: AttributeCollection[],
) => {
const vao = gl.createVertexArray()!;
gl.bindVertexArray(vao);
for (const attr of attributeCollections) {
const loc = gl.getAttribLocation(program, attr.name);
if (loc >= 0) {
const buf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, attr.data, gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.size, gl.FLOAT, false, 0, 0);
}
}
gl.bindVertexArray(null);
return vao;
};
export const updateVAO = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
vao: WebGLVertexArrayObject,
attributeCollections: AttributeCollection[],
) => {
gl.bindVertexArray(vao);
for (const attr of attributeCollections) {
const loc = gl.getAttribLocation(program, attr.name);
if (loc >= 0) {
const buf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, attr.data, gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.size, gl.FLOAT, false, 0, 0);
}
}
gl.bindVertexArray(null);
return vao;
};
export const setMat4 = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
name: string,
mat: mat4,
) => {
const loc = gl.getUniformLocation(program, name);
if (loc) gl.uniformMatrix4fv(loc, false, mat);
};
+197
View File
@@ -0,0 +1,197 @@
import { mat4, vec3 } from "gl-matrix";
import { ECS } from "../ecs/ecs";
import { createProgram, setMat4, setupVAO, updateVAO } from "./gl";
import defaultVertexSrc from "../shaders/triangle/vertex.glsl";
import defaultFragSrc from "../shaders/triangle/frag.glsl";
class Scene {
canvas: HTMLCanvasElement;
gl: WebGL2RenderingContext;
ecs: ECS<Record<string | number, any>>;
activeCamera: number;
constructor(canvas: HTMLCanvasElement, ctx: WebGL2RenderingContext) {
this.canvas = canvas;
this.gl = ctx;
this.activeCamera = -1;
/** make ecs */
this.ecs = new ECS();
this.initECS();
}
private initECS() {
const projection = mat4.create();
mat4.perspective(
projection,
45,
this.canvas.width / this.canvas.height,
0.1,
100.0,
);
this.ecs.addComponent("isMesh");
this.ecs.addComponent("isCamera");
this.ecs.addComponent("isMaterial");
this.ecs.addComponent("program");
this.ecs.addComponent("vao");
this.ecs.addComponent("name", "deafult", "default");
this.ecs.addComponent("attr_pos", [], []);
this.ecs.addComponent("attr_normals", [], []);
this.ecs.addComponent("u_model_matrix", mat4.create(), mat4.create());
this.ecs.addComponent("u_projection_matrix", projection, projection);
const defaultMaterial = this.addMaterial(
"default-material",
defaultVertexSrc,
defaultFragSrc,
);
this.ecs.addComponent("applied_material", defaultMaterial, defaultMaterial);
this.ecs.addSystem((data) => {
this.render(data);
const model = mat4.create();
const angle = Date.now() * 0.001;
mat4.translate(model, model, vec3.fromValues(0, 0, -5));
mat4.rotate(model, model, angle, [0.5, 1, 0]);
if (data["isMesh"]) {
return {
u_model_matrix: model,
};
}
return null;
});
}
runSystems() {
this.ecs.runSystems();
}
private render(data: any) {
if (this.activeCamera === -1) return;
const camera = this.ecs.getById(this.activeCamera);
const view = mat4.create();
mat4.invert(view, camera["u_model_matrix"] as mat4);
const projection = camera["u_projection_matrix"] as mat4;
const gl = this.gl;
gl.enable(gl.DEPTH_TEST);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (data["isMesh"]) {
const { program, vao } = this.ecs.getById(data["applied_material"]);
const attrCollection = [
{
name: "attr_pos",
data: data["attr_pos"],
size: 3,
},
{
name: "attr_normals",
data: data["attr_normals"],
size: 3,
},
];
updateVAO(gl, program as any, vao as any, attrCollection);
gl.useProgram(program!);
const model = data["u_model_matrix"];
setMat4(gl, program as any, "model", model);
setMat4(gl, program as any, "view", view);
setMat4(gl, program as any, "projection", projection);
gl.bindVertexArray(vao as any);
const vertexCount = data["attr_pos"].length / 3;
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
console.log(vertexCount);
}
gl.bindVertexArray(null);
gl.useProgram(null);
}
addMaterial(name: string, vs: string, fs: string) {
const program = createProgram(this.gl, vs, fs);
const vao = setupVAO(this.gl, program, []);
return this.ecs.addEntity({
name,
isMaterial: true,
program,
vao,
});
}
addCamera(name: string, setActive = true) {
const uid = this.ecs.addEntity({
name,
});
if (setActive) this.activeCamera = uid;
}
addMesh(name: string, positions: number[], normals?: number[]) {
const numVerts = Math.floor(positions.length / 3);
const numTris = Math.floor(numVerts / 3);
positions.length = numTris * 9;
if (!normals) {
normals = [];
const a = vec3.create();
const b = vec3.create();
const c = vec3.create();
const e1 = vec3.create();
const e2 = vec3.create();
for (let i = 0; i < numTris; i++) {
const base = i * 9;
vec3.set(
a,
positions[base + 0],
positions[base + 1],
positions[base + 2],
);
vec3.set(
b,
positions[base + 3],
positions[base + 4],
positions[base + 5],
);
vec3.set(
c,
positions[base + 6],
positions[base + 7],
positions[base + 8],
);
vec3.subtract(e1, b, a);
vec3.subtract(e2, c, a);
vec3.cross(e1, e2, e1);
vec3.normalize(e1, e1);
normals.push(e1[0], e1[1], e1[2]);
normals.push(e1[0], e1[1], e1[2]);
normals.push(e1[0], e1[1], e1[2]);
}
}
this.ecs.addEntity({
name,
attr_pos: new Float32Array(positions),
attr_normals: new Float32Array(normals),
isMesh: true,
});
}
}
export { Scene };
+16
View File
@@ -0,0 +1,16 @@
#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(vec3(0.0, 0.0, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuseColor = vec3(1.0, 0.5, 0.3) * diff;
fragColor = vec4(diffuseColor, 1.0);
}
+20
View File
@@ -0,0 +1,20 @@
#version 300 es
precision highp float;
in vec3 attr_pos;
in vec3 attr_normals;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 vNormal;
void main() {
mat3 normalMatrix = transpose(inverse(mat3(model)));
vNormal = normalize(normalMatrix * attr_normals);
gl_Position = projection * view * model * vec4(attr_pos, 1.0);
}