Procedural tile maps?

I want to build a procedural map out of tiles, but it seems that the existing tile map component is designed to use maps that are built in an external editor as opposed to runtime generation - unless I’m mistaken. What would be the best way to go about this?

If my prototype/idea works out, I’ll have a lot of overlapping layers of tiles to display on screen at one time (maybe 3000+), so it’d need to be efficiently rendered. I thought about maybe just procedurally generating and pooling a bunch of sprite nodes (one for each tile), but I’m unsure if that’d perform very well and it seems pretty heavy-handed.

I then thought about maybe just rendering the layers myself by building my own rendering component for this, but I don’t know how to go about that, either. :stuck_out_tongue:

No one has any tips about how to do something like this? :crying_cat_face:

Look at the code in FastTMXLayer for how to use the lower-level rendering XXXXXCommand classes, namely PrimitiveCommand. There are other posts on this forum if you search for that, but probably none about procedural (other than maybe a few similar questions of “how?”).

If you don’t need huge 128x128 or larger maps you could look at using the infrastructure for rendering a Tiled/.tmx map, but I probably wouldn’t advise for that (even though I’ve done that in one game, but the majority of the game was static maps with procedural being some “testing” extra maps). The main reason is that you’ll want to have full control over which tiles get batched together in a single draw call, decide on sorting with depth buffer or pre-GPU cpu sorting (topological, painters algo, etc).

An example of how to use the Tiled engine utilities you can define a map template in Tiled/editor and if your tilesets are not also procedural you can add those into the map template along with either the max size map or multiple size variants where the map has no tiles, essentially … or you could choose to say put 1 tile type in all map cells … and then you load in the map and then call your proc. gen. method to grab the layers of this map instance and fill it in using the correct tile IDs from Tiled. If you don’t already know the Tiled (or other) editor, nor the information about its .tmx format and the parser in the cocos2d-x engine then again you’re probably better off just starting from scratch and writing your own.

If your map is larger you’ll definitely want to write your own code for storing map info and rendering since as you mention you’ll want to render only some of the tiles at any given time.

If your map is really more of a side-scrolling platformer type game then you could consider Tiled/.tmx since 10x500+ maps are supported I believe (there’s a limit on total visible tiles).

If your map is a side-scrolling procedural like Alto’s Adventure or similar then there are much different techniques that you might use.

These questions often don’t get answers because it’s sort of a whole project unto itself, and its likely that very few developers are making procedural tile map games (even if many want to) in part because cocos2d isn’t readily designed for such games.

I tried to figure out how the tile component works, but for some reason I can’t figure it out. I assume this is the correct code for Cocos Creator? https://github.com/cocos-creator/engine/blob/master/cocos2d/tilemap/CCTiledLayer.js

I can’t seem to see where it actually… renders anything.

Ah, sorry I missed the fact you posted in “Cocos Creator” category. I don’t know much, if anything, about using that.

However, I do believe that the actual rendering is done in the assembler.
https://github.com/cocos-creator/engine/blob/master/cocos2d/tilemap/tmx-layer-assembler.js .

At least it appears that is where the buffer and vertices among other things are actually sent to the graphics layer. Whereas the CCTiledLayer is just filling in and allowing modifications to the internal javascript array of tile objects.

I created my own tile map logic as I want to implement a map editor for the users to create their own levels.

The performance is stable as far as I can tell. I did it simply by creatinf “sub nodes”, each containing 100*100 sprited. I simply remove the nodes that are definetly not visible.

If you want to expand this idea on having infinite maps, first think about the alogrithm to create the map itself. You will probably create some function and pass variables into it. Ideally you will have variables that change with the players position. So remove unvisible tiles to decrease the tile count and feed your players position in your function(s) to get the new tiles. These tiles are probably only sprites which will be added to the scene.

Going further if you want to reuse your map you want to store all generated parts as a text file. Simple create a parser then for loading the old generated tiles back in.

Reading my idea again it looks a lot like minecraft. Read about it!

So just using a bunch of sprite nodes performed okay? Huh! Maybe I should just try that instead of assuming that would be too slow. :stuck_out_tongue:

