Display TMX Map on Three.js Plane

Updated question with new code

I'm currently working on a WebGL shader that is designed to render a TMX Layer exported from the Tiled editor. My approach involves using THREE.js to create a Plane mesh and setting the material as ShaderMaterial to draw the map on it.

For those unfamiliar, a tilemap exported by the Tiled editor in json format provides a data attribute for each layer. This attribute contains an array of numerical values representing the tile index in the tileset like:

"data": [5438, 5436, 5437, 5438, 5436, 5437, 5438, 5436, 5437, 5438, 845, ...]

Given that my tilemap consists of 256x256 tiles, the array is 65536 elements long. Each element represents a tile in the tilemap indexed as follows:

https://i.sstatic.net/b7n4O.png
(source: melonjs.org)

Therefore, index 0 in the tilemap corresponds to tile 5438 according to the above indexing logic. The indexes specify which tile in the tilemap corresponds to a particular tile from the tileset using the same counting convention.

This is how I am setting up the material, plane, and mesh:

this.size = new THREE.Vector2(256, 256);
this.tileSize = new THREE.Vector2(16, 16);

this._material = new THREE.ShaderMaterial({
    uniforms: this._uniforms,
    vertexShader: this.vShader,
    fragmentShader: this.fShader,
    transparent: (this.opacity === 0)
});

this._plane = new THREE.PlaneGeometry(
    this.size.x * this.tileSize.x,
    this.size.y * this.tileSize.y
);

this._mesh = new THREE.Mesh(this._plane, this._material);

Here are the uniforms and shaders. I need to map the data element to an actual tile in the tileset and then render it. To pass the data array to the shader, I load it as a THREE.DataTexture and treat it as a texture.

Below is my revised attempt at the shaders:

//Shaders
var vShader = [
   // shader code
].join('\n');

var fShader = [
   // shader code
].join('\n');

As well as the uniforms and data texture:

// uniform and data texture setup

Upon rendering, I encounter the issue of the first tile in the tileset being repeated multiple times:

I suspect there might be a zero value causing this problem since it occurs at position 0, 0.

Answer №1

Finally, after much trial and error, I managed to find a solution that works flawlessly. The main hurdle I faced was in packing my array of floats into a texture and then decoding it later on. It slipped my mind that the bytes loaded from the data texture are "normalized", meaning each value is divided by 255 in order to fall within the [0, 1] range. After shifting the necessary bits, I had to remember to multiply by 255.0 to denormalize.

In addition, I came across information stating that WebGL floats are actually 24-bit rather than 32-bit. As a result, I adjusted both my encoding and decoding algorithms to utilize only the RGB channels.

Now, the process successfully involves packaging an array of integers (or floats) into a Uint8Array, which is then passed to WebGL as a texture and utilized effectively:

Array Packing:

var array = [5438, 5436, 5437, 5438, 5436, 5437, ...],
    arrayBuff = new ArrayBuffer(array.length * 3),
    array8 = new Uint8Array(arrayBuff);

for (var i = 0, y = 0, il = array.length; i < il; ++i, y += 3) {
    var value = array[i];

    array8[y + 0] = (value & 0x000000ff);
    array8[y + 1] = (value & 0x0000ff00) >> 8;
    array8[y + 2] = (value & 0x00ff0000) >> 16;
}

dataTex = new THREE.DataTexture(
    array8,
    this.size.x, //width
    this.size.y, //height
    THREE.RGBFormat, //format
    THREE.UnsignedByteType, //type
    THREE.UVMapping, //mapping
    THREE.ClampToEdgeWrapping, //wrapS
    THREE.ClampToEdgeWrapping, //wrapT
    THREE.NearestFilter, //magFilter
    THREE.NearestMipMapNearestFilter //minFilter
);
dataTex.needsUpdate = true;

Shaders:

var vShader = [
    'varying vec2 pixelCoord;',
    'varying vec2 texCoord;',

    'uniform vec2 mapSize;',
    'uniform vec2 inverseLayerSize;',

    'uniform vec2 inverseTileSize;',

    'void main(void) {',
    '   pixelCoord = (uv * mapSize);',
    '   texCoord = pixelCoord * inverseLayerSize * inverseTileSize;',
    '   gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);', //hand this position to WebGL
'}'
].join('\n');

