UNPKG

70.4 kBJavaScriptView Raw
1import { View, LoadImage, m4_identity, m4_invert, m4_perspective, m4_transpose, v3_transform, v3_sub, v3_normalize, v3_cross, m4_multiply, v4_transform, v3_scale, m4_getTranslation, TAU, v2_rotate, m4_toNormal4, v3_multiply, toRad, viewRoot } from "@croquet/worldcore-kernel";
2// import { LoadImage } from "./ViewAssetCache";
3// import { m4_identity, m4_invert, m4_perspective, m4_transpose, v3_transform, v3_sub, v3_normalize, v3_cross, m4_multiply, v4_transform, v3_scale, m4_getTranslation, TAU, v2_rotate, m4_toNormal4, v3_multiply, toRad } from "./Vector";
4// import { viewRoot } from "./Root";
5
6//------------------------------------------------------------------------------------------
7// Rendering Globals
8//------------------------------------------------------------------------------------------
9
10let gl;
11let glVersion = 0; // 0 = Basic WebGL, 1 = WebGL + Extensions, 2 = WebGL2
12let glAttributes = 0; // Needs to be at least 16 for instanced rendering
13let glShader; // The current shader that we're using.
14let glCamera; // The current camera that we're using.
15let glPipeline;
16
17//-- Necessary WebGL 1.0 extensions
18
19let instancedArraysExt;
20let drawBuffersExt;
21let depthTextureExt;
22let textureFloatExt;
23let textureFloatLinearExt;
24
25//-- Necessary WebGL 2.0 extensions
26
27let colorBufferFloatExt;
28
29//-- Global access functions
30
31// export function GetGL() {
32// return gl;
33// }
34
35export function GetGLVersion() {
36 return glVersion;
37}
38
39export function SetGLCamera(camera) {
40 glCamera = camera;
41}
42
43export function SetGLPipeline(pipeline) {
44 glPipeline = pipeline;
45}
46
47//------------------------------------------------------------------------------------------
48//-- RenderTarget --------------------------------------------------------------------------
49//------------------------------------------------------------------------------------------
50
51// Generic base class for any object that can be rendered into. This includes things like the
52// main display, or a gbuffer, or a texture framebuffer.
53//
54// You can set the width and height directly in the parameters, or set autoResize to make it
55// track the size of the main display. (AutoResize takes a scaling factor as its value, so
56// you can create render targets with the same aspect ratio as the main display but smaller.)
57//
58// RenderTarget also creates a default camera.
59//
60// You can call draw directly on a RenderTarget without creating any geometry primitives.
61// It will automatically create a single quad to trigger the render. This
62// is handy if you're using passthru shaders that don't have a vector component.
63
64class RenderTarget {
65 constructor(parameters = {width: 1, height: 1}) {
66 this.parameters = parameters;
67 this.background = [0.5, 0.5, 0.5, 1];
68 this.scale = 1;
69 if (parameters.width) this.width = parameters.width;
70 if (parameters.height) this.height = parameters.height;
71 if (parameters.autoResize) {
72 this.scale = parameters.autoResize;
73 const model = viewRoot.model;
74 this.view = new View(model);
75 this.view.subscribe("input", "resize", () => this.resizeToWindow());
76 }
77 }
78
79 destroy() {
80 if (this.quad) this.quad.destroy();
81 if (this.view) this.view.detach();
82 }
83
84 setBackground(background) {
85 this.background = background;
86 }
87
88 resizeToWindow() {
89 this.width = window.innerWidth * this.scale;
90 this.height = window.innerHeight * this.scale;
91 this.buildBuffers();
92 this.updateCameraAspect();
93 }
94
95 buildBuffers() {}
96
97 setCamera(camera) {
98 this.camera = camera;
99 this.updateCameraAspect();
100 }
101
102 updateCameraAspect() {
103 if (this.camera) this.camera.setAspect(this.width, this.height);
104 }
105
106 start() {
107 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
108 this.setup();
109 }
110
111 setup() {
112 gl.viewport(0, 0, this.width, this.height);
113 gl.clearColor(...this.background);
114 gl.clearDepth(1.0);
115 gl.disable(gl.BLEND);
116 gl.enable(gl.DEPTH_TEST);
117 gl.depthFunc(gl.LESS);
118 gl.depthMask(true);
119 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
120 gl.enable(gl.CULL_FACE);
121 gl.cullFace(gl.BACK);
122 gl.enable(gl.POLYGON_OFFSET_FILL); // Allows line primitives to draw in front of triangles
123 gl.polygonOffset(1, 0);
124 }
125
126 // Creates a viewport-filling quad so you can call draw even without a triangle list
127 // (This can be used for passthru shaders)
128 draw() {
129 if (!this.quad) {
130 this.quad = new Triangles(); // Fullscreen quad passthru rendering
131 this.quad.addFace([[-1,-1,0], [1,-1,0], [1,1,0], [-1,1,0]]);
132 this.quad.load();
133 this.quad.clear();
134 }
135 this.quad.draw();
136 }
137}
138
139//------------------------------------------------------------------------------------------
140//-- MainDisplay ---------------------------------------------------------------------------
141//------------------------------------------------------------------------------------------
142
143// The MainDisplay creates a canvas element and adds it to the current document. It then
144// checks to see which version of WebGL it can run. The three options are:
145//
146// 0: Vanilla WebGL with no extensions.
147// 1: WebGL plus every extension this library uses.
148// 2: WebGL2
149//
150// Creating a MainDisplay is what starts up WegGL, so do it first.
151
152export class MainDisplay extends RenderTarget {
153 constructor() {
154 super({autoResize: 1.0}); // The main display always autoresizes to match the window
155
156 this.canvas = document.createElement("canvas");
157 this.canvas.id = "GLCanvas";
158 this.canvas.style.cssText = "position: absolute; left: 0; top: 0; z-index: 0";
159 document.body.insertBefore(this.canvas, null);
160 this.setVersion();
161 this.getAttributeCount();
162 console.log("GL Version: " + glVersion);
163 console.log("GL Max Attributes: " + glAttributes);
164 if (glAttributes < 16) console.log("Warning -- too few GL attributes to support instanced rendering!");
165
166 this.resizeToWindow();
167 }
168
169 destroy() {
170 super.destroy();
171 this.canvas.remove();
172 }
173
174 buildBuffers() {
175 this.canvas.width = this.width;
176 this.canvas.height = this.height;
177 }
178
179 setVersion() {
180 glVersion = 0;
181 gl = this.canvas.getContext('webgl2', { antialias: true });
182 // gl = canvas.getContext('webgl',
183 // { antialias: true });
184 if (gl) {
185 glVersion = 2;
186 // If the device has webgl2 but not the color float texture extension, deferring lighting will break.
187 // Thankfully this doesn't seem to be common. But there might need to be more error handling here.
188 colorBufferFloatExt = gl.getExtension('EXT_color_buffer_float');
189 if (!colorBufferFloatExt) console.log("WebGL2 without color float textures!");
190 return;
191 }
192
193 gl = this.canvas.getContext('webgl');
194
195 // We SHOULD be able to support WebGL 1 with extensions so we can get pretty rendering on Macs, however there's
196 // currently a bug in the float textures that breaks 1+ rendering. Rather than spending time tracking it down
197 // now, I'm commenting out the extension checks to force everything to vanilla webGL 1. 2/10/20
198
199 // There's a bunch of WebGL 1 extension code scattered thru the renderer that mostly works. However, depending on
200 // how long its been since its been commented out, you should probably check it all to make sure it hasn't
201 // succumbed to rot ... .
202
203 // instancedArraysExt = gl.getExtension('ANGLE_instanced_arrays');
204 // drawBuffersExt = gl.getExtension('WEBGL_draw_buffers');
205 // depthTextureExt =gl.getExtension('WEBGL_depth_texture');
206 // textureFloatExt = gl.getExtension('OES_texture_float');
207 // textureFloatLinearExt = gl.getExtension('ANGLE_instanced_arrays');
208
209 // const hasExtensions = instancedArraysExt && drawBuffersExt && depthTextureExt && textureFloatExt && textureFloatLinearExt;
210 // if (hasExtensions) glVersion = 1;
211
212 }
213
214 getAttributeCount() {
215 glAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
216 }
217
218}
219
220//------------------------------------------------------------------------------------------
221//-- GeometryBuffer ------------------------------------------------------------------------
222//------------------------------------------------------------------------------------------
223
224// A render target that combines four buffers:
225//
226// * diffuse -- The scene rendered with only ambient light + one directional light.
227// * normal -- The scene normals in view space.
228// * position -- The surface positions in view space.
229// * depthStencil -- A combination depth and stencil buffer.
230//
231// Generally this buffer is rendered using a GeometryShader as the first stage in a
232// deferred lighting pipeline.
233
234export class GeometryBuffer extends RenderTarget {
235
236 constructor(parameters) {
237 super(parameters);
238 if (glVersion === 0) { console.log("This device does not support geometry buffers!"); return; }
239
240 this.fb = gl.createFramebuffer();
241
242 if (parameters.width) this.width = parameters.width;
243 if (parameters.height) this.width = parameters.height;
244 if (parameters.autoResize) {
245 this.width = window.innerWidth * this.scale;
246 this.height = window.innerHeight * this.scale;
247 }
248
249 this.fb = gl.createFramebuffer();
250 this.buildBuffers();
251 this.updateCameraAspect();
252 }
253
254 buildBuffers() {
255 if (this.diffuse) this.diffuse.destroy();
256 if (this.normal) this.normal.destroy();
257 if (this.position) this.position.destroy();
258 if (this.depthStencil) this.depthStencil.destroy();
259
260 this.diffuse = new FloatTexture(this.width, this.height);
261 this.normal = new FloatTexture(this.width, this.height);
262 this.position = new FloatTexture(this.width, this.height);
263 this.depthStencil = new DepthStencilBuffer(this.width, this.height);
264
265 gl.bindFramebuffer(gl.FRAMEBUFFER, this.fb);
266
267 if (glVersion === 1) { // Different syntax for WebGL and WebGL2
268 gl.framebufferTexture2D(gl.FRAMEBUFFER, drawBuffersExt.COLOR_ATTACHMENT0_WEBGL, gl.TEXTURE_2D, this.diffuse.texture, 0);
269 gl.framebufferTexture2D(gl.FRAMEBUFFER, drawBuffersExt.COLOR_ATTACHMENT1_WEBGL, gl.TEXTURE_2D, this.normal.texture, 0);
270 gl.framebufferTexture2D(gl.FRAMEBUFFER, drawBuffersExt.COLOR_ATTACHMENT2_WEBGL, gl.TEXTURE_2D, this.position.texture, 0);
271 } else {
272 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.diffuse.texture, 0);
273 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.normal.texture, 0);
274 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.position.texture, 0);
275 }
276 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, this.depthStencil.buffer);
277 }
278
279 destroy() {
280 super.destroy();
281 gl.deleteFramebuffer(this.fb);
282 this.diffuse.destroy();
283 this.normal.destroy();
284 this.position.destroy();
285 this.depthStencil.destroy();
286 }
287
288 start() {
289 gl.bindFramebuffer(gl.FRAMEBUFFER, this.fb);
290 if (glVersion === 1) { // Different syntax for WebGL and WebGL2
291 drawBuffersExt.drawBuffersWEBGL([
292 drawBuffersExt.COLOR_ATTACHMENT0_WEBGL, // diffuse lighting
293 drawBuffersExt.COLOR_ATTACHMENT1_WEBGL, // normals
294 drawBuffersExt.COLOR_ATTACHMENT2_WEBGL // positions
295 ]);
296 } else {
297 gl.drawBuffers([
298 gl.COLOR_ATTACHMENT0, // diffuse lighting
299 gl.COLOR_ATTACHMENT1, // normals
300 gl.COLOR_ATTACHMENT2 // positions
301 ]);
302 }
303
304 this.setup();
305 }
306}
307
308//------------------------------------------------------------------------------------------
309//-- Framebuffer ---------------------------------------------------------------------------
310//------------------------------------------------------------------------------------------
311
312// A render target that has only a destination texture but no depth buffer. Generally this is
313// used for intemediate rendering stages in the pipeline.
314
315export class Framebuffer extends RenderTarget {
316 constructor(parameters) {
317 super(parameters);
318
319 this.fb = gl.createFramebuffer();
320 if (parameters.width) this.width = parameters.width;
321 if (parameters.height) this.width = parameters.height;
322 if (parameters.autoResize) {
323 this.width = window.innerWidth * this.scale;
324 this.height = window.innerHeight * this.scale;
325 }
326 this.buildBuffers();
327 this.updateCameraAspect();
328 }
329
330 buildBuffers() {
331 if (this.texture) this.texture.destroy();
332 this.texture = new Texture(this.width, this.height);
333 gl.bindFramebuffer(gl.FRAMEBUFFER, this.fb);
334 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture.texture, 0);
335 }
336
337 destroy() {
338 super.destroy();
339 gl.deleteFramebuffer(this.fb);
340 this.texture.destroy();
341 }
342
343 start() {
344 gl.bindFramebuffer(gl.FRAMEBUFFER, this.fb);
345 this.setup();
346 }
347
348}
349
350//------------------------------------------------------------------------------------------
351//-- SharedStencilFramebuffer --------------------------------------------------------------
352//------------------------------------------------------------------------------------------
353
354// A special type of framebuffer that shares a depthStencil buffer with another render target
355// You initialize it by passing it the pointer to the other render target. Note:
356//
357// * It will be the same size as the other render target
358// * It does not destroy the stencil buffer on destruction
359//
360// This allows letter stages of the deferred lighting pipeline to use the stencil created by
361// the geometry buffer.
362
363export class SharedStencilFramebuffer extends Framebuffer {
364 constructor(owner) {
365 super(owner.parameters);
366 this.owner = owner;
367 this.buildBuffers();
368 }
369
370 buildBuffers() {
371 super.buildBuffers();
372 if (!this.owner) return;
373 this.depthStencil = this.owner.depthStencil;
374 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, this.depthStencil.buffer);
375 }
376}
377
378//------------------------------------------------------------------------------------------
379//-- Texture -------------------------------------------------------------------------------
380//------------------------------------------------------------------------------------------
381
382// Basic 2D texture. It can be loaded from a source image, or used as a render target.
383// If you load from a power of 2 image, it will generate mips and set the parameters so it will tile.
384
385export class Texture {
386
387 constructor(width = 1, height = 1) {
388 this.texture = gl.createTexture();
389 this.width = width;
390 this.height = height;
391 gl.bindTexture(gl.TEXTURE_2D, this.texture);
392 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
393 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
394 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
395 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
396 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
397 }
398
399 destroy() {
400 gl.deleteTexture(this.texture);
401 }
402
403 loadFromURL(url) {
404 gl.bindTexture(gl.TEXTURE_2D, this.texture);
405 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 255, 255]));
406 const cached = LoadImage(url, image => this.loadImage(image));
407 if (cached) this.loadImage(cached);
408 }
409
410 loadFromByteArray(width, height, array) {
411 gl.bindTexture(gl.TEXTURE_2D, this.texture);
412 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, array);
413 this.width = width;
414 this.height = height;
415 if (this.isPowerOfTwo) { // Generate MIPs
416 gl.generateMipmap(gl.TEXTURE_2D);
417 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
418 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
419 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
420 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
421 } else { // Display options are restricted in WebGL for non-power-of-two textures
422 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
423 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
424 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
425 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
426 }
427 }
428
429 loadImage(image) {
430 if (!gl.isTexture(this.texture)) return; // In case texture is destroyed while we're waiting for it to load.
431 gl.bindTexture(gl.TEXTURE_2D, this.texture);
432 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
433 this.width = image.width;
434 this.height = image.height;
435 if (this.isPowerOfTwo) { // Generate MIPs
436 gl.generateMipmap(gl.TEXTURE_2D);
437 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
438 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
439 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
440 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
441 } else { // Display options are restricted in WebGL for non-power-of-two textures
442 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
443 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
444 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
445 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
446 }
447 }
448
449 apply(unit = 0) {
450 gl.activeTexture(gl.TEXTURE0 + unit);
451 gl.bindTexture(gl.TEXTURE_2D, this.texture);
452 glShader.setUniform("uSampler" + unit, unit);
453 glShader.setUniform("uTextureWidth" + unit, this.width);
454 glShader.setUniform("uTextureHeight" + unit, this.height);
455 }
456
457 get isPowerOfTwo() {
458 return !(this.width & (this.width-1)) && !(this.height & (this.height-1));
459 }
460
461}
462
463//------------------------------------------------------------------------------------------
464//-- DepthStencilBuffer --------------------------------------------------------------------
465//------------------------------------------------------------------------------------------
466
467// Creates a buffer that will store both depth and stencil information.
468
469export class DepthStencilBuffer {
470 constructor(width = 1, height = 1) {
471 if (glVersion === 0) { console.log("This device does not support depth buffers!"); return; }
472 this.width = width;
473 this.height = height;
474 this.buffer = gl.createRenderbuffer();
475 gl.bindRenderbuffer(gl.RENDERBUFFER, this.buffer);
476 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height);
477 }
478
479 destroy() {
480 gl.deleteRenderbuffer(this.buffer);
481 }
482
483}
484
485//------------------------------------------------------------------------------------------
486//-- DepthTexture --------------------------------------------------------------------------
487//------------------------------------------------------------------------------------------
488
489// Stores depth information as a texture if you want to read from it later.
490// Only use the red channel of the texture to access the depth information.
491
492export class DepthTexture {
493 constructor(width = 1, height = 1) {
494 if (glVersion === 0) { console.log("This device does not support depth textures!"); return; }
495 this.texture = gl.createTexture();
496 this.width = width;
497 this.height = height;
498 gl.bindTexture(gl.TEXTURE_2D, this.texture);
499 if (glVersion === 1) {
500 gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
501 } else {
502 gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT16, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
503 }
504 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
505 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
506 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
507 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
508 }
509
510 destroy() {
511 gl.deleteTexture(this.texture);
512 }
513
514 apply(unit = 0) {
515 gl.activeTexture(gl.TEXTURE0 + unit);
516 gl.bindTexture(gl.TEXTURE_2D, this.texture);
517 glShader.setUniform("uSampler" + unit, unit);
518 glShader.setUniform("uTextureWidth" + unit, this.width);
519 glShader.setUniform("uTextureHeight" + unit, this.height);
520 }
521
522}
523
524//------------------------------------------------------------------------------------------
525//-- FloatTexture --------------------------------------------------------------------------
526//------------------------------------------------------------------------------------------
527
528// A texture that stores RGBA values as floats instead of bytes. Used by the GeometryBuffer
529// for diffuse/normal/position data.
530
531// BUG -- There seems to be an error in Safari on Macs. They use WebGL 1 with the OEM float texture
532// extension, but somehow this is throwing an error? Might be that the texture width/height is fractional
533// because of the scaling used in the AO buffer? Or using the wrong enum value for the internal format?
534// Needs experimentation.
535
536export class FloatTexture {
537 constructor(width = 1, height = 1) {
538 if (glVersion === 0) { console.log("This device does not support float textures!"); return; }
539 this.texture = gl.createTexture();
540 this.width = width;
541 this.height = height;
542 gl.bindTexture(gl.TEXTURE_2D, this.texture);
543 if (glVersion === 1) {
544 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null);
545 } else {
546 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, null);
547 }
548 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
549 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
550 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
551 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
552 }
553
554 destroy() {
555 gl.deleteTexture(this.texture);
556 }
557
558 apply(unit = 0) {
559 gl.activeTexture(gl.TEXTURE0 + unit);
560 gl.bindTexture(gl.TEXTURE_2D, this.texture);
561 glShader.setUniform("uSampler" + unit, unit);
562 glShader.setUniform("uTextureWidth" + unit, this.width);
563 glShader.setUniform("uTextureHeight" + unit, this.height);
564 }
565
566}
567
568//------------------------------------------------------------------------------------------
569//-- TextureTable --------------------------------------------------------------------------
570//------------------------------------------------------------------------------------------
571
572// A special texture for storing vector4 values as RGBA in a float texture. Unlike normal
573// FloatTextures, TextureTables do not interpolate between values when sampled. Used to pass
574// extra data into shaders.
575//
576// The source is an array of vector4: source = [[r0, g0, b0, a0], [r1, g1, b1, a1], ...]
577
578export class TextureTable {
579 constructor(width, height, source) {
580 if (glVersion === 0) { console.log("This device does not support texture tables!"); return; }
581
582 this.texture = gl.createTexture();
583 this.width = width;
584 this.height = height;
585 gl.bindTexture(gl.TEXTURE_2D, this.texture);
586
587 if (glVersion === 1) {
588 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, new Float32Array(source.flat()));
589 } else {
590 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, new Float32Array(source.flat()));
591 }
592
593 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
594 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
595 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
596 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
597 }
598
599 destroy() {
600 gl.deleteTexture(this.texture);
601 }
602
603 apply(unit = 0) {
604 gl.activeTexture(gl.TEXTURE0 + unit);
605 gl.bindTexture(gl.TEXTURE_2D, this.texture);
606 glShader.setUniform("uSampler" + unit, unit);
607 glShader.setUniform("uTextureWidth" + unit, this.width);
608 glShader.setUniform("uTextureHeight" + unit, this.height);
609 }
610
611}
612
613//------------------------------------------------------------------------------------------
614//-- Material ------------------------------------------------------------------------------
615//------------------------------------------------------------------------------------------
616
617// Materials are used to organize draw calls into passes. They hold configuration information
618// about how meshes should be rendered.
619
620export class Material {
621
622 constructor() {
623 this.pass = 'opaque';
624 this.texture = new Texture();
625 this.texture.loadFromByteArray(1,1, new Uint8Array([255, 255, 255, 255]));
626 this.decal = new Texture();
627 this.decal.loadFromByteArray(1,1, new Uint8Array([0, 0, 0, 0]));
628 this.zOffset = 1;
629 }
630
631 destroy() {
632 this.texture.destroy();
633 this.decal.destroy();
634 }
635
636 apply() {
637 gl.polygonOffset(this.zOffset, 0);
638 this.texture.apply(0);
639 this.decal.apply(1);
640 }
641
642}
643
644//------------------------------------------------------------------------------------------
645//-- Scene ---------------------------------------------------------------------------------
646//------------------------------------------------------------------------------------------
647
648// The scene holds lights and camera and lists of draw calls organized by rendering pass (opaque, translucent, etc).
649// Right now the draw calls aren't sorted by material/mesh, but eventually they probably should be
650// to minimize context changes.
651//
652// In particular, translucent draw calls need to be sorted back to front! Overlapping translucent
653// polys won't render properly until that is done.
654
655export class Scene {
656 constructor() {
657 this.background = [0,0,0,1];
658 this.lights = new Lights();
659 this.camera = new Camera();
660 this.passLists = new Map();
661 }
662
663 addDrawCall(drawCall) {
664 if (!this.passLists.has(drawCall.material.pass)) this.passLists.set(drawCall.material.pass, new Set());
665 const passList = this.passLists.get(drawCall.material.pass);
666 passList.add(drawCall);
667 }
668
669 removeDrawCall(drawCall) {
670 const passList = this.passLists.get(drawCall.material.pass);
671 if (!passList) return;
672 passList.delete(drawCall);
673 }
674
675 drawPass(pass) {
676 const passList = this.passLists.get(pass);
677 if (passList) passList.forEach(call => call.draw());
678 }
679}
680
681//------------------------------------------------------------------------------------------
682//-- InstancedDrawCall ---------------------------------------------------------------------
683//------------------------------------------------------------------------------------------
684
685// A drawcall is just an object holding a mesh, a material and a transform.
686// Don't change the material after you've added the call to a scene.
687// Right now scenes don't sort draw calls beyond grouping them by pass.
688
689export class InstancedDrawCall {
690 constructor(mesh, material) {
691 this.mesh = mesh;
692 this.material = material;
693 this.instances = new Instances();
694 }
695
696 destroy() {
697 this.instances.destroy();
698 }
699
700 draw() {
701 if (this.isHidden || this.instances.count === 0) return;
702 if (glVersion === 0) { // Fallback for version that doesn't support instancing
703 if (this.instances.mapChanged) this.instances.rebuild();
704 if (this.instances.count > 0) {
705 this.material.texture.apply(0);
706 this.material.decal.apply(1);
707 for (let i = 0; i < this.instances.count; i++) {
708 this.instances.applyOne(i);
709 this.mesh.draw();
710 }
711 }
712 } else if (this.instances.count > 0) {
713 this.material.texture.apply(0);
714 this.material.decal.apply(1);
715 this.instances.apply();
716 this.mesh.drawInstanced(this.instances.count);
717 for (let i = 0; i < glAttributes; i++) { // Not all GL implementations automatically reset the attribute divisor.
718 if (glVersion === 2) {
719 gl.vertexAttribDivisor(i,0); // Be sure to set this back to zero after you're done or it will break rendering on Android.
720 } else {
721 instancedArraysExt.vertexAttribDivisorANGLE(i,0);
722 }
723 }
724 }
725
726 }
727}
728
729//------------------------------------------------------------------------------------------
730//-- DrawCall ------------------------------------------------------------------------------
731//------------------------------------------------------------------------------------------
732
733// A drawcall is just an object holding a mesh, a material and a transform.
734// Don't change the material after you've added the call to a scene.
735// Right now scenes don't sort draw calls beyond grouping them by pass.
736
737export class DrawCall {
738 constructor(mesh, material = new Material()) {
739 this.mesh = mesh;
740 this.material = material;
741 this.transform = new Transform();
742 }
743
744 draw() {
745 if (this.isHidden) return;
746 this.material.apply();
747 this.transform.apply();
748 this.mesh.draw();
749 }
750}
751
752//------------------------------------------------------------------------------------------
753//-- Transform -----------------------------------------------------------------------------
754//------------------------------------------------------------------------------------------
755
756// Holds the local model transform. Lazily generates a normal transform matrix.
757
758export class Transform {
759
760 constructor() {
761 this.meshMatrix = m4_identity();
762 }
763
764 set(m) {
765 this.meshMatrix = m;
766 this._normalMatrix = null;
767 }
768
769 get normalMatrix() {
770 this._normalMatrix = this._normalMatrix || m4_toNormal4(this.meshMatrix);
771 return this._normalMatrix;
772 }
773
774 apply() { // Concatenates the view transform before applying
775
776 glShader.setUniform("uMeshMatrix", this.meshMatrix);
777 glShader.setUniform("uNormalMatrix", this.normalMatrix);
778
779 if (glShader.hasUniform("uMeshToViewMatrix")) glShader.setUniform("uMeshToViewMatrix", m4_multiply(this.meshMatrix, glCamera.viewMatrix));
780 if (glShader.hasUniform("uNormalToViewMatrix")) glShader.setUniform("uNormalToViewMatrix", m4_multiply(this.normalMatrix, glCamera.viewNormalMatrix));
781 if (glShader.hasUniform("uMeshToScreenMatrix")) glShader.setUniform("uMeshToScreenMatrix", m4_multiply(this.meshMatrix, glCamera.w2vMatrix));
782 }
783}
784
785//------------------------------------------------------------------------------------------
786//-- Instances -----------------------------------------------------------------------------
787//------------------------------------------------------------------------------------------
788
789export class Instances {
790 constructor() {
791 this.clear();
792 this.iBuffer = new GLMatrixBuffer('aMeshMatrix');
793 this.nBuffer = new GLMatrixBuffer('aNormalMatrix');
794 }
795
796 destroy() {
797 this.iBuffer.destroy();
798 this.nBuffer.destroy();
799 }
800
801 clear() {
802 this.count = 0;
803 this.entries = null;
804 this.matrices = [];
805 this.normals = [];
806 }
807
808 set(key, m, n) {
809 n = n || m4_toNormal4(m);
810 if (!this.entries) this.entries = new Map();
811 if (!this.entries.has(key)) this.entries.set(key, this.count++);
812 const offset = 16 * this.entries.get(key);
813 this.matrices.splice(offset, 16, ...m);
814 this.normals.splice(offset, 16, ...n);
815 this.valueChanged = true;
816 }
817
818 delete(key) {
819 if (this.entries) this.entries.delete(key);
820 this.mapChanged = true;
821 this.valueChanged = true;
822 }
823
824 rebuild() {
825 const oldEntries = this.entries;
826 const oldMatrices = this.matrices;
827 const oldNormals = this.normals;
828 this.clear();
829 oldEntries.forEach((i,key) => {
830 const offset = 16 * i;
831 const m = oldMatrices.slice(offset, offset + 16);
832 const n = oldNormals.slice(offset, offset + 16);
833 this.set(key,m,n);
834 });
835 this.mapChanged = false;
836 }
837
838 load() {
839 if (this.mapChanged) this.rebuild();
840 if (this.count > 0) {
841 this.iBuffer.load(this.matrices);
842 this.nBuffer.load(this.normals);
843 }
844 this.valueChanged = false;
845 }
846
847 apply() {
848 if (this.valueChanged) this.load();
849 if (this.count > 0) {
850 this.iBuffer.apply();
851 this.nBuffer.apply();
852 }
853 }
854
855 // Fallback rountines
856 //
857 // These exist to let the renderer use the information in an instance buffer even if it doesn't support instancing.
858
859 getMeshMatrix(i) {
860 const offset = 16 * i;
861 return this.matrices.slice(offset, offset + 16);
862 }
863
864 getNormalMatrix(i) {
865 const offset = 16 * i;
866 return this.normals.slice(offset, offset + 16);
867 }
868
869 applyOne(i) {
870 const m = this.getMeshMatrix(i);
871 const n = this.getNormalMatrix(i);
872 glShader.setUniform("uMeshMatrix", m);
873 glShader.setUniform("uNormalMatrix", n);
874
875 if (glShader.hasUniform("uMeshToViewMatrix")) glShader.setUniform("uMeshToViewMatrix", m4_multiply(m, glCamera.viewMatrix));
876 if (glShader.hasUniform("uNormalToViewMatrix")) glShader.setUniform("uNormalToViewMatrix", m4_multiply(n, glCamera.viewNormalMatrix));
877 if (glShader.hasUniform("uMeshToScreenMatrix")) glShader.setUniform("uMeshToScreenMatrix", m4_multiply(m, glCamera.w2vMatrix));
878 }
879
880}
881
882//------------------------------------------------------------------------------------------
883//-- Mesh ----------------------------------------------------------------------------
884//------------------------------------------------------------------------------------------
885
886// Mesh is a base class that handles glBuffer management and common types of data
887// manipulation for the different types of draw primitives
888
889class Mesh {
890 constructor(template) {
891 this.clear();
892 if (template) this.copy(template);
893 this.vertexBuffer = new GLBuffer('aVertex');
894 this.normalBuffer = new GLBuffer('aNormal');
895 this.colorBuffer = new GLBuffer('aColor');
896 this.coordinateBuffer = new GLBuffer('aCoordinate');
897 }
898
899 get vertexCount() { return this.vertices.length / 3;}
900
901 destroy() {
902 this.vertexBuffer.destroy();
903 this.normalBuffer.destroy();
904 this.colorBuffer.destroy();
905 this.coordinateBuffer.destroy();
906 }
907
908 // Deletes the local buffers, but not the glBuffers.
909 clear() {
910 this.vertices = [];
911 this.normals = [];
912 this.colors = [];
913 this.coordinates = [];
914 }
915
916 // Copies another mesh into this one. Does not copy the glBuffers, so you'll probably want to do a load immediately after
917 copy(source) {
918 this.vertices = source.vertices.slice();
919 this.normals = source.normals.slice();
920 this.colors = source.colors.slice();
921 this.coordinates = source.coordinates.slice();
922 }
923
924 // Merges another mesh with this one. Does not merge the glBuffers, so you'll probably want to do a load immediately after
925 merge(source) {
926 this.vertices = this.vertices.concat(source.vertices);
927 this.normals = this.normals.concat(source.normals);
928 this.colors = this.colors.concat(source.colors);
929 this.coordinates = this.coordinates.concat(source.coordinates);
930 }
931
932 // Transforms all the vertices and normals using a 4x4 matrix.
933 transform(m4) {
934 const vertices = this.vertices;
935 for (let i = 0; i < this.vertices.length; i+=3) {
936 const v0 = [vertices[i], vertices[i+1], vertices[i+2]];
937 const v1 = v3_transform(v0,m4);
938 vertices[i] = v1[0];
939 vertices[i+1] = v1[1];
940 vertices[i+2] = v1[2];
941 }
942 if (this.normals.length === 0) return;
943 const nm4 = m4_toNormal4(m4);
944 const normals = this.normals;
945 for (let i = 0; i < this.normals.length; i+=3) {
946 const n0 = [normals[i], normals[i+1], normals[i+2]];
947 const n1 = v3_normalize(v3_transform(n0, nm4));
948 normals[i] = n1[0];
949 normals[i+1] = n1[1];
950 normals[i+2] = n1[2];
951 }
952 }
953
954 setColor(color) {
955 this.colors = [];
956 const vertexCount = this.vertexCount;
957 for (let i = 0; i < vertexCount; i++) {
958 this.colors.push(...color);
959 }
960 }
961
962 // Loads the local buffers into the glBuffers
963 load() {
964 this.vertexBuffer.load(this.vertices);
965 this.normalBuffer.load(this.normals);
966 this.colorBuffer.load(this.colors);
967 this.coordinateBuffer.load(this.coordinates);
968 this.saveDrawCount();
969 }
970
971 saveDrawCount() {
972 this.drawCount = this.vertices.length / 3;
973 }
974
975 apply() {
976 this.vertexBuffer.apply();
977 this.normalBuffer.apply();
978 this.colorBuffer.apply();
979 this.coordinateBuffer.apply();
980 }
981
982 findNormal(v0, v1, v2) {
983 const d0 = v3_sub(v1, v0);
984 const d1 = v3_sub(v2, v0);
985 return v3_normalize(v3_cross(d1, d0));
986 }
987
988}
989
990//------------------------------------------------------------------------------------------
991//-- Triangles -----------------------------------------------------------------------------
992//------------------------------------------------------------------------------------------
993
994export class Triangles extends Mesh {
995
996 draw() {
997 if (this.drawCount === 0) return;
998 this.apply();
999 gl.drawArrays(gl.TRIANGLES, 0, this.drawCount);
1000 }
1001
1002 drawInstanced(instanceCount) {
1003 if (instanceCount === 0 || this.drawCount === 0) return;
1004 this.apply();
1005 if (glVersion === 2) {
1006 gl.drawArraysInstanced(gl.TRIANGLES, 0, this.drawCount, instanceCount);
1007 // const err = gl.getError();
1008 // console.log("Draw Instanced " + err);
1009 } else {
1010 instancedArraysExt.drawArraysInstancedANGLE(gl.TRIANGLES, 0, this.drawCount, instanceCount);
1011 }
1012 }
1013
1014 //-- Builder methods --
1015
1016 // The vertices are ordered CCW around the perimeter of the face. Vertex0 is shared by all triangles in the face.
1017 addFace(vertices, colors, coordinates, normals) {
1018 const triCount = vertices.length - 2;
1019 if (triCount < 1) return;
1020
1021 // if (!normals) {
1022 // normals = [];
1023 // const n = this.findNormal(vertices[0], vertices[1], vertices[2]);
1024 // for (let i = 0; i < vertices.length; i++) normals.push(n);
1025 // }
1026
1027 for (let i = 0; i < triCount; i++) {
1028
1029 let n = this.findNormal(vertices[0], vertices[i+1], vertices[i+2]);
1030
1031 //-- Vertex A--
1032
1033 this.vertices.push(...vertices[0]);
1034 if (normals) { this.normals.push(...normals[0]); } else { this.normals.push(...n) };
1035 if (colors) this.colors.push(...colors[0]);
1036 if (coordinates) this.coordinates.push(...coordinates[0]);
1037
1038 //-- Vertex B --
1039
1040 this.vertices.push(...vertices[i+1]);
1041 if (normals) { this.normals.push(...normals[i+1]) } else { this.normals.push(...n) };
1042 if (colors) this.colors.push(...colors[i+1]);
1043 if (coordinates) this.coordinates.push(...coordinates[i+1]);
1044
1045 //-- Vertex C --
1046
1047 this.vertices.push(...vertices[i+2]);
1048 if (normals) { this.normals.push(...normals[i+2]) } else { this.normals.push(...n) };
1049 if (colors) this.colors.push(...colors[i+2]);
1050 if (coordinates) this.coordinates.push(...coordinates[i+2]);
1051 }
1052 }
1053
1054}
1055
1056//------------------------------------------------------------------------------------------
1057//-- Lines ---------------------------------------------------------------------------------
1058//------------------------------------------------------------------------------------------
1059
1060export class Lines extends Mesh {
1061
1062 draw() {
1063 if (this.drawCount === 0) return;
1064 this.apply();
1065 gl.drawArrays(gl.LINES, 0, this.drawCount);
1066 }
1067
1068 drawInstanced(instanceCount) {
1069 if (instanceCount === 0 || this.drawCount === 0) return;
1070 this.apply();
1071 if (glVersion === 2) {
1072 gl.drawArraysInstanced(gl.LINES, 0, this.drawCount, instanceCount);
1073 } else {
1074 instancedArraysExt.drawArraysInstancedANGLE(gl.LINES, 0, this.drawCount, instanceCount);
1075 }
1076 }
1077
1078 saveDrawCount() {
1079 this.drawCount = this.vertices.length / 3;
1080 }
1081
1082 //-- Builder methods --
1083
1084 // Just a colored line with no lighting info
1085 addDebugLine(start, end, color) {
1086 this.vertices.push(...start);
1087 this.vertices.push(...end);
1088 if (color) {
1089 this.colors.push(...color);
1090 this.colors.push(...color);
1091 }
1092 this.normals.push(...[0,0,0]);
1093 this.normals.push(...[0,0,0]);
1094 this.coordinates.push(...[0,0]);
1095 this.coordinates.push(...[0,0]);
1096 }
1097
1098 addLine(vertices, colors, coordinates, normals) {
1099 const lineCount = vertices.length-1;
1100
1101 for (let i = 0; i < lineCount; i++) {
1102
1103 //-- Vertex A--
1104
1105 this.vertices.push(...vertices[i]);
1106 if (normals) this.normals.push(...normals[i]);
1107 if (colors) this.colors.push(...colors[i]);
1108 if (coordinates) this.coordinates.push(...coordinates[i]);
1109
1110 //-- Vertex B --
1111
1112 const b = (i+1);
1113 this.vertices.push(...vertices[b]);
1114 if (normals) this.normals.push(...normals[b]);
1115 if (colors) this.colors.push(...colors[b]);
1116 if (coordinates) this.coordinates.push(...coordinates[b]);
1117
1118 }
1119 }
1120
1121 addFace(vertices, colors, coordinates, normals) {
1122 const lineCount = vertices.length;
1123 if (lineCount < 3) return;
1124
1125 if (!normals) {
1126 normals = [];
1127 const n = this.findNormal(vertices[0], vertices[1], vertices[2]);
1128 for (let i = 0; i < vertices.length; i++) normals.push(n);
1129 }
1130
1131 for (let i = 0; i < lineCount; i++) {
1132
1133 //-- Vertex A--
1134
1135 this.vertices.push(...vertices[i]);
1136 if (normals) this.normals.push(...normals[i]);
1137 if (colors) this.colors.push(...colors[i]);
1138 if (coordinates) this.coordinates.push(...coordinates[i]);
1139
1140 //-- Vertex B --
1141
1142 const b = (i+1) % lineCount;
1143 this.vertices.push(...vertices[b]);
1144 if (normals) this.normals.push(...normals[b]);
1145 if (colors) this.colors.push(...colors[b]);
1146 if (coordinates) this.coordinates.push(...coordinates[b]);
1147
1148 }
1149
1150 }
1151}
1152
1153
1154//------------------------------------------------------------------------------------------
1155// -- GLBuffer -----------------------------------------------------------------------------
1156//------------------------------------------------------------------------------------------
1157
1158// Low-level interface to the shader. GLBuffers know how to lead themselves onto the graphics
1159// card and apply themselves during rendering. They hold array data for the renderer like
1160// lists of vertices, normals, colors, and texture coordinates.
1161
1162class GLBuffer {
1163 constructor(attribute, mode = 'dynamicDraw') {
1164 this.attribute = attribute;
1165 switch (mode) {
1166 case 'staticDraw':
1167 this.mode = gl.STATIC_DRAW;
1168 break;
1169 case 'dynamicDraw':
1170 default:
1171 this.mode = gl.DYNAMIC_DRAW;
1172 }
1173 }
1174
1175 destroy() {
1176 if (this.buffer) gl.deleteBuffer(this.buffer);
1177 }
1178
1179 load(values) {
1180 if (values.length === 0) {
1181 if (this.buffer) gl.deleteBuffer(this.buffer);
1182 this.buffer = null;
1183 return;
1184 }
1185 if (!this.buffer) this.buffer = gl.createBuffer();
1186 gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1187 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(values), this.mode);
1188 }
1189
1190 apply() {
1191 if (this.buffer) glShader.setAttribute(this.attribute, this.buffer);
1192 }
1193
1194}
1195
1196//------------------------------------------------------------------------------------------
1197// -- GLMatrixBuffer -----------------------------------------------------------------------
1198//------------------------------------------------------------------------------------------
1199
1200// Low-level interface to the shader. GLBuffers know how to lead themselves onto the graphics
1201// card and apply themselves during rendering. They hold 4x4 matrix data.
1202
1203class GLMatrixBuffer {
1204 constructor(attribute, mode = 'dynamicDraw') {
1205 this.attribute = attribute;
1206 switch (mode) {
1207 case 'staticDraw':
1208 this.mode = gl.STATIC_DRAW;
1209 break;
1210 case 'dynamicDraw':
1211 default:
1212 this.mode = gl.DYNAMIC_DRAW;
1213 }
1214 }
1215
1216 destroy() {
1217 if (this.buffer) gl.deleteBuffer(this.buffer);
1218 }
1219
1220 load(values) {
1221 if (values.length === 0) {
1222 if (this.buffer) gl.deleteBuffer(this.buffer);
1223 this.buffer = null;
1224 return;
1225 }
1226 if (!this.buffer) this.buffer = gl.createBuffer();
1227 gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1228 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(values), this.mode);
1229 }
1230
1231 apply() {
1232 if (this.buffer) glShader.setMatrixAttribute(this.attribute, this.buffer);
1233 }
1234
1235}
1236
1237//------------------------------------------------------------------------------------------
1238//-- Lights --------------------------------------------------------------------------------
1239//------------------------------------------------------------------------------------------
1240
1241// Default lighting for a scene is an ambient value and one directional light.
1242
1243export class Lights {
1244 constructor() {
1245 this.ambientColor = [0.7, 0.7, 0.7];
1246 this.directionalColor = [0.3, 0.3, 0.3];
1247 this.directionalAim = [0,-1,0];
1248 }
1249
1250 apply() {
1251 glShader.setUniform("uAmbientColor", this.ambientColor);
1252 glShader.setUniform("uDirectionalColor", this.directionalColor);
1253 glShader.setUniform("uDirectionalAim", this.directionalAim);
1254 }
1255
1256 setAmbientColor(color) {
1257 this.ambientColor = color;
1258 }
1259
1260 setDirectionalColor(color) {
1261 this.directionalColor = color;
1262 }
1263
1264 setDirectionalAim(aim) {
1265 this.directionalAim = v3_normalize(aim);
1266 }
1267}
1268
1269
1270//------------------------------------------------------------------------------------------
1271//-- Camera --------------------------------------------------------------------------------
1272//------------------------------------------------------------------------------------------
1273
1274export class Camera {
1275 constructor() {
1276 this.locationMatrix = m4_identity();
1277 this.fov = toRad(60);
1278 this.near = 1;
1279 this.far = 10000;
1280 }
1281
1282 setFOV(fov) {
1283 this.fov = fov;
1284 this._projectionMatrix = null;
1285 this._v2wMatrix = null;
1286 this._w2vMatrix = null;
1287 glPipeline.updateFOV();
1288 }
1289
1290 setProjection(fov, near, far) {
1291 this.fov = fov;
1292 this.near = near;
1293 this.far = far;
1294 this._projectionMatrix = null;
1295 this._v2wMatrix = null;
1296 this._w2vMatrix = null;
1297 }
1298
1299 setAspect(width, height) {
1300 this.width = width;
1301 this.height = height;
1302 this.aspect = this.width / this.height;
1303 this._projectionMatrix = null;
1304 this._v2wMatrix = null;
1305 this._w2vMatrix = null;
1306 }
1307
1308 setLocation(m) {
1309 this.locationMatrix = m;
1310 this._viewMatrix = null;
1311 this._viewNormalMatrix = null;
1312 this._v2wMatrix = null;
1313 this._w2vMatrix = null;
1314 }
1315
1316 get location() {
1317 return m4_getTranslation(this.locationMatrix);
1318 }
1319
1320 get projectionMatrix() {
1321 if (!this._projectionMatrix) this._projectionMatrix = m4_perspective(this.fov, this.aspect, this.near, this.far);
1322 return this._projectionMatrix;
1323 }
1324
1325 get viewMatrix() {
1326 if (!this._viewMatrix) this._viewMatrix = m4_invert(this.locationMatrix);
1327 return this._viewMatrix;
1328 }
1329
1330 get viewNormalMatrix() { //This may cause lighting issues if the camera has a scale transform because the shaders don't renormalize
1331 if (!this._viewNormalMatrix) this._viewNormalMatrix = m4_transpose(this.locationMatrix);
1332 return this._viewNormalMatrix;
1333 }
1334
1335 get v2wMatrix() {
1336 if (!this._v2wMatrix) this._v2wMatrix = m4_invert(this.w2vMatrix);
1337 return this._v2wMatrix;
1338 }
1339
1340 get w2vMatrix() {
1341 if (!this._w2vMatrix) this._w2vMatrix = m4_multiply(this.viewMatrix, this.projectionMatrix);
1342 return this._w2vMatrix;
1343 }
1344
1345 apply() {
1346 glShader.setUniform("uViewMatrix", this.viewMatrix); // Transforms world to view space
1347 glShader.setUniform("uViewNormalMatrix", this.viewNormalMatrix); // Transforms normals from world to view space
1348 glShader.setUniform("uProjectionMatrix", this.projectionMatrix); // Projects view space to screen
1349 glShader.setUniform("uCameraMatrix", this.w2vMatrix); // Combined view and projection matrix
1350 }
1351
1352 worldToView(v3) {
1353 const v = v3_transform(v3, this.w2vMatrix);
1354 return [(v[0] + 1.0) * this.width * 0.5, (v[1] + 1.0) * this.height * 0.5];
1355 }
1356
1357 clippedWorldToView(v3) {
1358 const v2 = this.worldToView(v3);
1359 v2[0] = Math.max(v2[0], 0);
1360 v2[0] = Math.min(v2[0], this.width);
1361 v2[1] = Math.max(v2[1], 0);
1362 v2[1] = Math.min(v2[1], this.height);
1363 return v2;
1364 }
1365
1366 viewToWorld(v3) {
1367 return v3_transform(v3, this.v2wMatrix);
1368 }
1369
1370 // Returns a normalized ray from the camera position that passes through the specified position
1371 // in the view. The view position ranges from -1 to 1 in both the x and y
1372 // directions. The default [0,0] is the direction the camera is pointing.
1373
1374 lookRay(x = 0, y = 0) {
1375 const v0 = [x,y,-1];
1376 const v1 = [x,y,1];
1377 const ray = v3_sub(this.viewToWorld(v1), this.viewToWorld(v0));
1378 return v3_normalize(ray);
1379 }
1380
1381 // Like normal lookRay, but takes view x,y pixel coordinates as its inputs.
1382
1383 viewLookRay(x, y) {
1384 x = (2.0 * x / this.width) - 1.0;
1385 y = 1.0 - (2.0 * y / this.height);
1386 return this.lookRay(x, y);
1387 }
1388
1389}
1390
1391//------------------------------------------------------------------------------------------
1392//-- Shader --------------------------------------------------------------------------------
1393//------------------------------------------------------------------------------------------
1394
1395// The base class that all shaders are derived from. It encapsulates compiling the vertex
1396// and fragment sources, and setting uniforms and attributes.
1397//
1398// Each shader is expected to have source for both WebGL and WebGL2.
1399
1400export class Shader {
1401
1402 constructor() {
1403 this.program = null;
1404 this.attributes = new Map();
1405 this.uniforms = new Map();
1406 }
1407
1408 destroy() {
1409 gl.deleteProgram(this.program);
1410 }
1411
1412 apply() {
1413
1414 gl.useProgram(this.program);
1415 glShader = this;
1416 for (let i = 0; i < 8; i++) gl.disableVertexAttribArray(i); // Disable previous buffers
1417 }
1418
1419 build(vSource, fSource, vSource2, fSource2) {
1420 // Load and compile the vertex and fragment shaders
1421 let vertexShader;
1422 let fragmentShader;
1423 if (glVersion < 2) {
1424 vertexShader = this.compile(gl.VERTEX_SHADER, vSource);
1425 fragmentShader = this.compile(gl.FRAGMENT_SHADER, fSource);
1426 } else {
1427 vertexShader = this.compile(gl.VERTEX_SHADER, vSource2);
1428 fragmentShader = this.compile(gl.FRAGMENT_SHADER, fSource2);
1429 }
1430
1431 // Create the shader program
1432 this.program = gl.createProgram();
1433 gl.attachShader(this.program, vertexShader);
1434 gl.attachShader(this.program, fragmentShader);
1435 gl.linkProgram(this.program);
1436 gl.deleteShader(vertexShader);
1437 gl.deleteShader(fragmentShader);
1438
1439 if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { // Report if creating shader program failed
1440 console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(this.program));
1441 gl.deleteProgram(this.program);
1442 this.program = null;
1443 return;
1444 }
1445
1446 this.findAttributes();
1447 this.findUniforms();
1448
1449 }
1450
1451 compile(type, source) {
1452 const shader = gl.createShader(type);
1453 gl.shaderSource(shader, source);
1454 gl.compileShader(shader);
1455
1456 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { // Report if compiling shader failed
1457 console.error('An error occurred compiling the shader: ' + gl.getShaderInfoLog(shader));
1458 gl.deleteShader(shader);
1459 return null;
1460 }
1461 return shader;
1462 }
1463
1464 findAttributes() {
1465 this.attributes.clear();
1466 const n = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
1467 for (let i = 0; i < n; ++i) {
1468 const info = gl.getActiveAttrib(this.program, i);
1469 const location = gl.getAttribLocation(this.program, info.name);
1470 const name = info.name.replace("[0]", "");
1471 let size = 1;
1472 const type = gl.FLOAT;
1473 switch (info.type) {
1474 case gl.FLOAT:
1475 size = 1;
1476 break;
1477 case gl.FLOAT_VEC2:
1478 size = 2;
1479 break;
1480 case gl.FLOAT_VEC3:
1481 size = 3;
1482 break;
1483 case gl.FLOAT_VEC4:
1484 size = 4;
1485 break;
1486 case gl.FLOAT_MAT4:
1487 size = 16;
1488 break;
1489 default:
1490 console.log("Unrecognized attribute type!");
1491 return;
1492 }
1493 this.attributes.set(name, {
1494 type,
1495 size,
1496 location
1497 });
1498 }
1499 }
1500
1501 findUniforms() {
1502 this.uniforms.clear();
1503 const n = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
1504 for (let i = 0; i < n; ++i) {
1505 const info = gl.getActiveUniform(this.program, i);
1506 const location = gl.getUniformLocation(this.program, info.name);
1507 const name = info.name.replace("[0]", "");
1508 this.uniforms.set(name, {
1509 type: info.type,
1510 size: info.size,
1511 location
1512 });
1513 }
1514 }
1515
1516 setAttribute(name, buffer) {
1517 const attribute = this.attributes.get(name);
1518 if (!attribute) return;
1519 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1520 gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, false, 0, 0);
1521 gl.enableVertexAttribArray(attribute.location);
1522 }
1523
1524 // Sets 4x4 matrix attributer as four 4 vectors
1525 setMatrixAttribute(name, buffer) {
1526 if (!this.attributes.has(name)) return;
1527 const attribute = this.attributes.get(name);
1528 const stride = 64;
1529 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1530 for (let i = 0; i < 4; i++) {
1531 const location = attribute.location + i;
1532 const offset = i * 16;
1533 gl.vertexAttribPointer(location, 4, attribute.type, false, stride, offset);
1534 gl.enableVertexAttribArray(location);
1535 if (glVersion === 2) {
1536 gl.vertexAttribDivisor(location, 1); // Be sure to set this back to zero after you're done or it will break rendering on Android.
1537 } else {
1538 instancedArraysExt.vertexAttribDivisorANGLE(location, 1);
1539 }
1540 }
1541 }
1542
1543 hasUniform(name) {
1544 return this.uniforms.has(name);
1545 }
1546
1547 setUniform(name, value) {
1548 const uniform = this.uniforms.get(name);
1549 if (!uniform) return;
1550 let v;
1551 if (value.length) {
1552 v = value.flat();
1553 } else {
1554 v = [value];
1555 }
1556 switch (uniform.type) {
1557 case gl.INT:
1558 case gl.BOOL:
1559 case gl.SAMPLER_2D:
1560 gl.uniform1iv(uniform.location, v);
1561 break;
1562 case gl.INT_VEC2:
1563 case gl.BOOL_VEC2:
1564 gl.uniform2iv(uniform.location, v);
1565 break;
1566 case gl.INT_VEC3:
1567 case gl.BOOL_VEC3:
1568 gl.uniform3iv(uniform.location, v);
1569 break;
1570 case gl.INT_VEC4:
1571 case gl.BOOL_VEC4:
1572 gl.uniform4iv(uniform.location, v);
1573 break;
1574 case gl.FLOAT:
1575 gl.uniform1fv(uniform.location, v);
1576 break;
1577 case gl.FLOAT_VEC2:
1578 gl.uniform2fv(uniform.location, v);
1579 break;
1580 case gl.FLOAT_VEC3:
1581 gl.uniform3fv(uniform.location, v);
1582 break;
1583 case gl.FLOAT_VEC4:
1584 gl.uniform4fv(uniform.location, v);
1585 break;
1586 case gl.FLOAT_MAT2:
1587 gl.uniformMatrix2fv(uniform.location, false, v);
1588 break;
1589 case gl.FLOAT_MAT3:
1590 gl.uniformMatrix3fv(uniform.location, false, v);
1591 break;
1592 case gl.FLOAT_MAT4:
1593 gl.uniformMatrix4fv(uniform.location, false, v);
1594 break;
1595 default:
1596 console.log("Unrecognized uniform type!");
1597 }
1598 }
1599
1600 get gl() { return gl; }
1601
1602}
1603
1604//------------------------------------------------------------------------------------------
1605//--Geometric Primitives -------------------------------------------------------------------
1606//------------------------------------------------------------------------------------------
1607
1608export function UnitCube() {
1609 const cube = new Triangles();
1610 cube.addFace([[-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5], [0.5, -0.5, -0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1611 cube.addFace([[-0.5, -0.5, -0.5], [-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1612 cube.addFace([[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1613 cube.addFace([[0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, 0.5, 0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1614 cube.addFace([[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1615 cube.addFace([[0.5, 0.5, 0.5], [0.5, -0.5, 0.5], [0.5, -0.5, -0.5], [0.5, 0.5, -0.5]], [[1,1,1,1], [1,1,1,1], [1,1,1,1], [1,1,1,1]], [[0,0], [1,0], [1,1], [0,1]]);
1616 return cube;
1617}
1618
1619export function Cube(x, y, z, color = [1,1,1,1]) {
1620 const cube = new Triangles();
1621 x /= 2;
1622 y /= 2;
1623 z /= 2;
1624 cube.addFace([[-x, -y, -z], [-x, y, -z], [x, y, -z], [x, -y, -z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1625 cube.addFace([[-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1626 cube.addFace([[-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1627 cube.addFace([[x, y, z], [x, y, -z], [-x, y, -z], [-x, y, z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1628 cube.addFace([[x, y, z], [-x, y, z], [-x, -y, z], [x, -y, z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1629 cube.addFace([[x, y, z], [x, -y, z], [x, -y, -z], [x, y, -z]], [color, color, color, color], [[0,0], [1,0], [1,1], [0,1]]);
1630 return cube;
1631}
1632
1633export function Sphere(r, facets, c = [1,1,1,1]) {
1634 const sphere = new Triangles();
1635 const ff = 1/facets;
1636
1637 //-- -X --
1638
1639 for (let i = 0; i < facets; i++) {
1640 for (let j = 0; j < facets; j++) {
1641
1642 const p0 = v3_scale(Spherify([-1, -1 + 2*ff*i, -1 + 2*ff*j]),r);
1643 const p1 = v3_scale(Spherify([-1, -1 + 2*ff*i, -1 + 2*ff*(j+1)]),r);
1644 const p2 = v3_scale(Spherify([-1, -1 + 2*ff*(i+1), -1 + 2*ff*(j+1)]),r);
1645 const p3 = v3_scale(Spherify([-1, -1 + 2*ff*(i+1), -1 + 2*ff*j]),r);
1646
1647 const u0 = i * ff;
1648 const u1 = u0 + ff;
1649 const v0 = j * ff;
1650 const v1 = v0 + ff;
1651
1652 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1653 }
1654 }
1655
1656 //-- +X --
1657
1658 for (let i = 0; i < facets; i++) {
1659 for (let j = 0; j < facets; j++) {
1660
1661 const p0 = v3_scale(Spherify([1, -1 + 2*ff*i, -1 + 2*ff*j]),r);
1662 const p1 = v3_scale(Spherify([1, -1 + 2*ff*(i+1), -1 + 2*ff*j]),r);
1663 const p2 = v3_scale(Spherify([1, -1 + 2*ff*(i+1), -1 + 2*ff*(j+1)]),r);
1664 const p3 = v3_scale(Spherify([1, -1 + 2*ff*i, -1 + 2*ff*(j+1)]),r);
1665
1666 const u0 = i * ff;
1667 const u1 = u0 + ff;
1668 const v0 = j * ff;
1669 const v1 = v0 + ff;
1670
1671 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1672 }
1673 }
1674
1675 //-- -Y --
1676
1677 for (let i = 0; i < facets; i++) {
1678 for (let j = 0; j < facets; j++) {
1679
1680 const p0 = v3_scale(Spherify([-1 + 2*ff*i, -1 , -1 + 2*ff*j]),r);
1681 const p1 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1 , -1 + 2*ff*j]),r);
1682 const p2 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1, -1 + 2*ff*(j+1)]),r);
1683 const p3 = v3_scale(Spherify([-1 + 2*ff*i, -1, -1 + 2*ff*(j+1)]),r);
1684
1685 const u0 = i * ff;
1686 const u1 = u0 + ff;
1687 const v0 = j * ff;
1688 const v1 = v0 + ff;
1689
1690 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1691 }
1692 }
1693
1694 //-- +Y --
1695
1696 for (let i = 0; i < facets; i++) {
1697 for (let j = 0; j < facets; j++) {
1698
1699 const p0 = v3_scale(Spherify([-1 + 2*ff*i, 1, -1 + 2*ff*j]),r);
1700 const p1 = v3_scale(Spherify([-1 + 2*ff*i, 1, -1 + 2*ff*(j+1)]),r);
1701 const p2 = v3_scale(Spherify([-1 + 2*ff*(i+1), 1, -1 + 2*ff*(j+1)]),r);
1702 const p3 = v3_scale(Spherify([-1 + 2*ff*(i+1), 1, -1 + 2*ff*j]),r);
1703
1704 const u0 = i * ff;
1705 const u1 = u0 + ff;
1706 const v0 = j * ff;
1707 const v1 = v0 + ff;
1708
1709 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1710 }
1711 }
1712
1713 //-- -Z --
1714
1715 for (let i = 0; i < facets; i++) {
1716 for (let j = 0; j < facets; j++) {
1717
1718 const p0 = v3_scale(Spherify([-1 + 2*ff*i, -1 + 2*ff*j, -1]),r);
1719 const p1 = v3_scale(Spherify([-1 + 2*ff*i, -1 + 2*ff*(j+1), -1]),r);
1720 const p2 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1 + 2*ff*(j+1), -1]),r);
1721 const p3 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1 + 2*ff*j, -1]),r);
1722
1723 const u0 = i * ff;
1724 const u1 = u0 + ff;
1725 const v0 = j * ff;
1726 const v1 = v0 + ff;
1727
1728 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1729 }
1730 }
1731
1732 //-- +Z --
1733
1734 for (let i = 0; i < facets; i++) {
1735 for (let j = 0; j < facets; j++) {
1736
1737 const p0 = v3_scale(Spherify([-1 + 2*ff*i, -1 + 2*ff*j, 1]),r);
1738 const p1 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1 + 2*ff*j, 1]),r);
1739 const p2 = v3_scale(Spherify([-1 + 2*ff*(i+1), -1 + 2*ff*(j+1), 1]),r);
1740 const p3 = v3_scale(Spherify([-1 + 2*ff*i, -1 + 2*ff*(j+1), 1]),r);
1741
1742 const u0 = i * ff;
1743 const u1 = u0 + ff;
1744 const v0 = j * ff;
1745 const v1 = v0 + ff;
1746
1747 sphere.addFace([p0, p1, p2, p3], [c, c, c, c], [[u0,v0], [u1,v0], [u1,v1], [u0,v1]]);
1748 }
1749 }
1750
1751 return sphere;
1752}
1753
1754// This yields more equal-sized triangles that the typical normalization approach
1755// For more info see: http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html
1756// And: https://medium.com/game-dev-daily/four-ways-to-create-a-mesh-for-a-sphere-d7956b825db4
1757
1758function Spherify(v3) {
1759 const p2 = v3_multiply(v3, v3);
1760 const x = p2[0];
1761 const y = p2[1];
1762 const z = p2[2];
1763 const rx = v3[0] * Math.sqrt(1.0 - 0.5 * (y + z) + y*z/3.0);
1764 const ry = v3[1] * Math.sqrt(1.0 - 0.5 * (z + x) + z*x/3.0);
1765 const rz = v3[2] * Math.sqrt(1.0 - 0.5 * (x + y) + x*y/3.0);
1766 return [rx, ry, rz];
1767}
1768
1769export function Cone(r0, r1, h, facets, color = [1,1,1,1]) {
1770 const cone = new Triangles();
1771 const b = [];
1772 const t = [];
1773 const angle = TAU / facets;
1774 const rotor0 = [0,r0];
1775 const rotor1 = [0,r1];
1776 for (let i = 0; i < facets; i++) {
1777 const a = i * angle;
1778 b.push(v2_rotate(rotor0, a));
1779 t.push(v2_rotate(rotor1, a));
1780 }
1781
1782 const diameter = r0 * TAU;
1783 for (let i = 0; i < facets; i++) {
1784 const j = (i+1) % facets;
1785 const c0 = b[i];
1786 const c1 = b[j];
1787 const c2 = t[j];
1788 const c3 = t[i];
1789 const v0 = [c0[1], -h/2, c0[0]];
1790 const v1 = [c1[1], -h/2, c1[0]];
1791 const v2 = [c2[1], h/2, c2[0]];
1792 const v3 = [c3[1], h/2, c3[0]];
1793 const u0 = i*diameter / (facets + 1);
1794 const u1 = (i+1) * diameter / (facets + 1);
1795 cone.addFace([v0, v1, v2, v3], [color, color, color, color], [[u0,0], [u1,0], [u1,h], [u0,h]]);
1796 }
1797
1798 b.reverse();
1799 const bVertices = [];
1800 const bColors = [];
1801 const bCoordinates = [];
1802 for (let i = 0; i < facets; i++) {
1803 const p = [b[i][1], -h/2, b[i][0]];
1804 bVertices.push(p);
1805 bColors.push(color);
1806 bCoordinates.push(b[i]);
1807 }
1808 cone.addFace(bVertices, bColors, bCoordinates);
1809
1810 const tVertices = [];
1811 const tColors = [];
1812 const tCoordinates = [];
1813 for (let i = 0; i < facets; i++) {
1814 const p = [t[i][1], h/2, t[i][0]];
1815 tVertices.push(p);
1816 tColors.push(color);
1817 tCoordinates.push(t[i]);
1818 }
1819 cone.addFace(tVertices, tColors, tCoordinates);
1820
1821 return cone;
1822}
1823
1824export function Cylinder(r, h, facets, color = [1,1,1,1]) {
1825 const cylinder = new Triangles();
1826 const b = [];
1827 const angle = TAU / facets;
1828 const rotor = [0,r];
1829 for (let i = 0; i < facets; i++) {
1830 const a = i * angle;
1831 b.push(v2_rotate(rotor, a));
1832 }
1833
1834 const diameter = r * TAU;
1835 for (let i = 0; i < facets; i++) {
1836 const j = (i+1) % facets;
1837 const c0 = b[i];
1838 const c1 = b[j];
1839 const v0 = [c0[1], -h/2, c0[0]];
1840 const v1 = [c1[1], -h/2, c1[0]];
1841 const v2 = [c1[1], h/2, c1[0]];
1842 const v3 = [c0[1], h/2, c0[0]];
1843 const u0 = i*diameter / (facets + 1);
1844 const u1 = (i+1) * diameter / (facets + 1);
1845 cylinder.addFace([v0, v1, v2, v3], [color, color, color, color], [[u0,0], [u1,0], [u1,h], [u0,h]]);
1846 }
1847
1848 const tVertices = [];
1849 const tColors = [];
1850 const tCoordinates = [];
1851 for (let i = 0; i < facets; i++) {
1852 const p = [b[i][1], h/2, b[i][0]];
1853 tVertices.push(p);
1854 tColors.push(color);
1855 tCoordinates.push(b[i]);
1856 }
1857 cylinder.addFace(tVertices, tColors, tCoordinates);
1858
1859 b.reverse();
1860 const bVertices = [];
1861 const bColors = [];
1862 const bCoordinates = [];
1863 for (let i = 0; i < facets; i++) {
1864 const p = [b[i][1], -h/2, b[i][0]];
1865 bVertices.push(p);
1866 bColors.push(color);
1867 bCoordinates.push(b[i]);
1868 }
1869 cylinder.addFace(bVertices, bColors, bCoordinates);
1870 return cylinder;
1871}
1872
1873//------------------------------------------------------------------------------------------
1874//--Stencil Functions ----------------------------------------------------------------------
1875//------------------------------------------------------------------------------------------
1876
1877export function StartStencilCapture() {
1878 gl.enable(gl.STENCIL_TEST);
1879 gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
1880 gl.stencilMask(0xff);
1881 gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
1882 gl.clearStencil(0);
1883 gl.clear(gl.STENCIL_BUFFER_BIT);
1884}
1885
1886export function EndStencil() {
1887 gl.disable(gl.STENCIL_TEST);
1888}
1889
1890export function StartStencilApply() {
1891 gl.enable(gl.STENCIL_TEST);
1892 gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
1893 gl.stencilMask(0x00);
1894 gl.stencilFunc(gl.EQUAL, 1, 0xFF);
1895}
1896
1897export function ClearAttribDivisor() {
1898 for (let i = 0; i < 16; i++) {
1899 gl.vertexAttribDivisor(i, 0);
1900 }
1901}
1902