So far, I’ve pieced together this as a possible starting point for a custom renderer component, but I’m still not seeing any output when I attach it to a node and set a texture asset, so I’m not sure what I’m missing.

I mostly arrived at this by starting with the tile map assembler and removing things over and over to try to find a minimal set that’s needed. I may have removed too much - but it really seems like this should be rendering one quad using the node’s content size, but nothing shows up. :confused:

// -----------------------------------------------------------------------------------------
// Component:
// -----------------------------------------------------------------------------------------

const RenderComponent = cc.RenderComponent;
const renderEngine = cc.renderer.renderEngine;
const SpriteMaterial = renderEngine.SpriteMaterial;

let MyRenderer = cc.Class({
    extends: RenderComponent,

    properties: {
        _texture: {
            default: null,
            type: cc.Texture2D,
        },

        texture: {
            get: function () {
                return this._texture;
            },
            set: function (value, force) {
                this._texture = value;
                this._activateMaterial();
            },
            type: cc.Texture2D,
        },
    },

    ctor () {
    },

    onEnable: function () {
        this._super();

        this.node.setContentSize(100, 100);

        this._activateMaterial();
    },

    _activateMaterial () {
        let material = this._material;
        if (!material) {
            material = this._material = new SpriteMaterial();
            material.useColor = false;
        }

        if (this._texture) {
            material.texture = this._texture;
            this.markForUpdateRenderData(true);
            this.markForRender(true);
        }
        else {
            this.disableRender();   
        }
        
        this._updateMaterial(material);
    },
});

// -----------------------------------------------------------------------------------------
// Assembler:
// -----------------------------------------------------------------------------------------

const RenderFlow = cc.RenderFlow;

const math = renderEngine.math;
const mat4 = math.mat4;
const vec3 = math.vec3;

let _mat4_temp = mat4.create();
let _vec3_temp = vec3.create();

MyRenderer._assembler = {
    updateRenderData (comp) {
        let renderData = comp._renderData;
        if (!renderData) {
            renderData = comp._renderData = comp.requestRenderData();
        }

        let size = comp.node._contentSize;
        let anchor = comp.node._anchorPoint;
        renderData.updateSizeNPivot(size.width, size.height, anchor.x, anchor.y);
        renderData.material = comp.getMaterial();

        this.updateVertices(comp);
    },

    fillBuffers (comp, renderer) {
        let renderData = comp._renderData;
        let data = renderData._data;

        let buffer = renderer._meshBuffer,
            vertexOffset = buffer.byteOffset >> 2,
            vertexCount = renderData.vertexCount;
        
        let indiceOffset = buffer.indiceOffset,
            vertexId = buffer.vertexOffset;
            
        buffer.request(vertexCount, renderData.indiceCount);

        let vbuf = buffer._vData,
            ibuf = buffer._iData,
            uintbuf = buffer._uintVData;

        for (let i = 0, l = renderData.vertexCount; i < l; i++) {
            let vert = data[i];
            vbuf[vertexOffset++] = vert.x;
            vbuf[vertexOffset++] = vert.y;
            vbuf[vertexOffset++] = vert.u;
            vbuf[vertexOffset++] = vert.v;
            uintbuf[vertexOffset++] = vert.color;
        }

        for (let i = 0, l = renderData.indiceCount; i < l; i+=6) {
            ibuf[indiceOffset++] = vertexId;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+2;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+3;
            ibuf[indiceOffset++] = vertexId+2;
            vertexId += 4;
        }

        comp.node._renderFlag |= RenderFlow.FLAG_UPDATE_RENDER_DATA;
    },

    updateVertices (comp) {
        let node = comp.node;
        let renderData = comp._renderData;
        let data = renderData._data;
        let color = node._color._val;

        renderData.dataLength = renderData.vertexCount = renderData.indiceCount = 0;

        let appx = node._anchorPoint.x * node._contentSize.width,
            appy = node._anchorPoint.y * node._contentSize.height;

        mat4.copy(_mat4_temp, node._worldMatrix);
        vec3.set(_vec3_temp, -appx, -appy, 0);
        mat4.translate(_mat4_temp, _mat4_temp, _vec3_temp);

        let a = _mat4_temp.m00, b = _mat4_temp.m01, c = _mat4_temp.m04, d = _mat4_temp.m05,
            tx = _mat4_temp.m12, ty = _mat4_temp.m13;

        let dataOffset = 0;

        renderData.vertexCount += 4;
        renderData.indiceCount += 6;
        renderData.dataLength = renderData.vertexCount;

        let grid = {
            l: 0,
            r: 1,
            t: 0,
            b: 1,
        }

        let left = 0;
        let top = 0;
        let right = node._contentSize.width;
        let bottom = node._contentSize.height;

        // tl
        data[dataOffset].x = left * a + top * c + tx;
        data[dataOffset].y = left * b + top * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.t;
        data[dataOffset].color = color;

        dataOffset++;

        // bl
        data[dataOffset].x = left * a + bottom * c + tx;
        data[dataOffset].y = left * b + bottom * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.b;
        data[dataOffset].color = color;
        dataOffset++;

        // tr
        data[dataOffset].x = right * a + top * c + tx;
        data[dataOffset].y = right * b + top * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.t;
        data[dataOffset].color = color;
        dataOffset++;

        // br
        data[dataOffset].x = right * a + bottom * c + tx;
        data[dataOffset].y = right * b + bottom * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.b;
        data[dataOffset].color = color;
        dataOffset++;
    },
};