var fShader = [
    'varying vec2 pixelCoord;',         
    'varying vec2 texCoord;',

    'uniform vec2 inverseTilesetSize;',

    'uniform vec2 tileSize;',
    'uniform vec2 numTiles;',

    'uniform sampler2D tileset;',
    'uniform sampler2D tileIds;',

    'float decode24(const in vec3 rgb) {',
    '   const vec3 bit_shift = vec3((256.0*256.0), 256.0, 1.0);',
    '   float fl = dot(rgb, bit_shift);', //shift the values appropriately
    '   return fl * 255.0;', //denormalize the value
    '}',

    'void main(void) {',
    '   vec3 tileId = texture2D(tileIds, texCoord).rgb;', //grab this tileId from the layer data
    '   tileId.rgb = tileId.bgr;', //flip flop due to endianess
    '   float tileValue = decode24(tileId);', //decode the normalized vec3 into the float ID
    '   vec2 tileLoc = vec2(mod(tileValue, numTiles.x) - 1.0, floor(tileValue / numTiles.x));', //convert the ID into x, y coords;
    '   tileLoc.y = numTiles.y - 1.0 - tileLoc.y;', //convert the coord from bottomleft to the top left

    '   vec2 offset = floor(tileLoc) * tileSize;', //offset in the tile set
    '   vec2 coord = mod(pixelCoord, tileSize);', //coord of the tile.

    '   gl_FragColor = texture2D(tileset, (offset + coord) * inverseTilesetSize);', //grab the tile from the tile set
'}'
].join('\n');

Uniforms:

