NormalProducer and TileSampler implementation
- 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
andsetStencilMask
- 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.