Thibaut CHARLES - CromFr@gmail.com
This document is a mix of information:
- I found by reverse engineering: basic structure, vertices, edged, triangles
- Extracted from Skywing NWN2DataLib source code: everything else
My implementation can be found here, and supports parsing, serialization, mesh importation, path table & island baking.
TRN.ASWM packets are used to store the walkmesh information in TRN and TRX files.
The packet itself is stored as is:
Type | Name | Description |
---|---|---|
char[4] |
data_type |
Always COMP for compressed walkmesh |
uint32_t |
compressed_data_size |
Size of compressed_data |
uint32_t |
uncomp_length |
Uncompressed walkmesh size |
void[compressed_data_size] |
compressed_data |
zlib-compressed walkmesh data |
Uncompressed walkmesh data can be retrieved using zlib.deflate
. The following structure information are for the uncompressed walkmesh data.
TRN.ASWM is slightly different weather it is stored in a TRX or TRN file. Generally, the TRX version is smaller as it has to be downloaded/loaded by the client, and contains the baked version of the walkmesh (with placeable walkmesh/walkmesh cutter alterations).
- TRN version:
- The whole map is stored, but no information on placeable walkmeshes nor walkmesh cutters
- Triangle
clockwise
flag is not set - Contains map border geometry
- Path tables & island list are empty
- TRX version:
- Baked counterpart of the TRN version
- Map borders are not stored, only the center.
- Triangles cut by a "walkmesh cutter" are removed (creates holes in the mesh)
- Placeable walkmesh modifications appears
- Path tables & island list are populated
Type | Name | d |
---|---|---|
Header |
header |
|
Vertex[header.vertices_count] |
vertices |
|
Edge[header.edges_count] |
edges |
|
Triangle[header.triangles_count] |
triangles |
|
TilesHeader |
tiles_header |
|
Tile[tiles_header.grid_height * tiles_header.grid_width] |
tiles |
|
uint32_t |
tiles_border_size |
Width of the map borders in tiles |
uint32_t |
islands_length |
Length of islands |
Island[islands_length] |
islands |
|
IslandPathNode[islands_length * islands_length] |
islands_path_nodes |
Pathfinding data |
Type | Name | Description |
---|---|---|
uint32_t |
version |
|
char[32] |
name |
|
uint8_t |
owns_data |
|
uint32_t |
vertices_count |
|
uint32_t |
edges_count |
|
uint32_t |
triangles_count |
|
uint32_t |
triangles_offset |
Seems to be always 0 |
Type | Description |
---|---|
float[3] |
x/y/z coordinates of the vertex |
Type | Description |
---|---|
uint32_t[2] |
Vertex indices |
uint32_t[2] |
Attached triangle indices. -1 if a triangle does not exist. |
- The struct defines a edge between two triangles, with the shared edge.
- Every triangle has 3 Edge structs.
- Probably only used to ease path tables generation
- TODO: I should try remove all Edges from a baked trx and see what happens
Type | Name | Description |
---|---|---|
uint32_t[3] |
vertices |
Vertex indices composing the triangle |
uint32_t[3] |
linked_edges |
Edge indices corresponding to this triangle edges |
uint32_t[3] |
linked_triangles |
Triangle indices that have an edge in common with this triangle. -1 is there is no triangle on one. |
float[2] |
center |
Triangle center x/y coordinates |
float[3] |
normal |
Normal vector |
float |
dot |
Dot product at plane |
uint16_t |
island |
-1 if the triangle is non walkable, else it is an island index |
uint16_t |
flags |
Walkmesh flags |
linked_triangles[i]
andlinked_edges[i]
must share the same edge
walkable = 0x01
: if the triangle can be walked onclockwise = 0x04
: vertices are wound clockwise and not ccwdirt = 0x08
: Floor type (for sound effects)grass = 0x10
: Floor type (for sound effects)stone = 0x20
: Floor type (for sound effects)wood = 0x40
: Floor type (for sound effects)carpet = 0x80
: Floor type (for sound effects)metal = 0x100
: Floor type (for sound effects)swamp = 0x200
: Floor type (for sound effects)mud = 0x400
: Floor type (for sound effects)leaves = 0x800
: Floor type (for sound effects)water = 0x1000
: Floor type (for sound effects)puddles = 0x2000
: Floor type (for sound effects)
- In TRN files:
- Map border megatiles triangles have
island=-1
and walkable flag set to 0 - Bakeable triangles (map center) have a valid
island
(ie: != -1), and the walkable flag set to 1/0 if the triangle is meant to be walkable/non walkable.
- Map border megatiles triangles have
- In TRX files:
- Map borders are not stored
- Non walkable triangles have
island=-1
and walkable flag set to 0 - Triangles cut by a "walkmesh cutter" are not stored
- For the raw map:
unknownA
is always 0 (or -0)
- For the icy peak map
- -498.294 ==> 475.229
Type | Name | Description |
---|---|---|
uint32_t |
flags |
Always 31 in TRX files, 15 in TRN files |
float |
width |
Width in meters of a terrain tile (most likely to be 10.0) |
uint32_t |
grid_height |
Number of tiles along Y axis |
uint32_t |
grid_width |
Number of tiles along X axis |
uint32_t |
border_size |
Width of the map borders in tiles (8 means that 8 tiles will be removed on each side) |
A tile is a square (generally 10 x 10 meters) containing mesh triangles. In TRX files, the tiles also contains pre-calculated pathfinding information.
Type | Name | Description |
---|---|---|
TileHeader |
header |
|
Vertex[] |
vertices |
Unused by nwn2. Might be usable with tile.header.owns_data == true |
Edge[] |
edges |
Unused by nwn2. Might be usable with tile.header.owns_data == true |
PathTableHeader |
path_table_header |
|
ubyte[] |
local_to_node |
local_to_node[localTriangleIndex] is an index in nodes |
uint32_t[] |
node_to_local |
node_to_local[nodeValue] is a local triangle index |
ubyte[] |
nodes |
nodes[node_to_local_length * fromLTNIndex + destLTNIndex] is an index in node_to_local |
uint32_t |
flags |
Reuse value aswm.tiles_header.tiles_flags |
- All triangles of a tile must have consecutive indices
- The triangle owned by a tile can be retrieved using
aswm.triangles[header.triangles_offset .. header.triangles_offset + header.triangles_count]
- A tile must not re-use triangles owned by another tile (will crash NWN2)
- TODO: I need to try making non-square tiles and see how the game handles it
This is a pre-calculated pathfinding table, that reference the next triangle to go to in order to reach any other triangle of the tile.
This is how a path on a tile is calculated:
- Get local triangle indices:
fromLocIdx = fromGlobIdx - tile.header.triangles_offset
for the starting triangledestLocIdx = destGlobIdx - tile.header.triangles_offset
for the triangle you are trying to reach- If
fromLocIdx
ordestLocIdx
equals255
, it means the triangle is not walkable
- Get node indices:
fromNodeIdx = tile.local_to_node[fromLocIdx]
destNodeIdx = tile.local_to_node[destLocIdx]
- Get the Node value
node = tile.nodes[fromNodeIdx * header.node_to_local_length + destNodeIdx]
node == 255
can mean two things:- Destination cannot be reached, because one of the triangles is non- walkable or there is an obstacle that can't be get around without leaving the tile.
- If
fromNodeIdx == destNodeIdx
, this means you already reached the destination triangle
- Get the next local triangle index to go to in order to reach destination
nextLocIdx = tile.node_to_local[node & 0b0111_1111]
- Loop on 2. with
fromNodeIdx = nextLocIdx
, untilnextLocIdx == destNodeIdx
nodes[i] & 0b1000_0000
is a bit flag for if there is a clear line of sight between the two triangle. It's not clear what LOS is since two linked triangles on flat ground may not have LOS = 1 in game files. A value of 0 means the game will have to calculate LOS in real time.
Type | Name | Description |
---|---|---|
char[32] |
name |
Sometime contains garbage, sometime contains only null values |
ubyte |
owns_data |
1 if the tile stores vertices / edges. NWN2 always set this to 0 |
uint32_t |
vertices_count |
Number of vertices in this tile |
uint32_t |
edges_count |
Number of edges in this tile |
uint32_t |
triangles_count |
Number of triangles in this tile (walkable + unwalkable) |
float |
size_x |
TODO: Always 0? |
float |
size_y |
TODO: Always 0? |
uint32_t |
triangles_offset |
Type | Name | Description |
---|---|---|
uint32_t |
compression_flags |
Always 0. Note: it's pointless to compress path table since the aswm is already compressed |
uint32_t |
local_to_node_length |
Length of local_to_node array |
ubyte |
node_to_local_length |
Length of node_to_local array |
uint32_t |
rle_table_size |
Always 0. |
compression_flags
values:rle = 1
,zcompress = 2
An island is a fraction of a tile, where all walkable triangles can be reached without leaving the tile. This is a pathfinding related struct that is not available in TRN files.
Type | Name | Description |
---|---|---|
IslandHeader |
header |
|
uint32_t |
linked_islands_length |
Length of linked_islands |
uint32_t[] |
linked_islands |
Island indices linked to this one |
uint32_t |
linked_islands_dist_length |
Length of linked_islands_dist |
float[] |
linked_islands_dist |
Distance to linked islands |
uint32_t |
linked_islands_exit_length |
Length of linked_islands_exit |
uint32_t[] |
linked_islands_exit |
Exit triangles leading to linked islands |
linked_islands_dist[i]
andlinked_islands_exit[i]
are the distance & exit tolinked_islands[i]
- I don't see the point of storing 3 times the same length. Maybe the nwn2 devs did not have time clean this.
Type | Name | Description |
---|---|---|
uint32_t |
index |
Index of the island in the aswm.islands array |
uint32_t |
tile |
Value looks pretty random, but is identical for all islands. TODO: try setting it to associated tile index |
float[3] |
center |
Center of the island. Z is always 0 |
uint32_t |
triangles_count |
Number of triangles in this island |
Pathfinding across islands works very similarly to Tile path table.
aswm.islands_path_nodes[fromIslandIdx * aswm.islands_length + toIslandIdx]
is
the next island to go to in order to reach toIslandIdx
from fromIslandIdx
Type | Name | Description |
---|---|---|
uint16_t |
next |
Next island to go to |
uint16_t |
_padding |
unused |
float |
weight |
Distance to the next island |