this._uniforms = window._uniforms = {
    mapSize:            { type: 'v2', value: new THREE.Vector2(this.size.x * this.tileSize.x, this.size.y * this.tileSize.y) },
    inverseLayerSize:   { type: 'v2', value: new THREE.Vector2(1 / this.size.x, 1 / this.size.y) },
    inverseTilesetSize: { type: 'v2', value: new THREE.Vector2(1 / this.tileset.image.width, 1 / this.tileset.image.height) },

    tileSize:           { type: 'v2', value: this.tileSize },
    inverseTileSize:    { type: 'v2', value: new THREE.Vector2(1 / this.tileSize.x, 1 / this.tileSize.y) },
    numTiles:           { type: 'v2', value: new THREE.Vector2(this.tileset.image.width / this.tileSize.x, this.tileset.image.height / this.tileSize.y) },

    tileset:            { type: 't', value: this.tileset },
    tileIds:            { type: 't', value: this.dataTex }
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieve the JSON response from the server and store it in variables using jQuery's AJAX function with the `done

I am trying to retrieve a JSON response from the server upon clicking a button and then parse it into a div. However, I am struggling with how to accomplish this. <button type="submit" id="btPay" name="btPay"> Go for Pay ...

Customizing variables in React based on the environment

I am working on a React app that includes a chart component which calls an external API. When the app is running locally, the API URL is set to localhost:8080. However, when the app is deployed, the API URL needs to be changed to prod:8080. I have tried ...

Enhance your webpage with dynamic styling using third-party CSS animation

Apologies for any similarity to other questions on this topic. I have searched extensively, but any assistance would be greatly appreciated. I am utilizing a new animation library from Animista to animate specific elements on a test website. Animating ele ...

Transform the MongoDB aggregation output by using variable keys

Here is a sample document from the Collection: { "_id" : ObjectId("5fec3b978b34e8b047b7ae14"), "duration" : 20.0, "createdOn" : ISODate("2020-12-16T22:28:44.000Z"), "ClockInTime" : ISODate ...

What sets apart optionalDependencies from peerDependencies in the Meta optional?

Why are both marking dependency as optional and what is the specific use-case of each? Is peerDependenciesMeta intended for npm packages while optionalDependencies is meant for projects? For example, in my npm package, certain modules require dependency ...

What sets apart custom events from postMessage?

If you want to send a message to another document, such as an iframe, there are two functions you can use - postMessage and createEvent. Consider the following: var event = document.createEvent('CustomEvent'); event.initCustomEvent("message", tr ...

Using jQuery's .html() method to update the inner HTML of an element can cause the .click() event on a form button to stop functioning

There is a puzzling issue with my HTML form button and jQuery functionality. The button is supposed to trigger a .click() event from a JavaScript file, and it was working perfectly until I used jQuery .html() to replace the main page content with the form ...

Guide on building an API that, once authenticated through LinkedIn, provides access to a person's complete list of connections

I'm currently working on creating an application that uses LinkedIn for authentication. Upon successful authentication, the app will retrieve the connections of the user who authenticated. ...

Error: Tried to import a module but encountered a TypeError stating that it cannot read properties of undefined while trying to read 'utf16le'

Attempting to conduct a Jest test on a React component that involves importing a module called "Siwe," which facilitates signing in with Ethereum. The configuration for Jest appears as follows: module.exports = { moduleNameMapper: { "module_nam ...

What steps are involved in creating a function within AngularJS?

Just starting out with AngularJS and looking to implement a shopping cart using WebSQL. Wondering how to create functions for adding items to the cart and removing them. Here's the code snippet I have so far: angular.module('ecommerce').fa ...

Is it possible to create a new field in mongoDB from a separate collection without any dependencies?

I manage two different sets of data: profiles and contents The profiles collection is structured as follows: { _id: ObjectId('618ef65e5295ba3132c11111'), blacklist: [ObjectId('618ef65e5295ba3132c33333'), ObjectId('618ef65e5295 ...

How do I verify response values in Postman tests when the input parameter may be empty and can have multiple possible values?

In my program, there is an input parameter known as IsFruit that can have a value of either 0 or 1. If IsFruit is set to 0, the response should return fruits with a FruitsYN value of N. Similarly, if it is set to 1, FruitsYN should be Y. In cases where I ...

A dynamic modal window built with ReactJS

I am struggling to understand how to make my app function properly. There is a component called ContactAdd that should render the component ModalWindow when clicked. The ModalWindow component requires a parameter called isOpened={this.state.open}. How c ...

Utilizing React with Firebase involves executing several Firebase requests simultaneously and ensuring the completion of all promises

I'm facing a challenge with the current code where it hits Firebase's 10 item limit in an array. My goal is to iterate through all teamIds and execute a Firebase query for each individual teamId. However, I'm unsure how to ensure that the ex ...

Ensure that Google Tag Manager (GTM) delays the pageview until the SPA URL title is available

I'm dealing with a React SPA that manages all the URL titles on the frontend, so the historyChange event registered on GTM captures the visited URLs along with their titles correctly. However, I've noticed that on the initial load of the SPA, su ...

Bringing PlayCanvas WebGL framework into a React environment

I am currently working on integrating the PlayCanvas webGL engine into React. PlayCanvas Github Since PlayCanvas operates on JS and the code remains in my index.html file, I am facing challenges in comprehending how it will interact with my React compone ...

The dropdown list in angularjs is failing to populate with options

For populating a dropdownlist using angularjs, I attempted the following: var app = angular.module('App', []); app.controller('UserSelection', function ($scope, $location) { var User = this; User.onSelectChange = function () { i ...

The partial sphere geometry in THREE.js unexpectedly yields two segments when only one segment was anticipated

Exploring the creation of partial sphere shapes using the additional features of the THREE.SphereGeometry constructor. // material var dish_material = new THREE.MeshLambertMaterial( { color: 0x00ffff, side: THREE.DoubleSide, ...

Having trouble resolving an error while attempting to incorporate an npm module into a vanilla JavaScript application

I admit my lack of expertise in building modern JavaScript apps is becoming evident. Currently, we have a Capacitor app that uses plain JavaScript without any build tools, and it functions well. Our goal is to incorporate Microsoft Code Push support throu ...

Enhancing User Experience in AngularJS UI-Router by Updating State without Refreshing the Template

I have noticed that every time I navigate to a different state, my templates and controllers are refreshing. This is not the behavior I want; I would like to retain the values entered into a form on contact.html when switching to home.html. Currently, I a ...