module.exports = MyRenderer;

Running in the simulator, I see the console fill with this warning - which seems maybe important.

Simulator: JS: [WARN]: Can not find vertex attribute: a_color
Simulator: JS: [WARN]: Can not find vertex attribute: a_color
Simulator: JS: [WARN]: Can not find vertex attribute: a_color
Simulator: JS: [WARN]: Can not find vertex attribute: a_color
Simulator: JS: [WARN]: Can not find vertex attribute: a_color

Yeah, I’m not familiar with the internal code of the Creator engine. Are you using the same WebGL shader from the engine? If this is a test maybe you can just post the non-cocos2d code + resources?

The error suggests that would guess the attribute for color was never bound.

Maybe your mesh setup is wrong, but it looks correct?
Maybe the shader that is supposed to be used is missing?

You could search the engine source for getAttribLocation and determine how that is called. It’s likely doing this somewhat automatically based on the data[] object property array … alright, I tried a quick 15 min search through the Creator engine code … it … is … quite convoluted :smiley:

I think the shaders are actually available to the engine as you can see here:

Again, sorry, I know javascript, but I haven’t used the Creator engine, nor looked inside its internals for any length of time. My guess is something simple is missing from your example. Either the data[].color isn’t triggering the use of the correct shader, or the shader isn’t getting loaded/compiled correctly, or its using the wrong shader, or maybe you’ve modified more of the engine than I’m guessing and you need to just add it back?

Thanks for digging with me! :slight_smile:

I haven’t modified the engine at all - the only source file I’ve been messing with is the one above. I just named it “MyRenderer.js” and attached it to a node and attached a random sprite image. Maybe I have to do more than that somehow. I know it’s running my assembler since I tried littering it with cc.log() and they all get called and the code is just a stripped version of the tile assembler. :confused: I went back and actually started fresh with the tile assembler and re-stripped things out and simplified just in case - but I still ended up with the same results.

It’s got to be something really dumb. :stuck_out_tongue:

1 Like

Maybe @slackmoehrle can take a look and/or forward to a Creator engine dev. I’m curious so I’ll download and give Creator another test run with your node script.

@jare can you ask an engineer to read this thread?

Thanks, and @jare to try and not waste your time, don’t read my posts (stevetranby).

For some reason the mesh buffer of a node w/component MyRenderer has a vertex format of "vfmtPosUv" instead of what you need "vfmtPosUvColor" (the default). May or may not be useful info.

That’s as far as I can probably help without spending real time debugging (along with time to read engine code and learn API reference).

1 Like

Tiny update on this - I removed the line material.useColor = false; and the runtime error about missing a_color goes away, anyway. Still doesn’t render anything, though, so now I have no leads as to what is wrong at all. :stuck_out_tongue:

hi, @bigzaphod. There are two ways to pass the color of the vertex. One is passed as an attribute variable through the buffer. In this way, the uniform color must disabled by material.useColor = false . Then the buffer format of the vertex data needs to use renderer.getBuffer('mesh', vfmtPosUvColor) instead of the default meshbuffer, because the meshbuffer uses vfmtPosUv . The other one is passed to the shader as a uniform variable. This method needs to set material.useColor = true , and the default meshBuffer can be used because the vertex data format is vfmtPosUv. The second way is now used in your code, so you need to comment out the material.useColor = false and uintbuf[vertexOffset++] = vert.color these two lines of code.

1 Like

Thank you! It worked! For reference, if anyone needs a starting point, this is now the current code:

// -----------------------------------------------------------------------------------------
// Component:
// -----------------------------------------------------------------------------------------

const RenderComponent = cc.RenderComponent;
const renderEngine = cc.renderer.renderEngine;
const SpriteMaterial = renderEngine.SpriteMaterial;

let MyRenderer = cc.Class({
    extends: RenderComponent,

    properties: {
        _texture: {
            default: null,
            type: cc.Texture2D
        },
        texture: {
            get () {
                return this._texture;
            },
            set (value) {
                if (this._texture === value) return;

                this._texture = value;
                this._activateMaterial();
            },
            type: cc.Texture2D,
        },
    },

    onEnable () {
        this._super();

        this._activateMaterial();
    },

    _activateMaterial () {
        let material = this._material;
        if (!material) {
            material = this._material = new SpriteMaterial();
        }
        
        if (this._texture) {
            material.texture = this._texture;
            this._updateMaterial(material);
            this.markForRender(true);
            this.markForUpdateRenderData(true);
        }
    },
});

// -----------------------------------------------------------------------------------------
// Assembler:
// -----------------------------------------------------------------------------------------

const RenderFlow = cc.RenderFlow;

const math = renderEngine.math;
const mat4 = math.mat4;
const vec3 = math.vec3;

let _mat4_temp = mat4.create();
let _vec3_temp = vec3.create();

MyRenderer._assembler = {
    updateRenderData (comp) {
        let renderData = comp._renderData;
        if (!renderData) {
            renderData = comp._renderData = comp.requestRenderData();
        }

        let size = comp.node._contentSize;
        let anchor = comp.node._anchorPoint;
        renderData.updateSizeNPivot(size.width, size.height, anchor.x, anchor.y);
        renderData.material = comp.getMaterial();

        this.updateVertices(comp);
    },

    fillBuffers (comp, renderer) {
        let renderData = comp._renderData;
        let data = renderData._data;

        let buffer = renderer._meshBuffer,
            vertexOffset = buffer.byteOffset >> 2,
            vertexCount = renderData.vertexCount;
        
        let indiceOffset = buffer.indiceOffset,
            vertexId = buffer.vertexOffset;
            
        buffer.request(vertexCount, renderData.indiceCount);

        let vbuf = buffer._vData,
            ibuf = buffer._iData;

        for (let i = 0, l = renderData.vertexCount; i < l; i++) {
            let vert = data[i];
            vbuf[vertexOffset++] = vert.x;
            vbuf[vertexOffset++] = vert.y;
            vbuf[vertexOffset++] = vert.u;
            vbuf[vertexOffset++] = vert.v;
        }

        for (let i = 0, l = renderData.indiceCount; i < l; i+=6) {
            ibuf[indiceOffset++] = vertexId;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+2;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+3;
            ibuf[indiceOffset++] = vertexId+2;
            vertexId += 4;
        }

        comp.node._renderFlag |= RenderFlow.FLAG_UPDATE_RENDER_DATA;
    },

    updateVertices (comp) {
        let node = comp.node;
        let renderData = comp._renderData;
        let data = renderData._data;

        renderData.dataLength = renderData.vertexCount = renderData.indiceCount = 0;

        let appx = node._anchorPoint.x * node._contentSize.width,
            appy = node._anchorPoint.y * node._contentSize.height;

        mat4.copy(_mat4_temp, node._worldMatrix);
        vec3.set(_vec3_temp, -appx, -appy, 0);
        mat4.translate(_mat4_temp, _mat4_temp, _vec3_temp);

        let a = _mat4_temp.m00, b = _mat4_temp.m01, c = _mat4_temp.m04, d = _mat4_temp.m05,
            tx = _mat4_temp.m12, ty = _mat4_temp.m13;

        let dataOffset = 0;

        renderData.vertexCount += 4;
        renderData.indiceCount += 6;
        renderData.dataLength = renderData.vertexCount;

        let grid = {
            l: 0,
            r: 1,
            t: 1,
            b: 0,
        }

        let left = 0;
        let top = 0;
        let right = node._contentSize.width;
        let bottom = node._contentSize.height;

        // tl
        data[dataOffset].x = left * a + top * c + tx;
        data[dataOffset].y = left * b + top * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.t;
        dataOffset++;

        // bl
        data[dataOffset].x = left * a + bottom * c + tx;
        data[dataOffset].y = left * b + bottom * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.b;
        dataOffset++;

        // tr
        data[dataOffset].x = right * a + top * c + tx;
        data[dataOffset].y = right * b + top * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.t;
        dataOffset++;

        // br
        data[dataOffset].x = right * a + bottom * c + tx;
        data[dataOffset].y = right * b + bottom * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.b;
        dataOffset++;
    },
};

