====== NormalProducer and TileSampler implementation ====== /* Started on 10/02/2022 */ * Our current target on this project is to create an instance of a **TileSampler** object to attach to our terrain in our ''Test8Scene'' * But to create that TileSampler, we first need an instance of a **NormalProducer**, and we'll try to implement that here. * Again we need a special factory to create the framebuffer for the normal producer given a normal texture: **OK** * But now to finalize the support to create the normal producer framebuffer, we need to add the FrameBuffer methods ''setColorMask'', ''setDepthMask'' and ''setStencilMask'' * **note** for the color mask it seems we don't have support for multiple color mask in WebGL2. * **OK!** we now have the constructor for NormalProducer ready. * Now let's confirm we can instanciate that correctly. **OK** Working just fine ðŸĪŠ! [//This was almost too easy...//] * Now that we have the NormalProducer instantiated, we should create the corresponding TileSampler object: **PK** no problem with that either: // Create the normal tile sampler: this.DEBUG("Creating normal tile sampler...") let ts = new TileSampler("fragmentNormalSampler", normProd); ts.setStoreParent(false); ts.setStoreInvisible(false);/*//*/ * So this means we are done already with the Normalproducer implementation ðŸĪ” okay, so now I need to figure out how to integrate those tile samplers in my terrain note object 😅 * I think I need a refresh on what is a "TerrainNode" and how we got that implemented so far: * In our previous test scene (test scene 7) we had already implemented the TerrainNode object, and we where also creating an entity containing that terrain: world .createEntity() .addComponent('transform') .addComponent('terrain', terrain) .addComponent('program', p3) .addComponent('meshbuffers', quad.getBuffers()) * Then we were also creating a system to handle that terrain entity: world.registerSystem('terrain', (entity: Entity) => { let terrain = ECS.getTerrain(entity); let mesh = ECS.getMesh(entity); terrain.update(entity); terrain.draw(entity); }); * In that initial example we were already assigning a quad mesh to the terrain entity => Here we should also do the same, except that the quad data is a bit more complex: * The new quas mesh contains 25 x 25 points. * The points should cover the range [0.0,1.0] * The indices should draw the triangles counter-clockwise: so we start with: 0 1 25 25 1 26 1 2 26 26 2 27 * => instead of loading that quad from a file we could manually generate it with a utility function: Let's create that function as a static method in a **MeshUtils** class. * One thing I'm wondering here is on the behavior of Float32Array vs DataView.setFloat32(): in the last one we can specify the endianess to be little endian. Is that the result we get when simply using a Float32Array ? Let's write a unit test to check that. **OK** so yes: writting to a Float32Array is the same as setFloat32 on DataView in little endian. * So we can just as well construct a Float32Array view on the vertices array to fill the data 👍! * => I have now created the function **MeshUtils.createPlane(width, settings=null)** where ''width'' is the number of points on one side of the plane grid. * We should now try using that function directly when building our scene, but to achieve that we need to be able to create a 'procedural-plane' using the resource manager: **OK**: just added that section in the ResourceManager.loadMesh() function: // Check if the mesh is procedural: if (name.startsWith('%')) { let parts = name.split('-'); let m: Mesh = null; // Check if we are trying to create a plane: if (parts[0] == '%plane') { // Get the width of the plane: let width = Number(parts[1]); let m = MeshUtils.createPlane(width); } this.CHECK(m != null, `Unsupported procedural shape: ${parts[0]}`) // Store that new mesh: this.meshes.set(name, m); return m; } * Next I should use that procedural generation path to build our "quad" object: * ðŸĪ” hmmm... tried to replace the "quad" with a "%plane-2" mesh for the default terrain rendering as follow: // load the quad mesh: // let quad = await rman.loadMesh('quad'); let quad = await rman.loadMesh('%plane-2');/*//*/ * ... but that doesn't seem to work: not displaying the terrain quads at all anymore. * Let's check the program in use here: // load the terrain waves shader: let p3 = await rman.loadProgram('terrainWaves');/*//*/ * (Because one thing to note already is that our procedural planes is constructed with "triangles" and not "triangle_strip" and uses **indices** * => The terrain program doesn't seem to be doing anything fancy with the coordinates, only using the x and y coords from each vertex, so using triangles and setting z=1 should not be a problem here => the issue is thus with the indices ? Checking... * Oh oh, another thing I just noticed when using my procedural plane is that we end up with a message: Mesh2: Uploading 0 vertices on GPU * => So there is something wrong with the vertices data already. * Arrff, OK, this is due to how we retrieve the vertices and indices count in a Mesh object: public getVerticesCount(): number { return this.verticesPos / this.vertexSize; } public getIndicesCount(): number { return this.indicesPos / this.indexSize; } * => With our procedural generation we should set those members correctly too: **OK** * And now I get a exception because I need to handle indices correctly: Error: Mesh2: Should handle indices here. * This comes from the ''Mesh.createBuffers'' method with this section: if (ni != 0) { this.THROW("Should handle indices here.") } * So, let's implement that part now: if (ni != 0) { if (this.usage == MeshUsage.GPU_STATIC || this.usage == MeshUsage.GPU_DYNAMIC || this.usage == MeshUsage.GPU_STREAM) { this.indexBuffer = new GPUBuffer(); if (this.usage == MeshUsage.GPU_STATIC) { this.uploadIndexDataToGPU(BufferUsage.STATIC_DRAW); } } else if (this.usage == MeshUsage.CPU) { this.indexBuffer = new CPUBuffer(this.indices); } // AttributeType type; // switch (sizeof(index)) { // case 1: // type = A8UI; // break; // case 2: // type = A16UI; // break; // default: // type = A32UI; // break; // } this.buffers.setIndicesBuffer(new AttributeBuffer(0, 1, this.indicesType, false, this.indexBuffer)); } In the code above the **sizeof(index)** part is commented because this is a template parameter for the Mesh class, but we use instead the indicesType member is our javascript implementation. * Now unfortunately, this change leads to an invalid GL operation exception ðŸ˜Ē. To be investigated. * Note: the warning before the error is: GPUBuffer.ts:49 WebGL: INVALID_OPERATION: bindBuffer: buffers bound to non ELEMENT_ARRAY_BUFFER targets can not be bound to ELEMENT_ARRAY_BUFFER target * => Okay, so it seems **we cannot bind a buffer bound to ELEMENT_ARRAY_BUFFER to something else**: so when copying the data with setData, we have to use that target too: **OK** * I have now update the ''GPUBuffer.setData'' method to also support overriding the target to use for the copy: public setData(buf: ArrayBuffer, u: BufferUsage, tgt: number = -1) { this.size = buf.byteLength; if (tgt == -1) tgt = this.ctx.gl2 != null ? this.ctx.gl2.COPY_WRITE_BUFFER : this.ctx.gl.ARRAY_BUFFER; this.ctx.gl.bindBuffer(tgt, this.bufferId); this.ctx.gl.bufferData(tgt, buf, u); this.ctx.gl.bindBuffer(tgt, null); this.ctx.checkGLError(); } * And this seems to do the trick to be able to draw meshes with indices 👍! I should probably update the method ''GPUBuffer.setSubData(...)'' eventually. * Next problem is: it doesn't quite work anymore if I use "%plane-4" for instance instead of "%plane-2": why that ? * => arrff, I was computing the indices incorrectly in the procedural generation function, now fixed that introducing the required 'offset' for each row: idx = 0; let offset = 0; for (let r = 0; r < (width - 1); r++) { offset = r * width; for (let c = 0; c < (width - 1); c++) { indices[idx++] = offset + c; indices[idx++] = offset + c + 1; indices[idx++] = offset + c + width; indices[idx++] = offset + c + width; indices[idx++] = offset + c + 1; indices[idx++] = offset + c + width + 1; } } * Now using our "quad" mesh for the terrain with 25 vertices on one side: **OK** * Also updating the terrain parameters: this.DEBUG('Creating TerrainNode...'); let deform = new Deformation(); let size = 50000.0; let zmin = 0.0; let zmax = 5000.0; let splitFactor = 2; let maxLevel = 16; let terrain = new TerrainNode(deform, size, zmin, zmax, splitFactor, maxLevel); * Okay, and now the next step is going to be to actually **use** the tile samplers & producers in our terrain node: I feel this might be a somewhat large topic, so let's move that on a new page.