@actis/core 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +129 -0
- package/dist/index.cjs +343 -0
- package/dist/index.d.cts +117 -0
- package/dist/index.d.mts +117 -0
- package/dist/index.mjs +340 -0
- package/package.json +48 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
//#region src/pass/Pass.ts
|
|
2
|
+
/**
|
|
3
|
+
* Represents a render pass in the WebGL pipeline.
|
|
4
|
+
* Responsible for managing framebuffer, texture, and draw calls for a single pass.
|
|
5
|
+
*/
|
|
6
|
+
var Pass = class {
|
|
7
|
+
constructor(gl, shader, width, height, offscreen = true, textures = []) {
|
|
8
|
+
this.gl = gl;
|
|
9
|
+
this.shader = shader;
|
|
10
|
+
this.width = width;
|
|
11
|
+
this.height = height;
|
|
12
|
+
this.texture = this.createTexture();
|
|
13
|
+
this.framebuffer = this.createFramebuffer(this.texture);
|
|
14
|
+
this.next = null;
|
|
15
|
+
this.offscreen = offscreen;
|
|
16
|
+
this.textures = textures;
|
|
17
|
+
this.positionAttributeLocation = this.shader.getAttribLocation("a_position");
|
|
18
|
+
this.positionBuffer = this.createPositionBuffer();
|
|
19
|
+
}
|
|
20
|
+
use() {
|
|
21
|
+
this.shader.use();
|
|
22
|
+
if (this.offscreen) {
|
|
23
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
|
|
24
|
+
this.gl.viewport(0, 0, this.width, this.height);
|
|
25
|
+
} else {
|
|
26
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
27
|
+
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
resize(width, height) {
|
|
31
|
+
this.width = width;
|
|
32
|
+
this.height = height;
|
|
33
|
+
if (this.offscreen) {
|
|
34
|
+
this.texture = this.createTexture();
|
|
35
|
+
this.framebuffer = this.createFramebuffer(this.texture);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
draw() {
|
|
39
|
+
const positions = new Float32Array([
|
|
40
|
+
-1,
|
|
41
|
+
-1,
|
|
42
|
+
1,
|
|
43
|
+
-1,
|
|
44
|
+
-1,
|
|
45
|
+
1,
|
|
46
|
+
-1,
|
|
47
|
+
1,
|
|
48
|
+
1,
|
|
49
|
+
-1,
|
|
50
|
+
1,
|
|
51
|
+
1
|
|
52
|
+
]);
|
|
53
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
54
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
55
|
+
this.gl.enableVertexAttribArray(this.positionAttributeLocation);
|
|
56
|
+
this.gl.vertexAttribPointer(this.positionAttributeLocation, 2, this.gl.FLOAT, false, 0, 0);
|
|
57
|
+
this.textures.forEach((texture, index) => {
|
|
58
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + index);
|
|
59
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
60
|
+
this.shader.setUniform(`u_texture${index}`, "1i", index);
|
|
61
|
+
});
|
|
62
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, positions.length / 2);
|
|
63
|
+
}
|
|
64
|
+
createTexture() {
|
|
65
|
+
const texture = this.gl.createTexture();
|
|
66
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
67
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
68
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
69
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
70
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
71
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.width, this.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
|
|
72
|
+
return texture;
|
|
73
|
+
}
|
|
74
|
+
createFramebuffer(texture) {
|
|
75
|
+
const framebuffer = this.gl.createFramebuffer();
|
|
76
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
77
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
|
|
78
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
79
|
+
return framebuffer;
|
|
80
|
+
}
|
|
81
|
+
createPositionBuffer() {
|
|
82
|
+
const buffer = this.gl.createBuffer();
|
|
83
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
|
|
84
|
+
return buffer;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/shader/Shader.ts
|
|
89
|
+
const ERROR_LOG_REGEX = /ERROR: 0:(\d+): (.*)(?=\n|$)/;
|
|
90
|
+
/**
|
|
91
|
+
* Compiles and manages a WebGL shader program.
|
|
92
|
+
* Responsible for compiling, linking, and providing access to uniforms and attributes.
|
|
93
|
+
*/
|
|
94
|
+
var Shader = class {
|
|
95
|
+
constructor(gl, vertexSource, fragmentSource, onError, passName) {
|
|
96
|
+
this.gl = gl;
|
|
97
|
+
this.onError = onError;
|
|
98
|
+
this.passName = passName;
|
|
99
|
+
this.uniformLocations = /* @__PURE__ */ new Map();
|
|
100
|
+
const vertexShader = this.compileShader(vertexSource || this.defaultVertexShader(), gl.VERTEX_SHADER);
|
|
101
|
+
const fragmentShader = this.compileShader(fragmentSource, gl.FRAGMENT_SHADER);
|
|
102
|
+
this.program = this.linkProgram(vertexShader, fragmentShader);
|
|
103
|
+
}
|
|
104
|
+
compileShader(source, type) {
|
|
105
|
+
const shader = this.gl.createShader(type);
|
|
106
|
+
if (!shader) throw new Error("Failed to create shader");
|
|
107
|
+
this.gl.shaderSource(shader, source);
|
|
108
|
+
this.gl.compileShader(shader);
|
|
109
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
110
|
+
const log = this.gl.getShaderInfoLog(shader);
|
|
111
|
+
this.gl.deleteShader(shader);
|
|
112
|
+
const coords = this.extractErrorCoords(log || "");
|
|
113
|
+
this.onError({
|
|
114
|
+
passName: this.passName,
|
|
115
|
+
coords
|
|
116
|
+
});
|
|
117
|
+
throw new Error(`Failed to compile shader: ${log}`);
|
|
118
|
+
}
|
|
119
|
+
return shader;
|
|
120
|
+
}
|
|
121
|
+
extractErrorCoords(log) {
|
|
122
|
+
const match = ERROR_LOG_REGEX.exec(log);
|
|
123
|
+
if (match) return {
|
|
124
|
+
line: Number.parseInt(match[1], 10),
|
|
125
|
+
message: match[2]
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
line: 0,
|
|
129
|
+
message: ""
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
linkProgram(vertexShader, fragmentShader) {
|
|
133
|
+
const program = this.gl.createProgram();
|
|
134
|
+
if (!program) throw new Error("Failed to create program");
|
|
135
|
+
if (vertexShader) this.gl.attachShader(program, vertexShader);
|
|
136
|
+
this.gl.attachShader(program, fragmentShader);
|
|
137
|
+
this.gl.linkProgram(program);
|
|
138
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
139
|
+
const log = this.gl.getProgramInfoLog(program);
|
|
140
|
+
this.gl.deleteProgram(program);
|
|
141
|
+
throw new Error(`Failed to link program: ${log}`);
|
|
142
|
+
}
|
|
143
|
+
return program;
|
|
144
|
+
}
|
|
145
|
+
use() {
|
|
146
|
+
this.gl.useProgram(this.program);
|
|
147
|
+
}
|
|
148
|
+
setUniform(name, type, ...values) {
|
|
149
|
+
let location = this.uniformLocations.get(name);
|
|
150
|
+
if (!location) {
|
|
151
|
+
location = this.gl.getUniformLocation(this.program, name);
|
|
152
|
+
this.uniformLocations.set(name, location);
|
|
153
|
+
}
|
|
154
|
+
switch (type) {
|
|
155
|
+
case "1f":
|
|
156
|
+
this.gl.uniform1f(location, values[0]);
|
|
157
|
+
break;
|
|
158
|
+
case "1i":
|
|
159
|
+
this.gl.uniform1i(location, values[0]);
|
|
160
|
+
break;
|
|
161
|
+
case "2fv":
|
|
162
|
+
this.gl.uniform2fv(location, values[0]);
|
|
163
|
+
break;
|
|
164
|
+
case "3fv":
|
|
165
|
+
this.gl.uniform3fv(location, values[0]);
|
|
166
|
+
break;
|
|
167
|
+
case "4fv":
|
|
168
|
+
this.gl.uniform4fv(location, values[0]);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
getAttribLocation(name) {
|
|
173
|
+
return this.gl.getAttribLocation(this.program, name);
|
|
174
|
+
}
|
|
175
|
+
defaultVertexShader() {
|
|
176
|
+
return `
|
|
177
|
+
attribute vec4 a_position;
|
|
178
|
+
void main() {
|
|
179
|
+
gl_Position = a_position;
|
|
180
|
+
}`;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/engine/WebGLRenderer.ts
|
|
185
|
+
/**
|
|
186
|
+
* Renderer class responsible for managing the WebGL context, render passes, and animation loop.
|
|
187
|
+
*/
|
|
188
|
+
var WebGLRenderer = class {
|
|
189
|
+
constructor(canvas, onError) {
|
|
190
|
+
this.canvas = canvas;
|
|
191
|
+
this.animationRequestID = -1;
|
|
192
|
+
this.gl = this.initializeWebGLContext(canvas);
|
|
193
|
+
this.passes = null;
|
|
194
|
+
this.textureMap = /* @__PURE__ */ new Map();
|
|
195
|
+
this.now = /* @__PURE__ */ new Date();
|
|
196
|
+
this.onError = onError;
|
|
197
|
+
this.mouseX = 0;
|
|
198
|
+
this.mouseY = 0;
|
|
199
|
+
this.time = 0;
|
|
200
|
+
this.timeDelta = 0;
|
|
201
|
+
this.realToCSSPixels = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
202
|
+
this.paused = false;
|
|
203
|
+
this.playbackTime = 0;
|
|
204
|
+
this.lastTime = 0;
|
|
205
|
+
this.frameRate = 0;
|
|
206
|
+
this.currentFrame = 0;
|
|
207
|
+
this.currentTime = 0;
|
|
208
|
+
this.startTime = 0;
|
|
209
|
+
this.initMouseEvents();
|
|
210
|
+
if (typeof window !== "undefined") window.addEventListener("resize", this.resizeCanvasToDisplaySize.bind(this));
|
|
211
|
+
}
|
|
212
|
+
initializeWebGLContext(canvas) {
|
|
213
|
+
const opts = {
|
|
214
|
+
alpha: false,
|
|
215
|
+
depth: false,
|
|
216
|
+
stencil: false,
|
|
217
|
+
premultipliedAlpha: false,
|
|
218
|
+
antialias: false,
|
|
219
|
+
preserveDrawingBuffer: true,
|
|
220
|
+
powerPreference: "high-performance"
|
|
221
|
+
};
|
|
222
|
+
const gl = canvas.getContext("webgl2", opts) || canvas.getContext("experimental-webgl2", opts) || canvas.getContext("webgl", opts) || canvas.getContext("experimental-webgl", opts);
|
|
223
|
+
if (!gl) throw new Error("WebGL not supported");
|
|
224
|
+
return gl;
|
|
225
|
+
}
|
|
226
|
+
initMouseEvents() {
|
|
227
|
+
this.canvas.addEventListener("mousemove", this.setMousePosition.bind(this));
|
|
228
|
+
this.canvas.addEventListener("touchstart", this.preventDefault, { passive: false });
|
|
229
|
+
this.canvas.addEventListener("touchmove", this.handleTouchMove.bind(this), { passive: false });
|
|
230
|
+
}
|
|
231
|
+
setMousePosition(e) {
|
|
232
|
+
const mouse = {
|
|
233
|
+
x: e.clientX || e.pageX,
|
|
234
|
+
y: e.clientY || e.pageY
|
|
235
|
+
};
|
|
236
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
237
|
+
if (mouse.x >= rect.left && mouse.x <= rect.right && mouse.y >= rect.top && mouse.y <= rect.bottom) {
|
|
238
|
+
this.mouseX = (mouse.x - rect.left) * this.realToCSSPixels;
|
|
239
|
+
this.mouseY = this.canvas.height - (mouse.y - rect.top) * this.realToCSSPixels;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
handleTouchMove(e) {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
if (e.touches.length > 0) this.setMousePosition(e.touches[0]);
|
|
245
|
+
}
|
|
246
|
+
preventDefault(e) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
}
|
|
249
|
+
addPass(pass) {
|
|
250
|
+
if (this.passes) {
|
|
251
|
+
let current = this.passes;
|
|
252
|
+
while (current.next) current = current.next;
|
|
253
|
+
current.next = pass;
|
|
254
|
+
} else this.passes = pass;
|
|
255
|
+
}
|
|
256
|
+
resizeCanvasToDisplaySize() {
|
|
257
|
+
const displayWidth = Math.floor(this.canvas.clientWidth * this.realToCSSPixels);
|
|
258
|
+
const displayHeight = Math.floor(this.canvas.clientHeight * this.realToCSSPixels);
|
|
259
|
+
if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight) {
|
|
260
|
+
this.canvas.width = displayWidth;
|
|
261
|
+
this.canvas.height = displayHeight;
|
|
262
|
+
let current = this.passes;
|
|
263
|
+
while (current) {
|
|
264
|
+
current.resize(displayWidth, displayHeight);
|
|
265
|
+
current = current.next;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
updateUniforms(pass) {
|
|
270
|
+
pass.shader.setUniform("u_date", "4fv", [
|
|
271
|
+
this.now.getFullYear(),
|
|
272
|
+
this.now.getMonth() + 1,
|
|
273
|
+
this.now.getDate(),
|
|
274
|
+
this.now.getHours() * 3600 + this.now.getMinutes() * 60 + this.now.getSeconds() + this.now.getMilliseconds() / 1e3
|
|
275
|
+
]);
|
|
276
|
+
pass.shader.setUniform("u_frame", "1i", this.currentFrame);
|
|
277
|
+
pass.shader.setUniform("u_time", "1f", this.currentTime);
|
|
278
|
+
pass.shader.setUniform("u_timeDelta", "1f", this.timeDelta);
|
|
279
|
+
pass.shader.setUniform("u_frameRate", "1f", this.frameRate);
|
|
280
|
+
pass.shader.setUniform("u_resolution", "2fv", [pass.width, pass.height]);
|
|
281
|
+
pass.shader.setUniform("u_mouse", "2fv", [this.mouseX, this.mouseY]);
|
|
282
|
+
}
|
|
283
|
+
updateTime(currentTime) {
|
|
284
|
+
if (this.paused) {
|
|
285
|
+
this.timeDelta = 0;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const t = currentTime ?? (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
289
|
+
if (this.startTime === 0) this.startTime = t;
|
|
290
|
+
if (this.lastTime === 0) this.lastTime = t;
|
|
291
|
+
this.timeDelta = (t - this.startTime) / 1e3;
|
|
292
|
+
this.currentTime += this.timeDelta;
|
|
293
|
+
this.frameRate = 1 / this.timeDelta;
|
|
294
|
+
this.currentFrame++;
|
|
295
|
+
this.startTime = t;
|
|
296
|
+
}
|
|
297
|
+
render(currentTime) {
|
|
298
|
+
this.updateTime(currentTime);
|
|
299
|
+
this.resizeCanvasToDisplaySize();
|
|
300
|
+
let current = this.passes;
|
|
301
|
+
while (current) {
|
|
302
|
+
current.use();
|
|
303
|
+
this.updateUniforms(current);
|
|
304
|
+
current.draw();
|
|
305
|
+
current = current.next;
|
|
306
|
+
}
|
|
307
|
+
if (typeof requestAnimationFrame !== "undefined") this.animationRequestID = requestAnimationFrame(this.render.bind(this));
|
|
308
|
+
}
|
|
309
|
+
setup(config) {
|
|
310
|
+
const displayWidth = Math.floor(this.canvas.clientWidth * this.realToCSSPixels);
|
|
311
|
+
const displayHeight = Math.floor(this.canvas.clientHeight * this.realToCSSPixels);
|
|
312
|
+
config.passes.forEach((passConfig) => {
|
|
313
|
+
try {
|
|
314
|
+
const shader = new Shader(this.gl, passConfig.vertexShader, passConfig.fragmentShader, this.onError, passConfig.name);
|
|
315
|
+
const offscreen = passConfig.offscreen || passConfig.name !== "MainBuffer";
|
|
316
|
+
const pass = new Pass(this.gl, shader, displayWidth, displayHeight, offscreen, passConfig.textures.map((textureName) => this.textureMap.get(textureName)));
|
|
317
|
+
this.addPass(pass);
|
|
318
|
+
this.textureMap.set(passConfig.name, pass.texture);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error(`Error in pass ${passConfig.name}: ${error.message}`);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
play() {
|
|
325
|
+
this.paused = false;
|
|
326
|
+
this.animationRequestID = requestAnimationFrame(this.render.bind(this));
|
|
327
|
+
}
|
|
328
|
+
pause() {
|
|
329
|
+
this.paused = true;
|
|
330
|
+
cancelAnimationFrame(this.animationRequestID);
|
|
331
|
+
}
|
|
332
|
+
reset() {
|
|
333
|
+
this.playbackTime = 0;
|
|
334
|
+
this.currentFrame = 0;
|
|
335
|
+
this.lastTime = 0;
|
|
336
|
+
this.frameRate = 0;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
//#endregion
|
|
340
|
+
export { Pass, Shader, WebGLRenderer };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actis/core",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "26.3.0",
|
|
5
|
+
"description": "A versatile WebGL renderer designed with multipass support in mind.",
|
|
6
|
+
"author": "Leandro Peres <leandroperes@protonmail.com>",
|
|
7
|
+
"license": "GPL-3.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/SOHNE/Actis.git",
|
|
11
|
+
"directory": "packages-engine/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/SOHNE/Actis/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"shader",
|
|
18
|
+
"glsl",
|
|
19
|
+
"multipass",
|
|
20
|
+
"webgl",
|
|
21
|
+
"canvas",
|
|
22
|
+
"actis"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.mjs",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"main": "dist/index.cjs",
|
|
32
|
+
"module": "dist/index.mjs",
|
|
33
|
+
"types": "dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/webgl2": "^0.0.12",
|
|
39
|
+
"tsdown": "^0.21.7",
|
|
40
|
+
"tslib": "^2.8.1",
|
|
41
|
+
"typescript": "5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsdown",
|
|
45
|
+
"dev": "tsdown --watch",
|
|
46
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
47
|
+
}
|
|
48
|
+
}
|