From fd1135c7af46eb2f5b99a11f48bf9f9ae335ea9c Mon Sep 17 00:00:00 2001 From: Perttu Ahola Date: Sat, 16 Jun 2012 03:40:45 +0300 Subject: Node texture animation --- src/clientserver.h | 2 + src/content_mapblock.cpp | 50 +++++++++++++++- src/game.cpp | 2 +- src/itemdef.cpp | 2 +- src/mapblock_mesh.cpp | 54 ++++++++++++++++- src/mapblock_mesh.h | 6 ++ src/nodedef.cpp | 147 ++++++++++++++++++++++++++++++++++++----------- src/nodedef.h | 38 ++++++++---- src/scriptapi.cpp | 96 +++++++++++++++++++++++++------ src/test.cpp | 8 +-- src/tile.cpp | 46 ++++++++++++++- src/tile.h | 13 ++++- 12 files changed, 390 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/clientserver.h b/src/clientserver.h index 7b8fe8e39..86e929f61 100644 --- a/src/clientserver.h +++ b/src/clientserver.h @@ -54,6 +54,8 @@ with this program; if not, write to the Free Software Foundation, Inc., PROTOCOL_VERSION 10: TOCLIENT_PRIVILEGES Version raised to force 'fly' and 'fast' privileges into effect. + Node metadata change (came in later; somewhat incompatible) + TileDef in ContentFeatures (non-TileDef deserialization is supported) */ #define PROTOCOL_VERSION 10 diff --git a/src/content_mapblock.cpp b/src/content_mapblock.cpp index 7585abb69..9d95e1758 100644 --- a/src/content_mapblock.cpp +++ b/src/content_mapblock.cpp @@ -471,7 +471,9 @@ void mapblock_mesh_generate_special(MeshMakeData *data, pa_liquid.x0(), pa_liquid.y0()), }; - // This fixes a strange bug + // To get backface culling right, the vertices need to go + // clockwise around the front of the face. And we happened to + // calculate corner levels in exact reverse order. s32 corner_resolve[4] = {3,2,1,0}; for(s32 i=0; i<4; i++) @@ -482,6 +484,52 @@ void mapblock_mesh_generate_special(MeshMakeData *data, vertices[i].Pos.Y += corner_levels[j]; vertices[i].Pos += intToFloat(p, BS); } + + // Default downwards-flowing texture animation goes from + // -Z towards +Z, thus the direction is +Z. + // Rotate texture to make animation go in flow direction + // Positive if liquid moves towards +Z + int dz = (corner_levels[side_corners[2][0]] + + corner_levels[side_corners[2][1]] < + corner_levels[side_corners[3][0]] + + corner_levels[side_corners[3][1]]); + // Positive if liquid moves towards +X + int dx = (corner_levels[side_corners[0][0]] + + corner_levels[side_corners[0][1]] < + corner_levels[side_corners[1][0]] + + corner_levels[side_corners[1][1]]); + // -X + if(-dx >= abs(dz)) + { + v2f t = vertices[0].TCoords; + vertices[0].TCoords = vertices[1].TCoords; + vertices[1].TCoords = vertices[2].TCoords; + vertices[2].TCoords = vertices[3].TCoords; + vertices[3].TCoords = t; + } + // +X + if(dx >= abs(dz)) + { + v2f t = vertices[0].TCoords; + vertices[0].TCoords = vertices[3].TCoords; + vertices[3].TCoords = vertices[2].TCoords; + vertices[2].TCoords = vertices[1].TCoords; + vertices[1].TCoords = t; + } + // -Z + if(-dz >= abs(dx)) + { + v2f t = vertices[0].TCoords; + vertices[0].TCoords = vertices[3].TCoords; + vertices[3].TCoords = vertices[2].TCoords; + vertices[2].TCoords = vertices[1].TCoords; + vertices[1].TCoords = t; + t = vertices[0].TCoords; + vertices[0].TCoords = vertices[3].TCoords; + vertices[3].TCoords = vertices[2].TCoords; + vertices[2].TCoords = vertices[1].TCoords; + vertices[1].TCoords = t; + } u16 indices[] = {0,1,2,2,3,0}; // Add to mesh collector diff --git a/src/game.cpp b/src/game.cpp index 6ccf02677..ac6d13af1 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2206,7 +2206,7 @@ void the_game( infotext = narrow_to_wide(meta->getString("infotext")); } else { MapNode n = map.getNode(nodepos); - if(nodedef->get(n).tname_tiles[0] == "unknown_block.png"){ + if(nodedef->get(n).tiledef[0].name == "unknown_block.png"){ infotext = L"Unknown node: "; infotext += narrow_to_wide(nodedef->get(n).name); } diff --git a/src/itemdef.cpp b/src/itemdef.cpp index e8de06387..c8771c30c 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -451,7 +451,7 @@ public: if(def->inventory_texture == NULL) { def->inventory_texture = - tsrc->getTextureRaw(f.tname_tiles[0]); + tsrc->getTextureRaw(f.tiledef[0].name); } } diff --git a/src/mapblock_mesh.cpp b/src/mapblock_mesh.cpp index 5584216ba..fd5937bbe 100644 --- a/src/mapblock_mesh.cpp +++ b/src/mapblock_mesh.cpp @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gamedef.h" #include "mesh.h" #include "content_mapblock.h" +#include "noise.h" /* MeshMakeData @@ -559,6 +560,11 @@ TileSpec getNodeTileN(MapNode mn, v3s16 p, u8 tileindex, MeshMakeData *data) spec.material_flags |= MATERIAL_FLAG_CRACK; spec.texture = data->m_gamedef->tsrc()->getTextureRawAP(spec.texture); } + // If animated, replace tile texture with one without texture atlas + if(spec.material_flags & MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES) + { + spec.texture = data->m_gamedef->tsrc()->getTextureRawAP(spec.texture); + } return spec; } @@ -983,6 +989,23 @@ MapBlockMesh::MapBlockMesh(MeshMakeData *data): crack_basename += "^[crack"; m_crack_materials.insert(std::make_pair(i, crack_basename)); } + // - Texture animation + if(p.tile.material_flags & MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES) + { + ITextureSource *tsrc = data->m_gamedef->tsrc(); + // Add to MapBlockMesh in order to animate these tiles + m_animation_tiles[i] = p.tile; + m_animation_frames[i] = 0; + // Get starting position from noise + m_animation_frame_offsets[i] = 100000 * (2.0 + noise3d( + data->m_blockpos.X, data->m_blockpos.Y, + data->m_blockpos.Z, 0)); + // Replace tile texture with the first animation frame + std::ostringstream os(std::ios::binary); + os<getTextureName(p.tile.texture.id); + os<<"^[verticalframe:"<<(int)p.tile.animation_frame_count<<":0"; + p.tile.texture = tsrc->getTexture(os.str()); + } // - Lighting for(u32 j = 0; j < p.vertices.size(); j++) { @@ -1055,7 +1078,8 @@ MapBlockMesh::MapBlockMesh(MeshMakeData *data): // Check if animation is required for this mesh m_has_animation = !m_crack_materials.empty() || - !m_daynight_diffs.empty(); + !m_daynight_diffs.empty() || + !m_animation_tiles.empty(); } MapBlockMesh::~MapBlockMesh() @@ -1094,6 +1118,34 @@ bool MapBlockMesh::animate(bool faraway, float time, int crack, u32 daynight_rat m_last_crack = crack; } + + // Texture animation + for(std::map::iterator + i = m_animation_tiles.begin(); + i != m_animation_tiles.end(); i++) + { + const TileSpec &tile = i->second; + // Figure out current frame + int frameoffset = m_animation_frame_offsets[i->first]; + int frame = (int)(time * 1000 / tile.animation_frame_length_ms + + frameoffset) % tile.animation_frame_count; + // If frame doesn't change, skip + if(frame == m_animation_frames[i->first]) + continue; + + m_animation_frames[i->first] = frame; + + scene::IMeshBuffer *buf = m_mesh->getMeshBuffer(i->first); + ITextureSource *tsrc = m_gamedef->getTextureSource(); + + // Create new texture name from original + std::ostringstream os(std::ios::binary); + os<getTextureName(tile.texture.id); + os<<"^[verticalframe:"<<(int)tile.animation_frame_count<<":"<getTexture(os.str()); + buf->getMaterial().setTexture(0, ap.atlas); + } // Day-night transition if(daynight_ratio != m_last_daynight_ratio) diff --git a/src/mapblock_mesh.h b/src/mapblock_mesh.h index 0877677bc..d5733120b 100644 --- a/src/mapblock_mesh.h +++ b/src/mapblock_mesh.h @@ -122,6 +122,12 @@ private: // Maps mesh buffer (i.e. material) indices to base texture names std::map m_crack_materials; + // Animation info: texture animationi + // Maps meshbuffers to TileSpecs + std::map m_animation_tiles; + std::map m_animation_frames; // last animation frame + std::map m_animation_frame_offsets; + // Animation info: day/night transitions // Last daynight_ratio value passed to animate() u32 m_last_daynight_ratio; diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 72f1ea2ea..d0ce3c19d 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -63,19 +63,29 @@ void NodeBox::deSerialize(std::istream &is) } /* - MaterialSpec + TileDef */ - -void MaterialSpec::serialize(std::ostream &os) const + +void TileDef::serialize(std::ostream &os) const { - os<tname_tiles[j]; - if(tname_tiles[j] == "") - tname_tiles[j] = "unknown_block.png"; - } + tiledef[j] = f->tiledef[j]; + if(tiledef[j].name == "") + tiledef[j].name = "unknown_block.png"; + } switch(f->drawtype){ default: @@ -547,7 +567,7 @@ public: f->drawtype = NDT_NORMAL; f->solidness = 2; for(u32 i=0; i<6; i++){ - tname_tiles[i] += std::string("^[noalpha"); + tiledef[i].name += std::string("^[noalpha"); } } break; @@ -560,29 +580,92 @@ public: break; } - // Tile textures + // Tiles (fill in f->tiles[]) for(u16 j=0; j<6; j++){ - f->tiles[j].texture = tsrc->getTexture(tname_tiles[j]); + // Texture + f->tiles[j].texture = tsrc->getTexture(tiledef[j].name); + // Alpha f->tiles[j].alpha = f->alpha; if(f->alpha == 255) f->tiles[j].material_type = MATERIAL_ALPHA_SIMPLE; else f->tiles[j].material_type = MATERIAL_ALPHA_VERTEX; + // Material flags f->tiles[j].material_flags = 0; if(f->backface_culling) f->tiles[j].material_flags |= MATERIAL_FLAG_BACKFACE_CULLING; + if(tiledef[j].animation.type == TAT_VERTICAL_FRAMES) + f->tiles[j].material_flags |= MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES; + // Animation parameters + if(f->tiles[j].material_flags & + MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES) + { + // Get raw texture size to determine frame count by + // aspect ratio + video::ITexture *t = tsrc->getTextureRaw(tiledef[j].name); + v2u32 size = t->getOriginalSize(); + int frame_height = (float)size.X / + (float)tiledef[j].animation.aspect_w * + (float)tiledef[j].animation.aspect_h; + int frame_count = size.Y / frame_height; + int frame_length_ms = 1000.0 * + tiledef[j].animation.length / frame_count; + f->tiles[j].animation_frame_count = frame_count; + f->tiles[j].animation_frame_length_ms = frame_length_ms; + + // If there are no frames for an animation, switch + // animation off (so that having specified an animation + // for something but not using it in the texture pack + // gives no overhead) + if(frame_count == 1){ + f->tiles[j].material_flags &= + ~MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES; + } + } } - // Special tiles + // Special tiles (fill in f->special_tiles[]) for(u16 j=0; jspecial_tiles[j].texture = tsrc->getTexture(f->mspec_special[j].tname); + // Texture + f->special_tiles[j].texture = + tsrc->getTexture(f->tiledef_special[j].name); + // Alpha f->special_tiles[j].alpha = f->alpha; if(f->alpha == 255) f->special_tiles[j].material_type = MATERIAL_ALPHA_SIMPLE; else f->special_tiles[j].material_type = MATERIAL_ALPHA_VERTEX; + // Material flags f->special_tiles[j].material_flags = 0; - if(f->mspec_special[j].backface_culling) + if(f->tiledef_special[j].backface_culling) f->special_tiles[j].material_flags |= MATERIAL_FLAG_BACKFACE_CULLING; + if(f->tiledef_special[j].animation.type == TAT_VERTICAL_FRAMES) + f->special_tiles[j].material_flags |= MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES; + // Animation parameters + if(f->special_tiles[j].material_flags & + MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES) + { + // Get raw texture size to determine frame count by + // aspect ratio + video::ITexture *t = tsrc->getTextureRaw(f->tiledef_special[j].name); + v2u32 size = t->getOriginalSize(); + int frame_height = (float)size.X / + (float)f->tiledef_special[j].animation.aspect_w * + (float)f->tiledef_special[j].animation.aspect_h; + int frame_count = size.Y / frame_height; + int frame_length_ms = 1000.0 * + f->tiledef_special[j].animation.length / frame_count; + f->special_tiles[j].animation_frame_count = frame_count; + f->special_tiles[j].animation_frame_length_ms = frame_length_ms; + + // If there are no frames for an animation, switch + // animation off (so that having specified an animation + // for something but not using it in the texture pack + // gives no overhead) + if(frame_count == 1){ + f->special_tiles[j].material_flags &= + ~MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES; + } + } } } #endif diff --git a/src/nodedef.h b/src/nodedef.h index 04f375043..a376da30a 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -95,15 +95,33 @@ struct NodeBox struct MapNode; class NodeMetadata; -struct MaterialSpec +/* + Stand-alone definition of a TileSpec (basically a server-side TileSpec) +*/ +enum TileAnimationType{ + TAT_NONE=0, + TAT_VERTICAL_FRAMES=1, +}; +struct TileDef { - std::string tname; - bool backface_culling; - - MaterialSpec(const std::string &tname_="", bool backface_culling_=true): - tname(tname_), - backface_culling(backface_culling_) - {} + std::string name; + bool backface_culling; // Takes effect only in special cases + struct{ + enum TileAnimationType type; + int aspect_w; // width for aspect ratio + int aspect_h; // height for aspect ratio + float length; // seconds + } animation; + + TileDef() + { + name = ""; + backface_culling = true; + animation.type = TAT_NONE; + animation.aspect_w = 1; + animation.aspect_h = 1; + animation.length = 1.0; + } void serialize(std::ostream &os) const; void deSerialize(std::istream &is); @@ -159,8 +177,8 @@ struct ContentFeatures // Visual definition enum NodeDrawType drawtype; float visual_scale; // Misc. scale parameter - std::string tname_tiles[6]; - MaterialSpec mspec_special[CF_SPECIAL_COUNT]; // Use setter methods + TileDef tiledef[6]; + TileDef tiledef_special[CF_SPECIAL_COUNT]; // eg. flowing liquid u8 alpha; // Post effect color, drawn when the camera is inside the node. diff --git a/src/scriptapi.cpp b/src/scriptapi.cpp index afd8546d9..3868d1035 100644 --- a/src/scriptapi.cpp +++ b/src/scriptapi.cpp @@ -426,6 +426,13 @@ struct EnumString es_CraftMethod[] = {0, NULL}, }; +struct EnumString es_TileAnimationType[] = +{ + {TAT_NONE, "none"}, + {TAT_VERTICAL_FRAMES, "vertical_frames"}, + {0, NULL}, +}; + /* C struct <-> Lua table converter functions */ @@ -991,6 +998,50 @@ static ItemDefinition read_item_definition(lua_State *L, int index, return def; } +/* + TileDef +*/ + +static TileDef read_tiledef(lua_State *L, int index) +{ + if(index < 0) + index = lua_gettop(L) + 1 + index; + + TileDef tiledef; + + // key at index -2 and value at index + if(lua_isstring(L, index)){ + // "default_lava.png" + tiledef.name = lua_tostring(L, index); + } + else if(lua_istable(L, index)) + { + // {name="default_lava.png", animation={}} + tiledef.name = ""; + getstringfield(L, index, "name", tiledef.name); + getstringfield(L, index, "image", tiledef.name); // MaterialSpec compat. + tiledef.backface_culling = getboolfield_default( + L, index, "backface_culling", true); + // animation = {} + lua_getfield(L, index, "animation"); + if(lua_istable(L, -1)){ + // {type="vertical_frames", aspect_w=16, aspect_h=16, length=2.0} + tiledef.animation.type = (TileAnimationType) + getenumfield(L, -1, "type", es_TileAnimationType, + TAT_NONE); + tiledef.animation.aspect_w = + getintfield_default(L, -1, "aspect_w", 16); + tiledef.animation.aspect_h = + getintfield_default(L, -1, "aspect_h", 16); + tiledef.animation.length = + getfloatfield_default(L, -1, "length", 1.0); + } + lua_pop(L, 1); + } + + return tiledef; +} + /* ContentFeatures */ @@ -1026,18 +1077,23 @@ static ContentFeatures read_content_features(lua_State *L, int index) f.drawtype = (NodeDrawType)getenumfield(L, index, "drawtype", es_DrawType, NDT_NORMAL); getfloatfield(L, index, "visual_scale", f.visual_scale); - - lua_getfield(L, index, "tile_images"); + + // tiles = {} + lua_getfield(L, index, "tiles"); + // If nil, try the deprecated name "tile_images" instead + if(lua_isnil(L, -1)){ + lua_pop(L, 1); + warn_if_field_exists(L, index, "tile_images", + "Deprecated; new name is \"tiles\"."); + lua_getfield(L, index, "tile_images"); + } if(lua_istable(L, -1)){ int table = lua_gettop(L); lua_pushnil(L); int i = 0; while(lua_next(L, table) != 0){ - // key at index -2 and value at index -1 - if(lua_isstring(L, -1)) - f.tname_tiles[i] = lua_tostring(L, -1); - else - f.tname_tiles[i] = ""; + // Read tiledef from value + f.tiledef[i] = read_tiledef(L, -1); // removes value, keeps key for next iteration lua_pop(L, 1); i++; @@ -1048,29 +1104,31 @@ static ContentFeatures read_content_features(lua_State *L, int index) } // Copy last value to all remaining textures if(i >= 1){ - std::string lastname = f.tname_tiles[i-1]; + TileDef lasttile = f.tiledef[i-1]; while(i < 6){ - f.tname_tiles[i] = lastname; + f.tiledef[i] = lasttile; i++; } } } lua_pop(L, 1); - - lua_getfield(L, index, "special_materials"); + + // special_tiles = {} + lua_getfield(L, index, "special_tiles"); + // If nil, try the deprecated name "special_materials" instead + if(lua_isnil(L, -1)){ + lua_pop(L, 1); + warn_if_field_exists(L, index, "special_materials", + "Deprecated; new name is \"special_tiles\"."); + lua_getfield(L, index, "special_materials"); + } if(lua_istable(L, -1)){ int table = lua_gettop(L); lua_pushnil(L); int i = 0; while(lua_next(L, table) != 0){ - // key at index -2 and value at index -1 - int smtable = lua_gettop(L); - std::string tname = getstringfield_default( - L, smtable, "image", ""); - bool backface_culling = getboolfield_default( - L, smtable, "backface_culling", true); - MaterialSpec mspec(tname, backface_culling); - f.mspec_special[i] = mspec; + // Read tiledef from value + f.tiledef_special[i] = read_tiledef(L, -1); // removes value, keeps key for next iteration lua_pop(L, 1); i++; diff --git a/src/test.cpp b/src/test.cpp index 5b969017f..448ba23ce 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -80,7 +80,7 @@ void define_some_nodes(IWritableItemDefManager *idef, IWritableNodeDefManager *n f = ContentFeatures(); f.name = itemdef.name; for(int i = 0; i < 6; i++) - f.tname_tiles[i] = "default_stone.png"; + f.tiledef[i].name = "default_stone.png"; f.is_ground_content = true; idef->registerItem(itemdef); ndef->set(i, f); @@ -100,10 +100,10 @@ void define_some_nodes(IWritableItemDefManager *idef, IWritableNodeDefManager *n "{default_dirt.png&default_grass_side.png"; f = ContentFeatures(); f.name = itemdef.name; - f.tname_tiles[0] = "default_grass.png"; - f.tname_tiles[1] = "default_dirt.png"; + f.tiledef[0].name = "default_grass.png"; + f.tiledef[1].name = "default_dirt.png"; for(int i = 2; i < 6; i++) - f.tname_tiles[i] = "default_dirt.png^default_grass_side.png"; + f.tiledef[i].name = "default_dirt.png^default_grass_side.png"; f.is_ground_content = true; idef->registerItem(itemdef); ndef->set(i, f); diff --git a/src/tile.cpp b/src/tile.cpp index d2a61b931..92c56c277 100644 --- a/src/tile.cpp +++ b/src/tile.cpp @@ -858,7 +858,7 @@ void TextureSource::buildMainAtlas(class IGameDef *gamedef) const ContentFeatures &f = ndef->get(j); for(u32 i=0; i<6; i++) { - std::string name = f.tname_tiles[i]; + std::string name = f.tiledef[i].name; sourcelist[name] = true; } } @@ -988,7 +988,7 @@ void TextureSource::buildMainAtlas(class IGameDef *gamedef) src_x = pos_in_atlas.X; } s32 y = y0 + pos_in_atlas.Y; - s32 src_y = MYMAX(pos_in_atlas.Y, MYMIN(pos_in_atlas.Y + dim.Height - 1, y)); + s32 src_y = MYMAX((int)pos_in_atlas.Y, MYMIN((int)pos_in_atlas.Y + (int)dim.Height - 1, y)); s32 dst_y = y; video::SColor c = atlas_img->getPixel(src_x, src_y); atlas_img->setPixel(dst_x,dst_y,c); @@ -1638,6 +1638,48 @@ bool generate_image(std::string part_of_name, video::IImage *& baseimg, img2->drop(); } } + /* + [verticalframe:N:I + Crops a frame of a vertical animation. + N = frame count, I = frame index + */ + else if(part_of_name.substr(0,15) == "[verticalframe:") + { + Strfnd sf(part_of_name); + sf.next(":"); + u32 frame_count = stoi(sf.next(":")); + u32 frame_index = stoi(sf.next(":")); + + if(baseimg == NULL){ + errorstream<<"generate_image(): baseimg!=NULL " + <<"for part_of_name=\""<getDimension(); + frame_size.Y /= frame_count; + + video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, + frame_size); + if(!img){ + errorstream<<"generate_image(): Could not create image " + <<"for part_of_name=\""< dim = frame_size; + core::position2d pos_dst(0, 0); + core::position2d pos_src(0, frame_index * frame_size.Y); + baseimg->copyToWithAlpha(img, pos_dst, + core::rect(pos_src, dim), + video::SColor(255,255,255,255), + NULL); + // Replace baseimg + baseimg->drop(); + baseimg = img; + } else { errorstream<<"generate_image(): Invalid " diff --git a/src/tile.h b/src/tile.h index 1211569bc..8b19b4c32 100644 --- a/src/tile.h +++ b/src/tile.h @@ -162,6 +162,9 @@ enum MaterialType{ // Should the crack be drawn on transparent pixels (unset) or not (set)? // Ignored if MATERIAL_FLAG_CRACK is not set. #define MATERIAL_FLAG_CRACK_OVERLAY 0x04 +// Animation made up by splitting the texture to vertical frames, as +// defined by extra parameters +#define MATERIAL_FLAG_ANIMATION_VERTICAL_FRAMES 0x08 /* This fully defines the looks of a tile. @@ -178,7 +181,9 @@ struct TileSpec material_flags( //0 // <- DEBUG, Use the one below MATERIAL_FLAG_BACKFACE_CULLING - ) + ), + animation_frame_count(1), + animation_frame_length_ms(0) { } @@ -227,10 +232,12 @@ struct TileSpec AtlasPointer texture; // Vertex alpha u8 alpha; - // Material type + // Material parameters u8 material_type; - // Material flags u8 material_flags; + // Animation parameters + u8 animation_frame_count; + u16 animation_frame_length_ms; }; #endif -- cgit v1.2.3