module.exports = MyRenderer;
1 Like

So in order to do my own per-vertex color, I’ll need to set material.useColor = false and use renderer.getBuffer('mesh', vfmtPosUvColor), right? Unfortunately I cannot figure out how to get a reference to vfmtPosUvColor in my component. It seems like everywhere else uses require() to get it, but since that code is within the engine it can locate it using a relative path. I’m doing this in my own component and not within the engine’s directory structure, so I don’t know how to reference it.

Update: I got vertex coloring to work!

I wasn’t able to figure out how to reference the existing vfmtPosUvColor in the engine, but just copying the definition and pasting it into my script worked. So this is now a self-contained custom Renderer that I think could be built off of for more complex things. I’d be interested in trying to minimize this code some more if possible, but it’s not too bad as it is.

// -----------------------------------------------------------------------------------------
// Component:
// -----------------------------------------------------------------------------------------

const RenderComponent = cc.RenderComponent;
const renderEngine = cc.renderer.renderEngine;
const SpriteMaterial = renderEngine.SpriteMaterial;
const gfx = renderEngine.gfx;

var vfmtPosUvColor = new gfx.VertexFormat([
    { name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
    { name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
    { name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
]);
vfmtPosUvColor.name = 'vfmtPosUvColor';

let MyRenderer = cc.Class({
    extends: RenderComponent,

    properties: {
        _texture: {
            default: null,
            type: cc.Texture2D,
        },

        texture: {
            get: function () {
                return this._texture;
            },
            set: function (value, force) {
                this._texture = value;
                this._activateMaterial();
            },
            type: cc.Texture2D,
        },
    },

    ctor () {
    },

    onEnable: function () {
        this._super();

        this.node.setContentSize(100, 100);

        this._activateMaterial();
    },

    _activateMaterial () {
        let material = this._material;
        if (!material) {
            material = this._material = new SpriteMaterial();
            material.useColor = false;
        }

        if (this._texture) {
            material.texture = this._texture;
            this.markForUpdateRenderData(true);
            this.markForRender(true);
        }
        else {
            this.disableRender();   
        }
        
        this._updateMaterial(material);
    },
});

// -----------------------------------------------------------------------------------------
// Assembler:
// -----------------------------------------------------------------------------------------

const RenderFlow = cc.RenderFlow;

const math = renderEngine.math;
const mat4 = math.mat4;
const vec3 = math.vec3;

let _mat4_temp = mat4.create();
let _vec3_temp = vec3.create();

MyRenderer._assembler = {
    updateRenderData (comp) {
        let renderData = comp._renderData;
        if (!renderData) {
            renderData = comp._renderData = comp.requestRenderData();
        }

        let size = comp.node._contentSize;
        let anchor = comp.node._anchorPoint;
        renderData.updateSizeNPivot(size.width, size.height, anchor.x, anchor.y);
        renderData.material = comp.getMaterial();

        this.updateVertices(comp);
    },

    fillBuffers (comp, renderer) {
        let renderData = comp._renderData;
        let data = renderData._data;

        let buffer = renderer.getBuffer('mesh', vfmtPosUvColor),
            vertexOffset = buffer.byteOffset >> 2,
            vertexCount = renderData.vertexCount;
        
        let indiceOffset = buffer.indiceOffset,
            vertexId = buffer.vertexOffset;
            
        buffer.request(vertexCount, renderData.indiceCount);

        let vbuf = buffer._vData,
            ibuf = buffer._iData,
            uintbuf = buffer._uintVData;

        for (let i = 0, l = renderData.vertexCount; i < l; i++) {
            let vert = data[i];
            vbuf[vertexOffset++] = vert.x;
            vbuf[vertexOffset++] = vert.y;
            vbuf[vertexOffset++] = vert.u;
            vbuf[vertexOffset++] = vert.v;
            uintbuf[vertexOffset++] = vert.color;
        }

        for (let i = 0, l = renderData.indiceCount; i < l; i+=6) {
            ibuf[indiceOffset++] = vertexId;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+2;
            ibuf[indiceOffset++] = vertexId+1;
            ibuf[indiceOffset++] = vertexId+3;
            ibuf[indiceOffset++] = vertexId+2;
            vertexId += 4;
        }

        comp.node._renderFlag |= RenderFlow.FLAG_UPDATE_RENDER_DATA;
    },

    updateVertices (comp) {
        let node = comp.node;
        let renderData = comp._renderData;
        let data = renderData._data;

        renderData.dataLength = renderData.vertexCount = renderData.indiceCount = 0;

        let appx = node._anchorPoint.x * node._contentSize.width,
            appy = node._anchorPoint.y * node._contentSize.height;

        mat4.copy(_mat4_temp, node._worldMatrix);
        vec3.set(_vec3_temp, -appx, -appy, 0);
        mat4.translate(_mat4_temp, _mat4_temp, _vec3_temp);

        let a = _mat4_temp.m00, b = _mat4_temp.m01, c = _mat4_temp.m04, d = _mat4_temp.m05,
            tx = _mat4_temp.m12, ty = _mat4_temp.m13;

        let dataOffset = 0;

        renderData.vertexCount += 4;
        renderData.indiceCount += 6;
        renderData.dataLength = renderData.vertexCount;

        let grid = {
            l: 0,
            r: 1,
            t: 1,
            b: 0,
        }

        let colors = {
            tl: cc.Color.RED._val,
            tr: cc.Color.GREEN._val,
            bl: cc.Color.BLUE._val,
            br: cc.Color.YELLOW._val,
        }

        let left = 0;
        let top = 0;
        let right = node._contentSize.width;
        let bottom = node._contentSize.height;

        // tl
        data[dataOffset].x = left * a + top * c + tx;
        data[dataOffset].y = left * b + top * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.t;
        data[dataOffset].color = colors.tl;

        dataOffset++;

        // bl
        data[dataOffset].x = left * a + bottom * c + tx;
        data[dataOffset].y = left * b + bottom * d + ty;
        data[dataOffset].u = grid.l;
        data[dataOffset].v = grid.b;
        data[dataOffset].color = colors.bl;
        dataOffset++;

        // tr
        data[dataOffset].x = right * a + top * c + tx;
        data[dataOffset].y = right * b + top * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.t;
        data[dataOffset].color = colors.tr;
        dataOffset++;

        // br
        data[dataOffset].x = right * a + bottom * c + tx;
        data[dataOffset].y = right * b + bottom * d + ty;
        data[dataOffset].u = grid.r;
        data[dataOffset].v = grid.b;
        data[dataOffset].color = colors.br;
        dataOffset++;
    },
};

module.exports = MyRenderer;

Sample:

4 Likes