public:projects:nervland:notes:0005_landjs_normal_producer

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 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.
  • public/projects/nervland/notes/0005_landjs_normal_producer.txt
  • Last modified: 2022/02/19 08:32
  • by 127.0.0.1