@fonsecabarreto/genesis-gl-core 0.1.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 +21 -0
- package/README.md +33 -0
- package/dist/Camera-DY_8gx3C.d.ts +45 -0
- package/dist/Core/classes/Material.d.ts +3 -0
- package/dist/Core/classes/Material.js +9 -0
- package/dist/Core/classes/Material.js.map +1 -0
- package/dist/Core/classes/Model.d.ts +5 -0
- package/dist/Core/classes/Model.js +7 -0
- package/dist/Core/classes/Model.js.map +1 -0
- package/dist/Core/classes/Renderer.d.ts +30 -0
- package/dist/Core/classes/Renderer.js +11 -0
- package/dist/Core/classes/Renderer.js.map +1 -0
- package/dist/Core/classes/Scene.d.ts +37 -0
- package/dist/Core/classes/Scene.js +7 -0
- package/dist/Core/classes/Scene.js.map +1 -0
- package/dist/Core/classes/Viewport.d.ts +37 -0
- package/dist/Core/classes/Viewport.js +7 -0
- package/dist/Core/classes/Viewport.js.map +1 -0
- package/dist/Core/domain/interfaces/Vectors.d.ts +4 -0
- package/dist/Core/domain/interfaces/Vectors.js +1 -0
- package/dist/Core/domain/interfaces/Vectors.js.map +1 -0
- package/dist/Core/index.d.ts +10 -0
- package/dist/Core/index.js +51 -0
- package/dist/Core/index.js.map +1 -0
- package/dist/Core/utils/get-overlap.d.ts +3 -0
- package/dist/Core/utils/get-overlap.js +11 -0
- package/dist/Core/utils/get-overlap.js.map +1 -0
- package/dist/Core/utils/load-glb.d.ts +101 -0
- package/dist/Core/utils/load-glb.js +697 -0
- package/dist/Core/utils/load-glb.js.map +1 -0
- package/dist/Core/utils/parse-obj.d.ts +10 -0
- package/dist/Core/utils/parse-obj.js +183 -0
- package/dist/Core/utils/parse-obj.js.map +1 -0
- package/dist/Editor/index.d.ts +364 -0
- package/dist/Editor/index.js +1737 -0
- package/dist/Editor/index.js.map +1 -0
- package/dist/Game/controls/KeyboardInput.d.ts +8 -0
- package/dist/Game/controls/KeyboardInput.js +7 -0
- package/dist/Game/controls/KeyboardInput.js.map +1 -0
- package/dist/Game/index.d.ts +45 -0
- package/dist/Game/index.js +353 -0
- package/dist/Game/index.js.map +1 -0
- package/dist/KeyboardControl-5w7Vm0J0.d.ts +18 -0
- package/dist/KeyboardInput-DTsfj3tE.d.ts +166 -0
- package/dist/Material-BGLkldxv.d.ts +74 -0
- package/dist/Model-CQvDXd-b.d.ts +302 -0
- package/dist/WebGLCore-DR7ZHJB0.d.ts +22 -0
- package/dist/chunk-3ULETMWF.js +144 -0
- package/dist/chunk-3ULETMWF.js.map +1 -0
- package/dist/chunk-5TAAXI6S.js +330 -0
- package/dist/chunk-5TAAXI6S.js.map +1 -0
- package/dist/chunk-6LS6AO5H.js +296 -0
- package/dist/chunk-6LS6AO5H.js.map +1 -0
- package/dist/chunk-JK2HEZAT.js +317 -0
- package/dist/chunk-JK2HEZAT.js.map +1 -0
- package/dist/chunk-P7QOKDLY.js +57 -0
- package/dist/chunk-P7QOKDLY.js.map +1 -0
- package/dist/chunk-QCQVJCSR.js +968 -0
- package/dist/chunk-QCQVJCSR.js.map +1 -0
- package/dist/chunk-SUNYSY45.js +81 -0
- package/dist/chunk-SUNYSY45.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// src/Core/classes/Camera.ts
|
|
2
|
+
import { mat4, vec3, vec4 } from "gl-matrix";
|
|
3
|
+
var Camera = class {
|
|
4
|
+
constructor(viewportWidth, viewportHeight) {
|
|
5
|
+
this.viewportWidth = viewportWidth;
|
|
6
|
+
this.viewportHeight = viewportHeight;
|
|
7
|
+
}
|
|
8
|
+
viewportWidth;
|
|
9
|
+
viewportHeight;
|
|
10
|
+
target = [0, 0, 0];
|
|
11
|
+
// camera looks at this
|
|
12
|
+
/* Translation */
|
|
13
|
+
position = [0, 0, 0];
|
|
14
|
+
// 3D position
|
|
15
|
+
/* Zoom */
|
|
16
|
+
zoom = 0.89;
|
|
17
|
+
minZoom = 0.5;
|
|
18
|
+
maxZoom = 1;
|
|
19
|
+
zoomSpeed = 0.06;
|
|
20
|
+
/* Rotation */
|
|
21
|
+
yaw = 0;
|
|
22
|
+
// horizontal rotation
|
|
23
|
+
pitch = 0.5;
|
|
24
|
+
// vertical rotation
|
|
25
|
+
near = 0.01;
|
|
26
|
+
far = 1e3;
|
|
27
|
+
/* Render Utils */
|
|
28
|
+
lastViewMatrix;
|
|
29
|
+
lastProjectionMatrix;
|
|
30
|
+
/** Translate the camera by a delta. */
|
|
31
|
+
move(dx, dy, dz = 0) {
|
|
32
|
+
this.position[0] += dx;
|
|
33
|
+
this.position[1] += dy;
|
|
34
|
+
this.position[2] += dz;
|
|
35
|
+
}
|
|
36
|
+
/** Clamp-set the zoom level. */
|
|
37
|
+
setZoom(zoom) {
|
|
38
|
+
this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));
|
|
39
|
+
}
|
|
40
|
+
/** Zoom by a signed delta (positive = zoom in). */
|
|
41
|
+
zoomBy(delta) {
|
|
42
|
+
const factor = Math.exp(delta * this.zoomSpeed);
|
|
43
|
+
this.setZoom(this.zoom * factor);
|
|
44
|
+
}
|
|
45
|
+
worldToNDC(x, y) {
|
|
46
|
+
const ndcX = (x - this.position[0]) / this.viewportWidth * 2 * this.zoom;
|
|
47
|
+
const ndcY = (y - this.position[1]) / this.viewportHeight * 2 * this.zoom;
|
|
48
|
+
return [ndcX, ndcY];
|
|
49
|
+
}
|
|
50
|
+
worldScale(scale) {
|
|
51
|
+
return [
|
|
52
|
+
scale[0] / this.viewportWidth * 2 * this.zoom,
|
|
53
|
+
scale[1] / this.viewportHeight * 2 * this.zoom
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
setViewport(width, height) {
|
|
57
|
+
this.viewportWidth = width;
|
|
58
|
+
this.viewportHeight = height;
|
|
59
|
+
}
|
|
60
|
+
/** Compute the eye position from yaw/pitch/distance to target. */
|
|
61
|
+
getComputedPosition() {
|
|
62
|
+
const radius = vec3.distance(this.position, this.target);
|
|
63
|
+
const camX = this.target[0] + radius * Math.cos(this.pitch) * Math.sin(this.yaw);
|
|
64
|
+
const camY = this.target[1] + radius * Math.sin(this.pitch);
|
|
65
|
+
const camZ = this.target[2] + radius * Math.cos(this.pitch) * Math.cos(this.yaw);
|
|
66
|
+
return vec3.fromValues(camX, camY, camZ);
|
|
67
|
+
}
|
|
68
|
+
/** Build the view matrix (lookAt). */
|
|
69
|
+
getViewMatrix() {
|
|
70
|
+
const view = mat4.create();
|
|
71
|
+
const eye = this.getComputedPosition();
|
|
72
|
+
const up = vec3.fromValues(0, 1, 0);
|
|
73
|
+
mat4.lookAt(view, eye, this.target, up);
|
|
74
|
+
return view;
|
|
75
|
+
}
|
|
76
|
+
/** Build the perspective projection matrix. */
|
|
77
|
+
getProjectionMatrix() {
|
|
78
|
+
const projection = mat4.create();
|
|
79
|
+
const aspect = this.viewportWidth / this.viewportHeight;
|
|
80
|
+
const fov = Math.PI / 4 / this.zoom;
|
|
81
|
+
mat4.perspective(projection, fov, aspect, this.near, this.far);
|
|
82
|
+
return projection;
|
|
83
|
+
}
|
|
84
|
+
worldToNDC3D(x, y, z = 0) {
|
|
85
|
+
const world = vec4.fromValues(x, y, z, 1);
|
|
86
|
+
const mvp = mat4.create();
|
|
87
|
+
mat4.multiply(mvp, this.getProjectionMatrix(), this.getViewMatrix());
|
|
88
|
+
vec4.transformMat4(world, world, mvp);
|
|
89
|
+
return [world[0] / world[3], world[1] / world[3], world[2] / world[3]];
|
|
90
|
+
}
|
|
91
|
+
worldScale3D(scale) {
|
|
92
|
+
return [
|
|
93
|
+
scale[0] / this.viewportWidth * 2 * this.zoom,
|
|
94
|
+
scale[1] / this.viewportHeight * 2 * this.zoom,
|
|
95
|
+
scale[2] / ((this.viewportWidth + this.viewportHeight) / 2) * 2 * this.zoom
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
minPitch = -Math.PI / 2 + 1.49;
|
|
99
|
+
maxPitch = Math.PI / 2 - 0.01;
|
|
100
|
+
rotate(deltaYaw, deltaPitch) {
|
|
101
|
+
this.yaw += deltaYaw;
|
|
102
|
+
this.pitch -= deltaPitch;
|
|
103
|
+
this.pitch = Math.max(this.minPitch, Math.min(this.maxPitch, this.pitch));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/Core/controls/Mouse/MouseWheelControl.ts
|
|
108
|
+
var MouseWheelControl = class {
|
|
109
|
+
listeners = [];
|
|
110
|
+
wheelHandler;
|
|
111
|
+
enable() {
|
|
112
|
+
if (this.wheelHandler) return;
|
|
113
|
+
this.wheelHandler = (e) => {
|
|
114
|
+
const direction = e.deltaY < 0 ? "up" : "down";
|
|
115
|
+
this.notify(direction, e.deltaY);
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("wheel", this.wheelHandler, { passive: true });
|
|
118
|
+
}
|
|
119
|
+
disable() {
|
|
120
|
+
if (this.wheelHandler) {
|
|
121
|
+
window.removeEventListener("wheel", this.wheelHandler);
|
|
122
|
+
this.wheelHandler = void 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
onChange(listener) {
|
|
126
|
+
this.listeners.push(listener);
|
|
127
|
+
}
|
|
128
|
+
/** Remove a previously registered listener. */
|
|
129
|
+
removeListener(listener) {
|
|
130
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
131
|
+
}
|
|
132
|
+
notify(direction, delta) {
|
|
133
|
+
for (const listener of this.listeners) listener(direction, delta);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/Core/controls/Mouse/MouseDragControl.ts
|
|
138
|
+
var MouseDragControl = class {
|
|
139
|
+
constructor(element) {
|
|
140
|
+
this.element = element;
|
|
141
|
+
}
|
|
142
|
+
element;
|
|
143
|
+
listeners = [];
|
|
144
|
+
isDragging = false;
|
|
145
|
+
dragButton = 0;
|
|
146
|
+
// 0 = left, 2 = right
|
|
147
|
+
lastX = 0;
|
|
148
|
+
lastY = 0;
|
|
149
|
+
enable() {
|
|
150
|
+
this.element.addEventListener("mousedown", this.onMouseDown);
|
|
151
|
+
window.addEventListener("mouseup", this.onMouseUp);
|
|
152
|
+
window.addEventListener("mousemove", this.onMouseMove);
|
|
153
|
+
}
|
|
154
|
+
disable() {
|
|
155
|
+
this.element.removeEventListener("mousedown", this.onMouseDown);
|
|
156
|
+
window.removeEventListener("mouseup", this.onMouseUp);
|
|
157
|
+
window.removeEventListener("mousemove", this.onMouseMove);
|
|
158
|
+
}
|
|
159
|
+
onChange(listener) {
|
|
160
|
+
this.listeners.push(listener);
|
|
161
|
+
}
|
|
162
|
+
/** Remove a previously registered listener. */
|
|
163
|
+
removeListener(listener) {
|
|
164
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
165
|
+
}
|
|
166
|
+
onMouseDown = (e) => {
|
|
167
|
+
this.isDragging = true;
|
|
168
|
+
this.dragButton = e.button;
|
|
169
|
+
this.lastX = e.clientX;
|
|
170
|
+
this.lastY = e.clientY;
|
|
171
|
+
};
|
|
172
|
+
onMouseUp = () => {
|
|
173
|
+
this.isDragging = false;
|
|
174
|
+
};
|
|
175
|
+
onMouseMove = (e) => {
|
|
176
|
+
if (!this.isDragging) return;
|
|
177
|
+
const dx = e.clientX - this.lastX;
|
|
178
|
+
const dy = e.clientY - this.lastY;
|
|
179
|
+
this.lastX = e.clientX;
|
|
180
|
+
this.lastY = e.clientY;
|
|
181
|
+
for (const listener of this.listeners) listener(dx, dy, this.dragButton);
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/Core/controls/Mouse/MousePointerLockControl.ts
|
|
186
|
+
var MousePointerLockControl = class {
|
|
187
|
+
constructor(element, sensitivity = 1) {
|
|
188
|
+
this.element = element;
|
|
189
|
+
this.sensitivity = sensitivity;
|
|
190
|
+
}
|
|
191
|
+
element;
|
|
192
|
+
sensitivity;
|
|
193
|
+
listeners = [];
|
|
194
|
+
isEnabled = false;
|
|
195
|
+
isPointerLocked = false;
|
|
196
|
+
enable() {
|
|
197
|
+
if (this.isEnabled) return;
|
|
198
|
+
this.isEnabled = true;
|
|
199
|
+
this.element.addEventListener("click", this.requestPointerLock);
|
|
200
|
+
document.addEventListener("pointerlockchange", this.onPointerLockChange);
|
|
201
|
+
document.addEventListener("mousemove", this.onMouseMove);
|
|
202
|
+
}
|
|
203
|
+
disable() {
|
|
204
|
+
if (!this.isEnabled) return;
|
|
205
|
+
this.isEnabled = false;
|
|
206
|
+
this.element.removeEventListener("click", this.requestPointerLock);
|
|
207
|
+
document.removeEventListener("pointerlockchange", this.onPointerLockChange);
|
|
208
|
+
document.removeEventListener("mousemove", this.onMouseMove);
|
|
209
|
+
if (this.isPointerLocked) document.exitPointerLock();
|
|
210
|
+
}
|
|
211
|
+
onChange(listener) {
|
|
212
|
+
this.listeners.push(listener);
|
|
213
|
+
}
|
|
214
|
+
/** Remove a previously registered listener. */
|
|
215
|
+
removeListener(listener) {
|
|
216
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
217
|
+
}
|
|
218
|
+
setSensitivity(value) {
|
|
219
|
+
this.sensitivity = value;
|
|
220
|
+
}
|
|
221
|
+
requestPointerLock = () => {
|
|
222
|
+
this.element.requestPointerLock();
|
|
223
|
+
};
|
|
224
|
+
onPointerLockChange = () => {
|
|
225
|
+
this.isPointerLocked = document.pointerLockElement === this.element;
|
|
226
|
+
};
|
|
227
|
+
onMouseMove = (e) => {
|
|
228
|
+
if (!this.isPointerLocked) return;
|
|
229
|
+
const dx = e.movementX * this.sensitivity;
|
|
230
|
+
const dy = e.movementY * this.sensitivity;
|
|
231
|
+
for (const listener of this.listeners) listener(dx, dy);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/Core/classes/Viewport.ts
|
|
236
|
+
var Viewport = class _Viewport {
|
|
237
|
+
/**
|
|
238
|
+
* @param canvasOrId - An HTMLCanvasElement or the `id` attribute of one.
|
|
239
|
+
* @param width - Logical viewport width.
|
|
240
|
+
* @param height - Logical viewport height.
|
|
241
|
+
* @param webglCore - Optional WebGLCore instance. When provided, the existing
|
|
242
|
+
* GL context is reused for viewport resize instead of
|
|
243
|
+
* requesting a new one.
|
|
244
|
+
*/
|
|
245
|
+
constructor(canvasOrId, width, height, webglCore) {
|
|
246
|
+
this.width = width;
|
|
247
|
+
this.height = height;
|
|
248
|
+
this.canvas = typeof canvasOrId === "string" ? document.getElementById(canvasOrId) : canvasOrId;
|
|
249
|
+
if (!this.canvas) throw new Error("Canvas not found");
|
|
250
|
+
this.webglCore = webglCore ?? null;
|
|
251
|
+
this.camera = new Camera(width, height);
|
|
252
|
+
this.dragControl = new MouseDragControl(this.canvas);
|
|
253
|
+
this.moveControl = new MousePointerLockControl(this.canvas, 0.25);
|
|
254
|
+
this.resizeCanvas();
|
|
255
|
+
this.setupControls();
|
|
256
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
257
|
+
}
|
|
258
|
+
width;
|
|
259
|
+
height;
|
|
260
|
+
static zoomInterval = 25;
|
|
261
|
+
wheelControl = new MouseWheelControl();
|
|
262
|
+
dragControl;
|
|
263
|
+
moveControl;
|
|
264
|
+
lastZoomTime = 0;
|
|
265
|
+
canvas;
|
|
266
|
+
scale = 1;
|
|
267
|
+
webglCore = null;
|
|
268
|
+
resizeHandler = () => this.resizeCanvas();
|
|
269
|
+
camera;
|
|
270
|
+
setupControls() {
|
|
271
|
+
const zoomInterval = _Viewport.zoomInterval;
|
|
272
|
+
this.wheelControl.onChange((direction) => {
|
|
273
|
+
const now = performance.now();
|
|
274
|
+
if (now - this.lastZoomTime < zoomInterval) return;
|
|
275
|
+
const zoomDelta = direction === "up" ? 1 : -1;
|
|
276
|
+
this.camera.zoomBy(zoomDelta);
|
|
277
|
+
this.lastZoomTime = now;
|
|
278
|
+
});
|
|
279
|
+
this.wheelControl.enable();
|
|
280
|
+
this.moveControl.onChange((dx, dy) => {
|
|
281
|
+
const factor = 0.01 * this.camera.zoom;
|
|
282
|
+
this.camera.rotate(-dx * factor, -dy * factor);
|
|
283
|
+
});
|
|
284
|
+
this.moveControl.enable();
|
|
285
|
+
this.canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
286
|
+
}
|
|
287
|
+
isDraggingCamera() {
|
|
288
|
+
return this.dragControl.isDragging;
|
|
289
|
+
}
|
|
290
|
+
resizeCanvas() {
|
|
291
|
+
const screenWidth = window.innerWidth;
|
|
292
|
+
const screenHeight = window.innerHeight;
|
|
293
|
+
const aspectViewport = this.width / this.height;
|
|
294
|
+
const aspectScreen = screenWidth / screenHeight;
|
|
295
|
+
let canvasWidth, canvasHeight;
|
|
296
|
+
if (aspectScreen > aspectViewport) {
|
|
297
|
+
canvasHeight = screenHeight;
|
|
298
|
+
canvasWidth = canvasHeight * aspectViewport;
|
|
299
|
+
} else {
|
|
300
|
+
canvasWidth = screenWidth;
|
|
301
|
+
canvasHeight = canvasWidth / aspectViewport;
|
|
302
|
+
}
|
|
303
|
+
this.canvas.width = canvasWidth;
|
|
304
|
+
this.canvas.height = canvasHeight;
|
|
305
|
+
this.scale = canvasWidth / this.width;
|
|
306
|
+
const gl = this.webglCore ? this.webglCore.getRenderingContext() : this.canvas.getContext("webgl2") ?? this.canvas.getContext("webgl");
|
|
307
|
+
if (!gl) throw new Error("WebGL not supported");
|
|
308
|
+
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
309
|
+
this.camera.setViewport(canvasWidth, canvasHeight);
|
|
310
|
+
}
|
|
311
|
+
getCanvas() {
|
|
312
|
+
return this.canvas;
|
|
313
|
+
}
|
|
314
|
+
getScale() {
|
|
315
|
+
return this.scale;
|
|
316
|
+
}
|
|
317
|
+
/** Release event listeners. Call when the viewport is no longer needed. */
|
|
318
|
+
dispose() {
|
|
319
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
320
|
+
this.wheelControl.disable();
|
|
321
|
+
this.dragControl.disable();
|
|
322
|
+
this.moveControl.disable();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export {
|
|
327
|
+
Camera,
|
|
328
|
+
Viewport
|
|
329
|
+
};
|
|
330
|
+
//# sourceMappingURL=chunk-5TAAXI6S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Core/classes/Camera.ts","../src/Core/controls/Mouse/MouseWheelControl.ts","../src/Core/controls/Mouse/MouseDragControl.ts","../src/Core/controls/Mouse/MousePointerLockControl.ts","../src/Core/classes/Viewport.ts"],"sourcesContent":["import { mat4, vec3, vec4 } from 'gl-matrix';\nimport { Vector3 } from '../domain/interfaces/Vectors';\n\n/**\n * Perspective camera with orbit (yaw/pitch), zoom, and viewport controls.\n */\nexport class Camera {\n public target: Vector3 = [0, 0, 0]; // camera looks at this\n\n /* Translation */\n public position: Vector3 = [0, 0, 0]; // 3D position\n\n /* Zoom */\n public zoom: number = 0.89;\n public minZoom: number = 0.5;\n public maxZoom: number = 1;\n public zoomSpeed: number = 0.06;\n\n /* Rotation */\n public yaw: number = 0; // horizontal rotation\n public pitch: number = 0.5; // vertical rotation\n\n public near = 0.01;\n public far = 1000.0;\n\n /* Render Utils */\n public lastViewMatrix?: Float32Array;\n public lastProjectionMatrix?: Float32Array;\n\n constructor(\n public viewportWidth: number,\n public viewportHeight: number,\n ) {}\n\n /** Translate the camera by a delta. */\n move(dx: number, dy: number, dz: number = 0) {\n this.position[0] += dx;\n this.position[1] += dy;\n this.position[2] += dz;\n }\n\n /** Clamp-set the zoom level. */\n setZoom(zoom: number) {\n this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));\n }\n\n /** Zoom by a signed delta (positive = zoom in). */\n zoomBy(delta: number) {\n const factor = Math.exp(delta * this.zoomSpeed);\n this.setZoom(this.zoom * factor);\n }\n\n worldToNDC(x: number, y: number): [number, number] {\n const ndcX = ((x - this.position[0]) / this.viewportWidth) * 2 * this.zoom;\n const ndcY = ((y - this.position[1]) / this.viewportHeight) * 2 * this.zoom;\n return [ndcX, ndcY];\n }\n\n worldScale(scale: [number, number]): [number, number] {\n return [\n (scale[0] / this.viewportWidth) * 2 * this.zoom,\n (scale[1] / this.viewportHeight) * 2 * this.zoom,\n ];\n }\n\n setViewport(width: number, height: number) {\n this.viewportWidth = width;\n this.viewportHeight = height;\n }\n\n /** Compute the eye position from yaw/pitch/distance to target. */\n getComputedPosition(): vec3 {\n const radius = vec3.distance(this.position, this.target);\n\n const camX =\n this.target[0] + radius * Math.cos(this.pitch) * Math.sin(this.yaw);\n const camY = this.target[1] + radius * Math.sin(this.pitch);\n const camZ =\n this.target[2] + radius * Math.cos(this.pitch) * Math.cos(this.yaw);\n\n return vec3.fromValues(camX, camY, camZ);\n }\n\n /** Build the view matrix (lookAt). */\n getViewMatrix(): mat4 {\n const view = mat4.create();\n\n const eye = this.getComputedPosition(); // Use the computed position\n const up = vec3.fromValues(0, 1, 0);\n\n mat4.lookAt(view, eye, this.target, up);\n return view;\n }\n\n /** Build the perspective projection matrix. */\n getProjectionMatrix(): mat4 {\n const projection = mat4.create();\n const aspect = this.viewportWidth / this.viewportHeight;\n const fov = Math.PI / 4 / this.zoom;\n mat4.perspective(projection, fov, aspect, this.near, this.far);\n return projection;\n }\n\n worldToNDC3D(x: number, y: number, z: number = 0): [number, number, number] {\n const world = vec4.fromValues(x, y, z, 1.0);\n const mvp = mat4.create();\n mat4.multiply(mvp, this.getProjectionMatrix(), this.getViewMatrix());\n vec4.transformMat4(world, world, mvp);\n return [world[0] / world[3], world[1] / world[3], world[2] / world[3]];\n }\n\n worldScale3D(scale: [number, number, number]): [number, number, number] {\n return [\n (scale[0] / this.viewportWidth) * 2 * this.zoom,\n (scale[1] / this.viewportHeight) * 2 * this.zoom,\n (scale[2] / ((this.viewportWidth + this.viewportHeight) / 2)) *\n 2 *\n this.zoom,\n ];\n }\n\n public minPitch: number = -Math.PI / 2 + 1.49;\n public maxPitch: number = Math.PI / 2 - 0.01;\n\n rotate(deltaYaw: number, deltaPitch: number) {\n this.yaw += deltaYaw;\n\n this.pitch -= deltaPitch; // ← add deltaPitch here\n this.pitch = Math.max(this.minPitch, Math.min(this.maxPitch, this.pitch));\n }\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type WheelDirection = 'up' | 'down';\nexport type WheelListener = (direction: WheelDirection, delta: number) => void;\n\nexport class MouseWheelControl implements IMouseControl {\n private listeners: WheelListener[] = [];\n private wheelHandler?: (e: WheelEvent) => void;\n\n public enable() {\n if (this.wheelHandler) return;\n\n this.wheelHandler = (e: WheelEvent) => {\n const direction: WheelDirection = e.deltaY < 0 ? 'up' : 'down';\n this.notify(direction, e.deltaY);\n };\n\n window.addEventListener('wheel', this.wheelHandler, { passive: true });\n }\n\n public disable() {\n if (this.wheelHandler) {\n window.removeEventListener('wheel', this.wheelHandler);\n this.wheelHandler = undefined;\n }\n }\n\n public onChange(listener: WheelListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n public removeListener(listener: WheelListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n private notify(direction: WheelDirection, delta: number) {\n for (const listener of this.listeners) listener(direction, delta);\n }\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type DragListener = (dx: number, dy: number, button: number) => void;\n\nexport class MouseDragControl implements IMouseControl {\n private listeners: DragListener[] = [];\n public isDragging = false;\n\n private dragButton: number = 0; // 0 = left, 2 = right\n private lastX = 0;\n private lastY = 0;\n\n constructor(private element: HTMLElement) {}\n\n enable() {\n this.element.addEventListener('mousedown', this.onMouseDown);\n window.addEventListener('mouseup', this.onMouseUp);\n window.addEventListener('mousemove', this.onMouseMove);\n }\n\n disable() {\n this.element.removeEventListener('mousedown', this.onMouseDown);\n window.removeEventListener('mouseup', this.onMouseUp);\n window.removeEventListener('mousemove', this.onMouseMove);\n }\n\n onChange(listener: DragListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n removeListener(listener: DragListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n private onMouseDown = (e: MouseEvent) => {\n this.isDragging = true;\n this.dragButton = e.button;\n\n this.lastX = e.clientX;\n this.lastY = e.clientY;\n };\n\n private onMouseUp = () => {\n this.isDragging = false;\n };\n\n private onMouseMove = (e: MouseEvent) => {\n if (!this.isDragging) return;\n\n const dx = e.clientX - this.lastX;\n const dy = e.clientY - this.lastY;\n\n this.lastX = e.clientX;\n this.lastY = e.clientY;\n\n for (const listener of this.listeners) listener(dx, dy, this.dragButton);\n };\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type MoveListener = (dx: number, dy: number) => void;\n\n/**\n * Captures mouse movement using the Pointer Lock API.\n * Ideal for camera-like controls where the cursor should not hit screen edges.\n */\nexport class MousePointerLockControl implements IMouseControl {\n private listeners: MoveListener[] = [];\n private isEnabled = false;\n private isPointerLocked = false;\n\n constructor(\n private element: HTMLElement,\n private sensitivity = 1,\n ) {}\n\n enable() {\n if (this.isEnabled) return;\n this.isEnabled = true;\n\n // Must be initiated by user click\n this.element.addEventListener('click', this.requestPointerLock);\n document.addEventListener('pointerlockchange', this.onPointerLockChange);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n disable() {\n if (!this.isEnabled) return;\n this.isEnabled = false;\n\n this.element.removeEventListener('click', this.requestPointerLock);\n document.removeEventListener('pointerlockchange', this.onPointerLockChange);\n document.removeEventListener('mousemove', this.onMouseMove);\n\n if (this.isPointerLocked) document.exitPointerLock();\n }\n\n onChange(listener: MoveListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n removeListener(listener: MoveListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n setSensitivity(value: number) {\n this.sensitivity = value;\n }\n\n private requestPointerLock = () => {\n this.element.requestPointerLock();\n };\n\n private onPointerLockChange = () => {\n this.isPointerLocked = document.pointerLockElement === this.element;\n };\n\n private onMouseMove = (e: MouseEvent) => {\n if (!this.isPointerLocked) return;\n\n const dx = e.movementX * this.sensitivity;\n const dy = e.movementY * this.sensitivity;\n\n for (const listener of this.listeners) listener(dx, dy);\n };\n}\n","import { Camera } from './Camera';\nimport {\n MouseWheelControl,\n WheelDirection,\n} from '../controls/Mouse/MouseWheelControl';\nimport { MouseDragControl } from '../controls/Mouse/MouseDragControl';\nimport { MousePointerLockControl } from '../controls/Mouse/MousePointerLockControl';\nimport WebGLCore from './WebGLCore';\n\nexport class Viewport {\n private static zoomInterval = 25;\n\n private wheelControl = new MouseWheelControl();\n private dragControl: MouseDragControl;\n private moveControl: MousePointerLockControl;\n\n private lastZoomTime: number = 0;\n private canvas: HTMLCanvasElement;\n private scale: number = 1;\n private webglCore: WebGLCore | null = null;\n private resizeHandler = () => this.resizeCanvas();\n public camera: Camera;\n\n /**\n * @param canvasOrId - An HTMLCanvasElement or the `id` attribute of one.\n * @param width - Logical viewport width.\n * @param height - Logical viewport height.\n * @param webglCore - Optional WebGLCore instance. When provided, the existing\n * GL context is reused for viewport resize instead of\n * requesting a new one.\n */\n constructor(\n canvasOrId: string | HTMLCanvasElement,\n public width: number,\n public height: number,\n webglCore?: WebGLCore,\n ) {\n this.canvas =\n typeof canvasOrId === 'string'\n ? (document.getElementById(canvasOrId) as HTMLCanvasElement)\n : canvasOrId;\n if (!this.canvas) throw new Error('Canvas not found');\n\n this.webglCore = webglCore ?? null;\n this.camera = new Camera(width, height);\n this.dragControl = new MouseDragControl(this.canvas);\n this.moveControl = new MousePointerLockControl(this.canvas, 0.25);\n\n this.resizeCanvas();\n this.setupControls();\n\n window.addEventListener('resize', this.resizeHandler);\n }\n\n private setupControls() {\n const zoomInterval = Viewport.zoomInterval;\n\n // Camera Zoom\n this.wheelControl.onChange((direction: WheelDirection) => {\n const now = performance.now();\n if (now - this.lastZoomTime < zoomInterval) return;\n\n const zoomDelta = direction === 'up' ? 1 : -1;\n this.camera.zoomBy(zoomDelta);\n\n this.lastZoomTime = now;\n });\n this.wheelControl.enable();\n\n // Drag / pan / rotate\n /* this.dragControl.onChange((dx, dy, button) => {\n const factor = 0.01 * this.camera.zoom;\n\n if (button === 2) {\n // Right click = orbit\n this.camera.rotate(-dx * factor, -dy * factor);\n } else if (button === 0) {\n this.camera.target[0] -= (dx * factor) / this.scale;\n this.camera.target[1] += (dy * factor) / this.scale;\n }\n\n });\n this.dragControl.enable(); */\n\n this.moveControl.onChange((dx, dy) => {\n const factor = 0.01 * this.camera.zoom;\n this.camera.rotate(-dx * factor, -dy * factor);\n });\n this.moveControl.enable();\n\n this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());\n }\n\n public isDraggingCamera(): boolean {\n return this.dragControl.isDragging;\n }\n\n private resizeCanvas() {\n const screenWidth = window.innerWidth;\n const screenHeight = window.innerHeight;\n\n const aspectViewport = this.width / this.height;\n const aspectScreen = screenWidth / screenHeight;\n\n let canvasWidth: number, canvasHeight: number;\n\n if (aspectScreen > aspectViewport) {\n canvasHeight = screenHeight;\n canvasWidth = canvasHeight * aspectViewport;\n } else {\n canvasWidth = screenWidth;\n canvasHeight = canvasWidth / aspectViewport;\n }\n\n this.canvas.width = canvasWidth;\n this.canvas.height = canvasHeight;\n\n this.scale = canvasWidth / this.width;\n\n // Reuse the existing GL context when available, otherwise fall back.\n const gl = this.webglCore\n ? this.webglCore.getRenderingContext()\n : (this.canvas.getContext('webgl2') ?? this.canvas.getContext('webgl'));\n\n if (!gl) throw new Error('WebGL not supported');\n gl.viewport(0, 0, canvasWidth, canvasHeight);\n\n this.camera.setViewport(canvasWidth, canvasHeight);\n }\n\n getCanvas(): HTMLCanvasElement {\n return this.canvas;\n }\n\n getScale(): number {\n return this.scale;\n }\n\n /** Release event listeners. Call when the viewport is no longer needed. */\n dispose() {\n window.removeEventListener('resize', this.resizeHandler);\n this.wheelControl.disable();\n this.dragControl.disable();\n this.moveControl.disable();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// src/Core/utils/load-texture.ts
|
|
2
|
+
async function loadWebGlTexture(gl, url, options = {}) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const texture = gl.createTexture();
|
|
5
|
+
if (!texture) return reject(new Error("Failed to create texture"));
|
|
6
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
7
|
+
const level = 0;
|
|
8
|
+
const internalFormat = gl.RGBA;
|
|
9
|
+
const width = 1;
|
|
10
|
+
const height = 1;
|
|
11
|
+
const border = 0;
|
|
12
|
+
const srcFormat = gl.RGBA;
|
|
13
|
+
const srcType = gl.UNSIGNED_BYTE;
|
|
14
|
+
const pixel = new Uint8Array([128, 128, 128, 255]);
|
|
15
|
+
gl.texImage2D(
|
|
16
|
+
gl.TEXTURE_2D,
|
|
17
|
+
level,
|
|
18
|
+
internalFormat,
|
|
19
|
+
width,
|
|
20
|
+
height,
|
|
21
|
+
border,
|
|
22
|
+
srcFormat,
|
|
23
|
+
srcType,
|
|
24
|
+
pixel
|
|
25
|
+
);
|
|
26
|
+
const image = new Image();
|
|
27
|
+
image.crossOrigin = "anonymous";
|
|
28
|
+
image.onload = () => {
|
|
29
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
30
|
+
gl.texImage2D(
|
|
31
|
+
gl.TEXTURE_2D,
|
|
32
|
+
level,
|
|
33
|
+
internalFormat,
|
|
34
|
+
srcFormat,
|
|
35
|
+
srcType,
|
|
36
|
+
image
|
|
37
|
+
);
|
|
38
|
+
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
|
|
39
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
40
|
+
gl.texParameteri(
|
|
41
|
+
gl.TEXTURE_2D,
|
|
42
|
+
gl.TEXTURE_MIN_FILTER,
|
|
43
|
+
gl.LINEAR_MIPMAP_LINEAR
|
|
44
|
+
);
|
|
45
|
+
if (options.repeat) {
|
|
46
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
47
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
51
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
52
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
53
|
+
}
|
|
54
|
+
resolve(texture);
|
|
55
|
+
};
|
|
56
|
+
image.onerror = reject;
|
|
57
|
+
image.src = url;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function isPowerOf2(value) {
|
|
61
|
+
return (value & value - 1) === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/Core/utils/parse-hex-to-rgb.ts
|
|
65
|
+
function parseHexToRgbArray(hex) {
|
|
66
|
+
if (hex.startsWith("#")) hex = hex.slice(1);
|
|
67
|
+
let r = 1, g = 1, b = 1, a = 1;
|
|
68
|
+
if (hex.length === 6) {
|
|
69
|
+
r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
70
|
+
g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
71
|
+
b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
72
|
+
a = 1;
|
|
73
|
+
} else if (hex.length === 8) {
|
|
74
|
+
r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
75
|
+
g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
76
|
+
b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
77
|
+
a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error("Invalid hex color format. Use #RRGGBB or #RRGGBBAA.");
|
|
80
|
+
}
|
|
81
|
+
return [r, g, b, a];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/Core/classes/Material.ts
|
|
85
|
+
var MAX_LIGHTS = 5;
|
|
86
|
+
var Material = class _Material {
|
|
87
|
+
constructor(webglCore, options = {}) {
|
|
88
|
+
this.webglCore = webglCore;
|
|
89
|
+
this.program = webglCore.getProgram();
|
|
90
|
+
const { gl } = webglCore;
|
|
91
|
+
const names = [
|
|
92
|
+
"uColor",
|
|
93
|
+
"uUnlit",
|
|
94
|
+
"uModel",
|
|
95
|
+
"uView",
|
|
96
|
+
"uProjection",
|
|
97
|
+
"uLightCount",
|
|
98
|
+
"uUseTexture",
|
|
99
|
+
"uTexture",
|
|
100
|
+
// --- LIGHTING UNIFORMS ---
|
|
101
|
+
"uViewPosition",
|
|
102
|
+
// Camera position for specular light
|
|
103
|
+
"uShininess",
|
|
104
|
+
"uSpecularColor",
|
|
105
|
+
"uAmbientColor",
|
|
106
|
+
"uDissolve",
|
|
107
|
+
"uDiffuseColor",
|
|
108
|
+
"uLightDirection[0]",
|
|
109
|
+
"uLightColor[0]",
|
|
110
|
+
"uLightIntensity[0]",
|
|
111
|
+
"uLightType[0]",
|
|
112
|
+
"uLightPosition[0]",
|
|
113
|
+
// NEW: For Point Lights
|
|
114
|
+
"uLightConstant[0]",
|
|
115
|
+
// NEW: Attenuation
|
|
116
|
+
"uLightLinear[0]",
|
|
117
|
+
// NEW: Attenuation
|
|
118
|
+
"uLightQuadratic[0]",
|
|
119
|
+
// NEW: Attenuation
|
|
120
|
+
// --- SKINNING UNIFORMS ---
|
|
121
|
+
"uUseSkinning",
|
|
122
|
+
"uJointMatrices[0]"
|
|
123
|
+
];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
this.uniformLocations[name] = gl.getUniformLocation(this.program, name);
|
|
126
|
+
}
|
|
127
|
+
const attribs = [
|
|
128
|
+
"aPosition",
|
|
129
|
+
"aNormal",
|
|
130
|
+
"aTexCoord",
|
|
131
|
+
"aJointIndices",
|
|
132
|
+
"aJointWeights"
|
|
133
|
+
];
|
|
134
|
+
for (const name of attribs) {
|
|
135
|
+
this.attribLocations[name] = gl.getAttribLocation(this.program, name);
|
|
136
|
+
}
|
|
137
|
+
Object.assign(this, options);
|
|
138
|
+
}
|
|
139
|
+
webglCore;
|
|
140
|
+
// cache
|
|
141
|
+
program;
|
|
142
|
+
uniformLocations = {};
|
|
143
|
+
attribLocations = {};
|
|
144
|
+
// Attributes
|
|
145
|
+
albedoColor = [1, 1, 1, 1];
|
|
146
|
+
unlit = false;
|
|
147
|
+
texture;
|
|
148
|
+
specular = [0.3, 0.3, 0.3];
|
|
149
|
+
shininess = 64;
|
|
150
|
+
doubleSided = false;
|
|
151
|
+
//others
|
|
152
|
+
ambientColor = [0.1, 0.1, 0.1];
|
|
153
|
+
// Ka
|
|
154
|
+
dissolve = 1;
|
|
155
|
+
// d or Tr
|
|
156
|
+
diffuse = [1, 1, 1];
|
|
157
|
+
// Kd
|
|
158
|
+
/** Physics friction coefficient [0–1]. 0 = no resistance (ice), 1 = instant stop. */
|
|
159
|
+
friction = 0.3;
|
|
160
|
+
setColorHex(hex) {
|
|
161
|
+
this.albedoColor = parseHexToRgbArray(hex);
|
|
162
|
+
}
|
|
163
|
+
setColor(rgba) {
|
|
164
|
+
this.albedoColor = rgba;
|
|
165
|
+
this.dissolve = rgba[3];
|
|
166
|
+
this.diffuse = [rgba[0], rgba[1], rgba[2]];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Load an image from URL and create a WebGL texture.
|
|
170
|
+
*/
|
|
171
|
+
async loadTexture(url, options) {
|
|
172
|
+
const { gl } = this.webglCore;
|
|
173
|
+
this.texture = await loadWebGlTexture(gl, url, options);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Create an independent copy of this material.
|
|
177
|
+
* Shares the same WebGL program but copies all mutable properties.
|
|
178
|
+
* The texture reference is shared (immutable GPU resource).
|
|
179
|
+
*/
|
|
180
|
+
clone() {
|
|
181
|
+
const copy = new _Material(this.webglCore);
|
|
182
|
+
copy.albedoColor = [...this.albedoColor];
|
|
183
|
+
copy.specular = [...this.specular];
|
|
184
|
+
copy.ambientColor = [...this.ambientColor];
|
|
185
|
+
copy.diffuse = [...this.diffuse];
|
|
186
|
+
copy.shininess = this.shininess;
|
|
187
|
+
copy.dissolve = this.dissolve;
|
|
188
|
+
copy.unlit = this.unlit;
|
|
189
|
+
copy.doubleSided = this.doubleSided;
|
|
190
|
+
copy.friction = this.friction;
|
|
191
|
+
copy.texture = this.texture;
|
|
192
|
+
return copy;
|
|
193
|
+
}
|
|
194
|
+
apply(gl, lights, viewPosition) {
|
|
195
|
+
if (this.uniformLocations["uColor"])
|
|
196
|
+
gl.uniform4fv(this.uniformLocations["uColor"], this.albedoColor);
|
|
197
|
+
if (this.uniformLocations["uUnlit"])
|
|
198
|
+
gl.uniform1i(this.uniformLocations["uUnlit"], this.unlit ? 1 : 0);
|
|
199
|
+
if (this.uniformLocations["uShininess"])
|
|
200
|
+
gl.uniform1f(this.uniformLocations["uShininess"], this.shininess);
|
|
201
|
+
if (this.uniformLocations["uSpecularColor"])
|
|
202
|
+
gl.uniform3fv(this.uniformLocations["uSpecularColor"], this.specular);
|
|
203
|
+
if (this.uniformLocations["uDissolve"])
|
|
204
|
+
gl.uniform1f(this.uniformLocations["uDissolve"], this.dissolve);
|
|
205
|
+
if (this.uniformLocations["uAmbientColor"])
|
|
206
|
+
gl.uniform3fv(this.uniformLocations["uAmbientColor"], this.ambientColor);
|
|
207
|
+
if (this.uniformLocations["uDiffuseColor"])
|
|
208
|
+
gl.uniform3fv(this.uniformLocations["uDiffuseColor"], this.diffuse);
|
|
209
|
+
if (viewPosition && this.uniformLocations["uViewPosition"]) {
|
|
210
|
+
gl.uniform3fv(this.uniformLocations["uViewPosition"], viewPosition);
|
|
211
|
+
}
|
|
212
|
+
const hasTexture = !!this.texture;
|
|
213
|
+
if (this.uniformLocations["uUseTexture"])
|
|
214
|
+
gl.uniform1i(this.uniformLocations["uUseTexture"], hasTexture ? 1 : 0);
|
|
215
|
+
if (hasTexture && this.texture) {
|
|
216
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
217
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
218
|
+
if (this.uniformLocations["uTexture"])
|
|
219
|
+
gl.uniform1i(this.uniformLocations["uTexture"], 0);
|
|
220
|
+
}
|
|
221
|
+
const MAX = MAX_LIGHTS;
|
|
222
|
+
if (!this.unlit && lights?.length) {
|
|
223
|
+
const count = Math.min(lights.length, MAX);
|
|
224
|
+
if (this.uniformLocations["uLightCount"])
|
|
225
|
+
gl.uniform1i(this.uniformLocations["uLightCount"], count);
|
|
226
|
+
const dirs = [];
|
|
227
|
+
const colors = [];
|
|
228
|
+
const intensities = [];
|
|
229
|
+
const types = [];
|
|
230
|
+
const positions = [];
|
|
231
|
+
const constants = [];
|
|
232
|
+
const linears = [];
|
|
233
|
+
const quadratics = [];
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
const light = lights[i];
|
|
236
|
+
let typeInt = 0;
|
|
237
|
+
if (light.type === "point") typeInt = 1;
|
|
238
|
+
else if (light.type === "ambient") typeInt = 2;
|
|
239
|
+
types.push(typeInt);
|
|
240
|
+
dirs.push(...light.direction ?? [0, 0, 0]);
|
|
241
|
+
positions.push(...light.position ?? [0, 0, 0]);
|
|
242
|
+
colors.push(...light.color);
|
|
243
|
+
intensities.push(light.intensity);
|
|
244
|
+
constants.push(light.constant);
|
|
245
|
+
linears.push(light.linear);
|
|
246
|
+
quadratics.push(light.quadratic);
|
|
247
|
+
}
|
|
248
|
+
if (this.uniformLocations["uLightDirection[0]"])
|
|
249
|
+
gl.uniform3fv(
|
|
250
|
+
this.uniformLocations["uLightDirection[0]"],
|
|
251
|
+
new Float32Array(dirs)
|
|
252
|
+
);
|
|
253
|
+
if (this.uniformLocations["uLightColor[0]"])
|
|
254
|
+
gl.uniform3fv(
|
|
255
|
+
this.uniformLocations["uLightColor[0]"],
|
|
256
|
+
new Float32Array(colors)
|
|
257
|
+
);
|
|
258
|
+
if (this.uniformLocations["uLightIntensity[0]"])
|
|
259
|
+
gl.uniform1fv(
|
|
260
|
+
this.uniformLocations["uLightIntensity[0]"],
|
|
261
|
+
new Float32Array(intensities)
|
|
262
|
+
);
|
|
263
|
+
if (this.uniformLocations["uLightType[0]"])
|
|
264
|
+
gl.uniform1iv(
|
|
265
|
+
this.uniformLocations["uLightType[0]"],
|
|
266
|
+
new Int32Array(types)
|
|
267
|
+
);
|
|
268
|
+
if (this.uniformLocations["uLightPosition[0]"])
|
|
269
|
+
gl.uniform3fv(
|
|
270
|
+
this.uniformLocations["uLightPosition[0]"],
|
|
271
|
+
new Float32Array(positions)
|
|
272
|
+
);
|
|
273
|
+
if (this.uniformLocations["uLightConstant[0]"])
|
|
274
|
+
gl.uniform1fv(
|
|
275
|
+
this.uniformLocations["uLightConstant[0]"],
|
|
276
|
+
new Float32Array(constants)
|
|
277
|
+
);
|
|
278
|
+
if (this.uniformLocations["uLightLinear[0]"])
|
|
279
|
+
gl.uniform1fv(
|
|
280
|
+
this.uniformLocations["uLightLinear[0]"],
|
|
281
|
+
new Float32Array(linears)
|
|
282
|
+
);
|
|
283
|
+
if (this.uniformLocations["uLightQuadratic[0]"])
|
|
284
|
+
gl.uniform1fv(
|
|
285
|
+
this.uniformLocations["uLightQuadratic[0]"],
|
|
286
|
+
new Float32Array(quadratics)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
MAX_LIGHTS,
|
|
294
|
+
Material
|
|
295
|
+
};
|
|
296
|
+
//# sourceMappingURL=chunk-6LS6AO5H.js.map
|