#include "extract_shrub.h" #include #include "common/log/log.h" #include "common/util/FileUtil.h" #include "decompiler/ObjectFile/LinkedObjectFile.h" #include "decompiler/level_extractor/extract_common.h" namespace decompiler { using namespace level_tools; std::array extract_shrub_matrix(const u16* data) { std::array result; for (int i = 0; i < 4; i++) { s32 x = data[12 + i]; x <<= 16; x >>= 10; result[3][i] = x; } for (int vec = 0; vec < 3; vec++) { for (int i = 0; i < 4; i++) { s32 x = data[vec * 4 + i]; x <<= 16; x >>= 16; result[vec][i] = (float)x / 4096.f; } } return result; } struct ShrubVertex { math::Vector xyz; math::Vector st; math::Vector rgba_generic; bool adc = false; }; struct DrawSettings { DrawMode mode; u32 combo_tex; }; struct ShrubDraw { u32 start_vtx_idx = -1; AdGifData adgif; DrawSettings settings; std::vector vertices; }; struct ShrubFrag { std::vector draws; }; struct ShrubInstanceInfo { u32 proto_idx; u32 color_idx; std::array mat; math::Vector4f bsphere; }; struct ShrubProtoInfo { std::vector frags; std::vector instances; }; std::string debug_dump_proto_to_obj(const ShrubProtoInfo& proto) { std::vector> verts; std::vector> tcs; std::vector> faces; for (auto& frag : proto.frags) { for (auto& strip : frag.draws) { // add verts... ASSERT(strip.vertices.size() >= 3); int vert_idx = 0; int vtx_idx_queue[3]; int q_idx = 0; int startup = 0; while (vert_idx < (int)strip.vertices.size()) { verts.push_back(strip.vertices.at(vert_idx).xyz / 65536); // no idea tcs.push_back(math::Vector{strip.vertices.at(vert_idx).st.x(), strip.vertices.at(vert_idx).st.y()}); vtx_idx_queue[q_idx++] = verts.size(); // wrap the index if (q_idx == 3) { q_idx = 0; } // bump the startup if (startup < 3) { startup++; } if (startup >= 3 && strip.vertices.at(vert_idx).adc) { faces.push_back( math::Vector{vtx_idx_queue[0], vtx_idx_queue[1], vtx_idx_queue[2]}); } vert_idx++; } } } std::string result; for (auto& vert : verts) { result += fmt::format("v {} {} {}\n", vert.x(), vert.y(), vert.z()); } for (auto& tc : tcs) { result += fmt::format("vt {} {}\n", tc.x(), tc.y()); } for (auto& face : faces) { result += fmt::format("f {}/{} {}/{} {}/{}\n", face.x(), face.x(), face.y(), face.y(), face.z(), face.z()); } return result; } namespace { /*! * adgif shader texture id's can be "remapped". I think it allows textures to be shared. * So far we haven't seen this feature used, but we do have the texture map and we check it here. */ u32 remap_texture(u32 original, const std::vector& map) { auto masked = original & 0xffffff00; for (auto& t : map) { if (t.original_texid == masked) { ASSERT_MSG(false, "OKAY! remapped!"); return t.new_texid | 20; } } return original; } } // namespace DrawSettings adgif_to_draw_mode(const AdGifData& ad, const TextureDB& tdb, const std::vector& map, int count, bool alpha_tpage_flag) { // initialize draw mode DrawMode current_mode; current_mode.set_at(true); current_mode.set_alpha_test(DrawMode::AlphaTest::GEQUAL); current_mode.set_aref(0x26); current_mode.set_alpha_fail(GsTest::AlphaFail::KEEP); current_mode.set_zt(true); current_mode.set_depth_test(GsTest::ZTest::GEQUAL); current_mode.set_depth_write_enable(true); // todo, is this actual true current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_SRC_SRC_SRC); current_mode.enable_fog(); current_mode.set_ab(false); if (alpha_tpage_flag) { current_mode.set_alpha_test(DrawMode::AlphaTest::NEVER); current_mode.set_aref(0); current_mode.set_alpha_fail(GsTest::AlphaFail::FB_ONLY); current_mode.set_ab(true); } // ADGIF 0 bool weird = (u8)ad.tex0_addr != (u32)GsRegisterAddress::TEX0_1; if (weird) { lg::info("---------------- WEIRD: 0x{:x}", ad.tex0_addr); lg::info("i have {} verts", count); } else { if (ad.tex0_data == 0) { current_mode.set_decal(false); } else if (ad.tex0_data == 0x8'0000'0000) { current_mode.set_decal(true); } else { ASSERT(false); } } // tw/th // ADGIF 1 ASSERT((u8)ad.tex1_addr == (u32)GsRegisterAddress::TEX1_1); u32 original_tex = ad.tex1_addr; u32 new_tex = remap_texture(original_tex, map); // try remapping it if (original_tex != new_tex) { lg::info("map from 0x{:x} to 0x{:x}", original_tex, new_tex); } // texture the texture page/texture index, and convert to a PC port texture ID u32 tpage = new_tex >> 20; u32 tidx = (new_tex >> 8) & 0b1111'1111'1111; u32 tex_combo = (((u32)tpage) << 16) | tidx; // look up the texture to make sure it's valid auto tex = tdb.textures.find(tex_combo); ASSERT(tex != tdb.textures.end()); if (weird) { lg::info("tex: {}", tex->second.name); } // ADGIF 2 ASSERT((u8)ad.mip_addr == (u32)GsRegisterAddress::MIPTBP1_1); // ADGIF 3 ASSERT((u8)ad.clamp_addr == (u32)GsRegisterAddress::CLAMP_1); { bool clamp_s = ad.clamp_data & 0b001; bool clamp_t = ad.clamp_data & 0b100; current_mode.set_clamp_s_enable(clamp_s); current_mode.set_clamp_t_enable(clamp_t); } u64 final_alpha; // ADGIF 4 if ((u8)ad.alpha_addr == (u32)GsRegisterAddress::ALPHA_1) { final_alpha = ad.alpha_data; } else { ASSERT(false); // ASSERT((u8)ad.alpha_addr == (u32)GsRegisterAddress::MIPTBP2_1); } GsAlpha reg(final_alpha); auto a = reg.a_mode(); auto b = reg.b_mode(); auto c = reg.c_mode(); auto d = reg.d_mode(); if (a == GsAlpha::BlendMode::SOURCE && b == GsAlpha::BlendMode::DEST && c == GsAlpha::BlendMode::SOURCE && d == GsAlpha::BlendMode::DEST) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_SRC_DST); } else if (a == GsAlpha::BlendMode::SOURCE && b == GsAlpha::BlendMode::ZERO_OR_FIXED && c == GsAlpha::BlendMode::SOURCE && d == GsAlpha::BlendMode::DEST) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_0_SRC_DST); } else if (a == GsAlpha::BlendMode::ZERO_OR_FIXED && b == GsAlpha::BlendMode::SOURCE && c == GsAlpha::BlendMode::SOURCE && d == GsAlpha::BlendMode::DEST) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::ZERO_SRC_SRC_DST); } else if (a == GsAlpha::BlendMode::SOURCE && b == GsAlpha::BlendMode::DEST && c == GsAlpha::BlendMode::ZERO_OR_FIXED && d == GsAlpha::BlendMode::DEST) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_DST_FIX_DST); } else if (a == GsAlpha::BlendMode::SOURCE && b == GsAlpha::BlendMode::SOURCE && c == GsAlpha::BlendMode::SOURCE && d == GsAlpha::BlendMode::SOURCE) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_SRC_SRC_SRC); } else if (a == GsAlpha::BlendMode::SOURCE && b == GsAlpha::BlendMode::ZERO_OR_FIXED && c == GsAlpha::BlendMode::DEST && d == GsAlpha::BlendMode::DEST) { current_mode.set_alpha_blend(DrawMode::AlphaBlend::SRC_0_DST_DST); } else { // unsupported blend: a 0 b 2 c 2 d 1 // lg::error("unsupported blend: a {} b {} c {} d {}", (int)a, (int)b, (int)c, (int)d); // ASSERT(false); } return {current_mode, tex_combo}; } ShrubProtoInfo extract_proto(const shrub_types::PrototypeBucketShrub& proto, const TextureDB& tdb, const std::vector& map, GameVersion version) { ShrubProtoInfo result; for (int frag_idx = 0; frag_idx < proto.generic_geom.length; frag_idx++) { auto& frag_out = result.frags.emplace_back(); auto& frag = proto.generic_geom.shrubs.at(frag_idx); std::vector adgif_data; adgif_data.resize(frag.textures.size() / sizeof(AdGifData)); memcpy(adgif_data.data(), frag.textures.data(), frag.textures.size()); if (frag_idx == 0 && proto.name == "vil2-cattail.mb") { lg::info("Skipping broken village2 thing"); continue; } for (size_t i = 0; i < adgif_data.size(); i++) { auto& draw = frag_out.draws.emplace_back(); draw.adgif = adgif_data[i]; const auto& ag = adgif_data[i]; int count = (ag.tex1_addr >> 32) & 0xfff; // drop the eop flag draw.start_vtx_idx = ((ag.tex0_addr >> 32) & 0xffff) / 3; if (i > 0) { auto& prev_draw = frag_out.draws[frag_out.draws.size() - 2]; ASSERT(prev_draw.start_vtx_idx + prev_draw.vertices.size() + 3 == draw.start_vtx_idx); } for (int vert_idx = 0; vert_idx < count; vert_idx++) { auto& vert_out = draw.vertices.emplace_back(); s16 vert_data[3]; memcpy(vert_data, frag.vtx.data() + sizeof(u16) * 3 * (vert_idx + draw.start_vtx_idx), 3 * sizeof(u16)); vert_out.xyz = math::Vector3f(vert_data[0], vert_data[1], vert_data[2]); s16 st_data[2]; memcpy(st_data, frag.stq.data() + sizeof(u16) * 2 * (vert_idx + draw.start_vtx_idx), 2 * sizeof(u16)); vert_out.st = math::Vector2f(st_data[0], st_data[1]); vert_out.adc = (st_data[0] & 1) == 0; // adc in the low bit of texture coordinate memcpy(vert_out.rgba_generic.data(), frag.col.data() + 3 * (vert_idx + draw.start_vtx_idx), 3); ASSERT(3 * (vert_idx + draw.start_vtx_idx) + 3 <= frag.col.size()); } bool alpha_tpage_flag = false; if (version > GameVersion::Jak1) { alpha_tpage_flag = proto.flags & 0x4; // tpage-alpha } draw.settings = adgif_to_draw_mode(ag, tdb, map, count, alpha_tpage_flag); } ASSERT(frag.vtx_cnt * 3 * sizeof(u16) <= frag.vtx.size()); } /* file_util::write_text_file( file_util::get_file_path({fmt::format("debug_out/shrub/{}.obj", proto.name)}), debug_dump_proto_to_obj(result)); */ return result; } void extract_instance(const shrub_types::InstanceShrubbery& inst, std::vector& protos) { ShrubInstanceInfo result; result.proto_idx = inst.bucket_index; for (int i = 0; i < 4; i++) { result.bsphere[i] = inst.bsphere.data[i]; } // from ee asm result.mat = extract_shrub_matrix(inst.origin.data); result.mat[3][0] += result.bsphere[0]; result.mat[3][1] += result.bsphere[1]; result.mat[3][2] += result.bsphere[2]; // result.wind_index = instance.wind_index; result.mat[0][3] = 0.f; result.color_idx = inst.color_indices / 4; protos.at(result.proto_idx).instances.push_back(result); } /*! * Transform a point in a prototype to the actual point location in the game world. */ math::Vector transform_shrub(const std::array mat, const math::Vector3f& pt) { auto temp = mat[0] * pt.x() + mat[1] * pt.y() + mat[2] * pt.z() + mat[3]; math::Vector3f result; result.x() = temp.x(); result.y() = temp.y(); result.z() = temp.z(); return result; } /*! * Dump the entire tie tree to an obj. Used to debug the transform_tie function. If we get this * right, it should fit in with .obj's produced from the tfrag debug. */ std::string dump_full_to_obj(const std::vector& protos) { std::vector> verts; std::vector> faces; for (auto& proto : protos) { for (auto& inst : proto.instances) { auto& mat = inst.mat; for (auto& frag : proto.frags) { for (auto& strip : frag.draws) { // add verts... ASSERT(strip.vertices.size() >= 3); int vert_idx = 0; int vtx_idx_queue[3]; int q_idx = 0; int startup = 0; while (vert_idx < (int)strip.vertices.size()) { verts.push_back(transform_shrub(mat, strip.vertices.at(vert_idx).xyz) / 65536); // no idea vtx_idx_queue[q_idx++] = verts.size(); // wrap the index if (q_idx == 3) { q_idx = 0; } // bump the startup if (startup < 3) { startup++; } if (startup >= 3 && strip.vertices.at(vert_idx).adc) { faces.push_back( math::Vector{vtx_idx_queue[0], vtx_idx_queue[1], vtx_idx_queue[2]}); } vert_idx++; } } } } } std::string result; for (auto& vert : verts) { result += fmt::format("v {} {} {}\n", vert.x(), vert.y(), vert.z()); } for (auto& face : faces) { result += fmt::format("f {}/{} {}/{} {}/{}\n", face.x(), face.x(), face.y(), face.y(), face.z(), face.z()); } return result; } void make_draws(tfrag3::Level& lev, tfrag3::ShrubTree& tree_out, const std::vector& protos, const TextureDB& tdb) { std::vector> indices_regrouped_by_draw; std::unordered_map> static_draws_by_tex; size_t global_vert_counter = 0; for (u32 proto_idx = 0; proto_idx < protos.size(); proto_idx++) { auto& proto = protos[proto_idx]; // packed_vert_indices[frag][draw] = {start, end} std::vector>> packed_vert_indices; for (size_t frag_idx = 0; frag_idx < proto.frags.size(); frag_idx++) { auto& frag_inds = packed_vert_indices.emplace_back(); auto& frag = proto.frags[frag_idx]; for (auto& draw : frag.draws) { int start = tree_out.packed_vertices.vertices.size(); for (auto& vert : draw.vertices) { tree_out.packed_vertices.vertices.push_back( {vert.xyz.x(), vert.xyz.y(), vert.xyz.z(), vert.st.x(), vert.st.y(), {vert.rgba_generic[0], vert.rgba_generic[1], vert.rgba_generic[2]}}); } int end = tree_out.packed_vertices.vertices.size(); frag_inds.emplace_back(start, end); } } for (auto& inst : proto.instances) { u32 matrix_idx = tree_out.packed_vertices.matrices.size(); tree_out.packed_vertices.matrices.push_back(inst.mat); for (size_t frag_idx = 0; frag_idx < proto.frags.size(); frag_idx++) { auto& frag = proto.frags[frag_idx]; // shared info for all instances of this frag for (size_t draw_idx = 0; draw_idx < frag.draws.size(); draw_idx++) { auto& draw = frag.draws[draw_idx]; // what texture are we using? u32 combo_tex = draw.settings.combo_tex; // try looking it up in the existing textures that we have in the C++ renderer data. // (this is shared with tfrag) u32 idx_in_lev_data = UINT32_MAX; for (u32 i = 0; i < lev.textures.size(); i++) { if (lev.textures[i].combo_id == combo_tex) { idx_in_lev_data = i; break; } } if (idx_in_lev_data == UINT32_MAX) { // didn't find it, have to add a new one texture. auto tex_it = tdb.textures.find(combo_tex); if (tex_it == tdb.textures.end()) { bool ok_to_miss = false; // for TIE, there's no missing textures. if (ok_to_miss) { // we're missing a texture, just use the first one. tex_it = tdb.textures.begin(); } else { ASSERT_MSG( false, fmt::format( "texture {} wasn't found. make sure it is loaded somehow. You may need to " "include ART.DGO or GAME.DGO in addition to the level DGOs for shared " "textures. tpage is {} id is {} (0x{:x})", combo_tex, combo_tex >> 16, combo_tex & 0xffff, combo_tex & 0xffff)); } } // add a new texture to the level data idx_in_lev_data = lev.textures.size(); lev.textures.emplace_back(); auto& new_tex = lev.textures.back(); new_tex.combo_id = combo_tex; new_tex.w = tex_it->second.w; new_tex.h = tex_it->second.h; new_tex.debug_name = tex_it->second.name; new_tex.debug_tpage_name = tdb.tpage_names.at(tex_it->second.page); new_tex.data = tex_it->second.rgba_bytes; } DrawMode mode = draw.settings.mode; // okay, we now have a texture and draw mode, let's see if we can add to an existing... auto existing_draws_in_tex = static_draws_by_tex.find(idx_in_lev_data); tfrag3::ShrubDraw* draw_to_add_to = nullptr; std::vector* verts_to_add_to = nullptr; if (existing_draws_in_tex != static_draws_by_tex.end()) { for (auto idx : existing_draws_in_tex->second) { auto& candidate_draw_out = tree_out.static_draws.at(idx); if (candidate_draw_out.mode == mode && (!tree_out.has_per_proto_visibility_toggle || candidate_draw_out.proto_idx == proto_idx)) { draw_to_add_to = &tree_out.static_draws[idx]; verts_to_add_to = &indices_regrouped_by_draw[idx]; } } } if (!draw_to_add_to) { // nope, need to create a new draw tree_out.static_draws.emplace_back(); static_draws_by_tex[idx_in_lev_data].push_back(tree_out.static_draws.size() - 1); draw_to_add_to = &tree_out.static_draws.back(); draw_to_add_to->mode = mode; draw_to_add_to->tree_tex_id = idx_in_lev_data; if (tree_out.has_per_proto_visibility_toggle) { draw_to_add_to->proto_idx = proto_idx; } verts_to_add_to = &indices_regrouped_by_draw.emplace_back(); } // now we have a draw, time to add vertices // draw_to_add_to->num_triangles += draw.vertices.size() - 2; tfrag3::PackedShrubVertices::InstanceGroup grp; grp.matrix_idx = matrix_idx; grp.color_index = inst.color_idx; grp.start_vert = packed_vert_indices.at(frag_idx).at(draw_idx).first; grp.end_vert = packed_vert_indices.at(frag_idx).at(draw_idx).second; tree_out.packed_vertices.instance_groups.push_back(grp); for (size_t vidx = 0; vidx < draw.vertices.size(); vidx++) { if (draw.vertices[vidx].adc) { verts_to_add_to->push_back(vidx + global_vert_counter); draw_to_add_to->num_triangles++; } else { verts_to_add_to->push_back(UINT32_MAX); verts_to_add_to->push_back(vidx + global_vert_counter - 1); verts_to_add_to->push_back(vidx + global_vert_counter); } } verts_to_add_to->push_back(UINT32_MAX); global_vert_counter += draw.vertices.size(); } } } } for (size_t didx = 0; didx < tree_out.static_draws.size(); didx++) { auto& draw = tree_out.static_draws[didx]; auto& inds = indices_regrouped_by_draw[didx]; draw.num_triangles = clean_up_vertex_indices(inds); draw.num_indices = inds.size(); draw.first_index_index = tree_out.indices.size(); tree_out.indices.insert(tree_out.indices.end(), inds.begin(), inds.end()); } tree_out.packed_vertices.total_vertex_count = global_vert_counter; } void extract_shrub(const shrub_types::DrawableTreeInstanceShrub* tree, const std::string& debug_name, const std::vector& map, const TextureDB& tex_db, const std::vector>& /*expected_missing_textures*/, tfrag3::Level& out, bool dump_level, GameVersion version) { auto& tree_out = out.shrub_trees.emplace_back(); if (version > GameVersion::Jak1) { tree_out.has_per_proto_visibility_toggle = true; } auto& protos = tree->info.prototype_inline_array_shrub; std::vector proto_info; for (auto& proto : protos.data) { proto_info.push_back(extract_proto(proto, tex_db, map, version)); tree_out.proto_names.push_back(proto.name); } for (auto& arr : tree->discovered_arrays) { auto as_shrubs = dynamic_cast(arr.get()); if (as_shrubs) { for (auto& inst : as_shrubs->instances) { extract_instance(inst, proto_info); } } } // time of day colors tree_out.time_of_day_colors = pack_colors(tree->time_of_day); make_draws(out, tree_out, proto_info, tex_db); if (dump_level) { auto path = file_util::get_file_path({fmt::format("debug_out/shrub_all/{}.obj", debug_name)}); file_util::create_dir_if_needed_for_file(path); file_util::write_text_file(path, dump_full_to_obj(proto_info)); } } } // namespace decompiler