[wip] build actor tool (#3266)

This does a couple of things:

- The `custom_levels` folder was renamed to `custom_assets` and contains
`levels`, `models` and `texture_replacements` folders for Jak 1, 2 and 3
in order to keep everything regarding custom stuff in one place.
- With this, texture replacements now use separate folders for all games
- A build actor tool was added that generates art groups for custom
actors
- Custom levels can now specify what custom models from the `models`
folder they want to import, this will add them to the level's FR3.
- A `test-zone-obs.gc` file was added, containing a `test-actor` process
that uses a custom model as an example.

The build actor tool is still very WIP, the joints and the default
animation are hardcoded, but it allows for importing any GLB file as a
merc model.
This commit is contained in:
Hat Kid 2024-05-18 18:18:25 +02:00 committed by GitHub
parent 271007e552
commit 62ef9fe49d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1766 additions and 556 deletions

View File

@ -32,4 +32,4 @@ cp -r $SOURCE/decompiler/config $DEST/data/decompiler/
cp -r $SOURCE/goal_src $DEST/data
cp -r $SOURCE/game/assets $DEST/data/game/
cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer
cp -r $SOURCE/custom_levels $DEST/data
cp -r $SOURCE/custom_assets $DEST/data

View File

@ -24,4 +24,4 @@ cp -r $SOURCE/decompiler/config $DEST/data/decompiler/
cp -r $SOURCE/goal_src $DEST/data
cp -r $SOURCE/game/assets $DEST/data/game/
cp -r $SOURCE/game/graphics/opengl_renderer/shaders $DEST/data/game/graphics/opengl_renderer
cp -r $SOURCE/custom_levels $DEST/data
cp -r $SOURCE/custom_assets $DEST/data

4
.gitignore vendored
View File

@ -54,7 +54,9 @@ imgui.ini
node_modules/
# texture replacements
texture_replacements/*
custom_assets/jak1/texture_replacements/*
custom_assets/jak2/texture_replacements/*
custom_assets/jak3/texture_replacements/*
# generated cmake files
svnrev.h

View File

@ -87,9 +87,11 @@ add_library(common
util/term_util.cpp
util/Timer.cpp
util/unicode_util.cpp
versions/versions.cpp)
util/gltf_util.cpp
versions/versions.cpp
)
target_link_libraries(common fmt lzokay replxx libzstd_static tree-sitter sqlite3 libtinyfiledialogs)
target_link_libraries(common fmt lzokay replxx libzstd_static tree-sitter sqlite3 libtinyfiledialogs tiny_gltf)
if(WIN32)
target_link_libraries(common wsock32 ws2_32 windowsapp)

467
common/util/gltf_util.cpp Normal file
View File

@ -0,0 +1,467 @@
#include "gltf_util.h"
#include "common/log/log.h"
namespace gltf_util {
/*!
* Convert a GLTF position buffer or similar to std::vector<Vec3f>
*/
std::vector<math::Vector3f> extract_vec3f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector3f> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
memcpy(&result.emplace_back(), data, sizeof(math::Vector3f));
data += stride;
}
return result;
}
std::vector<math::Vector2f> extract_vec2f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector2f> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
memcpy(&result.emplace_back(), data, sizeof(math::Vector2f));
data += stride;
}
return result;
}
/*!
* Convert a GLTF color buffer (float format) to u8 colors.
*/
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_float(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<float, 4> temp;
memcpy(&temp, data, sizeof(math::Vector<float, 4>));
data += stride;
result.emplace_back(temp.x() * 255, temp.y() * 255, temp.z() * 255, temp.w() * 255);
}
return result;
}
/*!
* Convert a GLTF color buffer (u16 format) to u8 colors.
*/
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<u16, 4> temp;
memcpy(&temp, data, sizeof(math::Vector<u16, 4>));
data += stride;
result.emplace_back(temp.x() >> 8, temp.y() >> 8, temp.z() >> 8, temp.w() >> 8);
}
return result;
}
/*!
* Convert a GLTF index buffer
*/
std::vector<u32> gltf_index_buffer(const tinygltf::Model& model,
int indices_idx,
u32 index_offset) {
const auto& indices_accessor = model.accessors[indices_idx];
const auto& buffer_view = model.bufferViews[indices_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + indices_accessor.byteOffset;
const auto stride = indices_accessor.ByteStride(buffer_view);
const auto count = indices_accessor.count;
switch (indices_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_BYTE:
return index_list_to_u32<s8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
return index_list_to_u32<u8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_SHORT:
return index_list_to_u32<s16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
return index_list_to_u32<u16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_INT:
return index_list_to_u32<s32>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
return index_list_to_u32<u32>(data_ptr, count, index_offset, stride);
default:
ASSERT_MSG(false, "unsupported component type");
}
}
/*!
* Extract positions, colors, and normals from a mesh.
*/
ExtractedVertices gltf_vertices(const tinygltf::Model& model,
const std::map<std::string, int>& attributes,
const math::Matrix4f& w_T_local,
bool get_colors,
bool get_normals,
const std::string& debug_name) {
std::vector<tfrag3::PreloadedVertex> result;
std::vector<math::Vector<u8, 4>> vtx_colors;
{
const auto& position_attrib = attributes.find("POSITION");
ASSERT_MSG(position_attrib != attributes.end(), "Did not find position attribute.");
const auto attrib_accessor = model.accessors[position_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "POSITION wasn't vec3");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"POSITION wasn't float");
// for (auto& attrib : attributes) {
// lg::print("attrib: {}\n", attrib.first);
//}
auto mesh_verts = extract_vec3f(data_ptr, count, byte_stride);
result.reserve(mesh_verts.size());
for (auto& vert : mesh_verts) {
auto& new_vert = result.emplace_back();
math::Vector4f v_in(vert.x(), vert.y(), vert.z(), 1);
math::Vector4f v_w = w_T_local * v_in;
new_vert.x = v_w.x() * 4096;
new_vert.y = v_w.y() * 4096;
new_vert.z = v_w.z() * 4096;
}
}
if (get_colors) {
const auto& color_attrib = attributes.find("COLOR_0");
if (color_attrib == attributes.end()) {
lg::error("Mesh {} didn't have any colors, using white", debug_name);
for (size_t i = 0; i < result.size(); i++) {
vtx_colors.emplace_back(0x80, 0x80, 0x80, 0xff);
}
} else {
const auto attrib_accessor = model.accessors[color_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC4, "COLOR_0 wasn't vec4");
std::vector<math::Vector<u8, 4>> colors;
switch (attrib_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_FLOAT:
colors = extract_color_from_vec4_float(data_ptr, count, byte_stride);
break;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride);
break;
default:
lg::die("Unknown component type for COLOR_0: {}", attrib_accessor.componentType);
}
vtx_colors.insert(vtx_colors.end(), colors.begin(), colors.end());
}
}
bool got_texture = false;
{
const auto& texcoord_attrib = attributes.find("TEXCOORD_0");
if (texcoord_attrib != attributes.end()) {
const auto attrib_accessor = model.accessors[texcoord_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC2, "TEXCOORD wasn't vec2");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"TEXCOORD wasn't float");
auto mesh_verts = extract_vec2f(data_ptr, count, byte_stride);
ASSERT(mesh_verts.size() == result.size());
got_texture = true;
for (size_t i = 0; i < mesh_verts.size(); i++) {
result[i].s = mesh_verts[i].x();
result[i].t = mesh_verts[i].y();
}
} else {
if (!get_normals) {
// don't warn if we're just getting collision
lg::warn("No texcoord attribute for mesh: {}", debug_name);
}
}
}
std::vector<math::Vector3f> normals;
if (get_normals) {
const auto& normal_attrib = attributes.find("NORMAL");
if (normal_attrib != attributes.end()) {
const auto attrib_accessor = model.accessors[normal_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "NORMAL wasn't vec3");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"NORMAL wasn't float");
normals = extract_vec3f(data_ptr, count, byte_stride);
for (auto& nrm : normals) {
math::Vector4f nrm4(nrm.x(), nrm.y(), nrm.z(), 0.f);
nrm = (w_T_local * nrm4).xyz();
}
ASSERT(normals.size() == result.size());
} else {
lg::error("No NORMAL attribute for mesh: {}", debug_name);
}
}
for (auto& v : result) {
v.color_index = 0;
if (!got_texture) {
v.s = 0;
v.t = 0;
}
}
// TODO: other properties
return {result, vtx_colors, normals};
}
DrawMode make_default_draw_mode() {
DrawMode mode;
mode.set_depth_write_enable(true);
mode.set_depth_test(GsTest::ZTest::GEQUAL);
mode.set_alpha_blend(DrawMode::AlphaBlend::DISABLED);
mode.set_aref(0);
mode.set_alpha_fail(GsTest::AlphaFail::KEEP);
mode.set_clamp_s_enable(false);
mode.set_clamp_t_enable(false);
mode.disable_filt(); // for checkerboard...
mode.enable_tcc(); // ?
mode.disable_at();
mode.enable_zt();
mode.disable_ab();
mode.disable_decal();
mode.enable_fog();
return mode;
}
int texture_pool_debug_checker(TexturePool* pool) {
const auto& existing = pool->textures_by_name.find("DEBUG_CHECKERBOARD");
if (existing == pool->textures_by_name.end()) {
size_t idx = pool->textures_by_idx.size();
pool->textures_by_name["DEBUG_CHECKERBOARD"] = idx;
auto& tex = pool->textures_by_idx.emplace_back();
tex.w = 16;
tex.h = 16;
tex.debug_name = "DEBUG_CHECKERBOARD";
tex.debug_tpage_name = "DEBUG";
tex.load_to_pool = false;
tex.combo_id = 0; // doesn't matter, not a pool tex
tex.data.resize(16 * 16);
u32 c0 = 0xa0303030;
u32 c1 = 0xa0e0e0e0;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
tex.data[i * 16 + j] = (((i / 4) & 1) ^ ((j / 4) & 1)) ? c1 : c0;
}
}
return idx;
} else {
return existing->second;
}
}
int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex) {
const auto& existing = pool->textures_by_name.find(tex.name);
if (existing != pool->textures_by_name.end()) {
lg::info("Reusing image: {}", tex.name);
return existing->second;
} else {
lg::info("adding new texture: {}, size {} kB", tex.name, tex.width * tex.height * 4 / 1024);
}
ASSERT(tex.bits == 8);
ASSERT(tex.component == 4);
ASSERT(tex.pixel_type == TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE);
size_t idx = pool->textures_by_idx.size();
pool->textures_by_name[tex.name] = idx;
auto& tt = pool->textures_by_idx.emplace_back();
tt.w = tex.width;
tt.h = tex.height;
tt.debug_name = tex.name;
tt.debug_tpage_name = "custom-level";
tt.load_to_pool = false;
tt.combo_id = 0; // doesn't matter, not a pool tex
tt.data.resize(tt.w * tt.h);
ASSERT(tex.image.size() >= tt.data.size());
memcpy(tt.data.data(), tex.image.data(), tt.data.size() * 4);
return idx;
}
math::Matrix4f affine_translation(const math::Vector3f& translation) {
math::Matrix4f result = math::Matrix4f::identity();
result(0, 3) = translation[0];
result(1, 3) = translation[1];
result(2, 3) = translation[2];
result(3, 3) = 1;
return result;
}
math::Matrix4f affine_scale(const math::Vector3f& scale) {
math::Matrix4f result = math::Matrix4f::zero();
result(0, 0) = scale[0];
result(1, 1) = scale[1];
result(2, 2) = scale[2];
result(3, 3) = 1;
return result;
}
math::Matrix4f affine_rot_qxyzw(const math::Vector4f& quat) {
math::Matrix4f result = math::Matrix4f::zero();
result(3, 3) = 1;
result(0, 0) = 1.0 - 2.0 * (quat.y() * quat.y() + quat.z() * quat.z());
result(0, 1) = 2.0 * (quat.x() * quat.y() - quat.z() * quat.w());
result(0, 2) = 2.0 * (quat.x() * quat.z() + quat.y() * quat.w());
result(1, 0) = 2.0 * (quat.x() * quat.y() + quat.z() * quat.w());
result(1, 1) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.z() * quat.z());
result(1, 2) = 2.0 * (quat.y() * quat.z() - quat.x() * quat.w());
result(2, 0) = 2.0 * (quat.x() * quat.z() - quat.y() * quat.w());
result(2, 1) = 2.0 * (quat.y() * quat.z() + quat.x() * quat.w());
result(2, 2) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.y() * quat.y());
return result;
}
math::Vector3f vector3f_from_gltf(const std::vector<double>& in) {
ASSERT(in.size() == 3);
return math::Vector3f{in[0], in[1], in[2]};
}
math::Vector4f vector4f_from_gltf(const std::vector<double>& in) {
ASSERT(in.size() == 4);
return math::Vector4f{in[0], in[1], in[2], in[3]};
}
math::Matrix4f matrix_from_node(const tinygltf::Node& node) {
if (!node.matrix.empty()) {
math::Matrix4f result;
for (int i = 0; i < 16; i++) {
result.data()[i] = node.matrix[i];
}
return result;
} else {
// from trs
math::Matrix4f t, r, s;
if (!node.translation.empty()) {
t = affine_translation(vector3f_from_gltf(node.translation));
} else {
t = math::Matrix4f::identity();
}
if (!node.rotation.empty()) {
r = affine_rot_qxyzw(vector4f_from_gltf(node.rotation));
} else {
r = math::Matrix4f::identity();
}
if (!node.scale.empty()) {
s = affine_scale(vector3f_from_gltf(node.scale));
} else {
s = math::Matrix4f::identity();
}
return t * r * s;
}
}
/*!
* Recursively walk the tree of nodes, flatten, and compute w_T_node for each.
*/
void node_find_helper(const tinygltf::Model& model,
const math::Matrix4f& w_T_parent,
int node_idx,
std::vector<NodeWithTransform>* out) {
const auto& node = model.nodes.at(node_idx);
math::Matrix4f w_T_node = w_T_parent * matrix_from_node(node);
out->push_back({node_idx, w_T_node});
for (auto& child : node.children) {
node_find_helper(model, w_T_node, child, out);
}
}
std::vector<NodeWithTransform> flatten_nodes_from_all_scenes(const tinygltf::Model& model) {
std::vector<NodeWithTransform> out;
for (auto& scene : model.scenes) {
for (auto& nidx : scene.nodes) {
math::Matrix4f identity = math::Matrix4f::identity();
node_find_helper(model, identity, nidx, &out);
}
}
return out;
}
void dedup_vertices(const std::vector<tfrag3::PreloadedVertex>& vertices_in,
std::vector<tfrag3::PreloadedVertex>& vertices_out,
std::vector<u32>& old_to_new_out) {
ASSERT(vertices_out.empty());
ASSERT(old_to_new_out.empty());
old_to_new_out.resize(vertices_in.size(), -1);
std::unordered_map<tfrag3::PreloadedVertex, u32, tfrag3::PreloadedVertex::hash> vtx_to_new;
for (size_t in_idx = 0; in_idx < vertices_in.size(); in_idx++) {
auto& vtx = vertices_in[in_idx];
const auto& lookup = vtx_to_new.find(vtx);
if (lookup == vtx_to_new.end()) {
// first time seeing this one
size_t new_idx = vertices_out.size();
vertices_out.push_back(vtx);
old_to_new_out[in_idx] = new_idx;
vtx_to_new[vtx] = new_idx;
} else {
old_to_new_out[in_idx] = lookup->second;
}
}
}
DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler) {
DrawMode mode = make_default_draw_mode();
if (sampler.magFilter == TINYGLTF_TEXTURE_FILTER_NEAREST) {
ASSERT(sampler.minFilter == TINYGLTF_TEXTURE_FILTER_NEAREST);
mode.set_filt_enable(false);
} else {
ASSERT(sampler.minFilter != TINYGLTF_TEXTURE_FILTER_NEAREST);
mode.set_filt_enable(true);
}
switch (sampler.wrapS) {
case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
mode.set_clamp_s_enable(true);
break;
case TINYGLTF_TEXTURE_WRAP_REPEAT:
mode.set_clamp_s_enable(false);
break;
default:
ASSERT(false);
}
switch (sampler.wrapT) {
case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
mode.set_clamp_t_enable(true);
break;
case TINYGLTF_TEXTURE_WRAP_REPEAT:
mode.set_clamp_t_enable(false);
break;
default:
ASSERT(false);
}
return mode;
}
} // namespace gltf_util

71
common/util/gltf_util.h Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include "common/common_types.h"
#include "common/custom_data/Tfrag3Data.h"
#include "common/math/Vector.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
namespace gltf_util {
/*!
* Convert a GLTF index buffer to std::vector<u32>
*/
template <typename T>
std::vector<u32> index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u32 stride) {
std::vector<u32> result;
result.reserve(num_verts);
for (u32 i = 0; i < num_verts; i++) {
T val;
memcpy(&val, data, sizeof(T));
result.push_back(offset + val);
data += stride;
}
return result;
}
std::vector<math::Vector3f> extract_vec3f(const u8* data, u32 count, u32 stride);
std::vector<math::Vector2f> extract_vec2f(const u8* data, u32 count, u32 stride);
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data, u32 count, u32 stride);
std::vector<u32> gltf_index_buffer(const tinygltf::Model& model, int indices_idx, u32 index_offset);
struct ExtractedVertices {
std::vector<tfrag3::PreloadedVertex> vtx;
std::vector<math::Vector<u8, 4>> vtx_colors;
std::vector<math::Vector3f> normals;
};
ExtractedVertices gltf_vertices(const tinygltf::Model& model,
const std::map<std::string, int>& attributes,
const math::Matrix4f& w_T_local,
bool get_colors,
bool get_normals,
const std::string& debug_name);
DrawMode make_default_draw_mode();
struct TexturePool {
std::unordered_map<std::string, int> textures_by_name;
std::vector<tfrag3::Texture> textures_by_idx;
};
int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex);
int texture_pool_debug_checker(TexturePool* pool);
struct NodeWithTransform {
int node_idx;
math::Matrix4f w_T_node;
};
void dedup_vertices(const std::vector<tfrag3::PreloadedVertex>& vertices_in,
std::vector<tfrag3::PreloadedVertex>& vertices_out,
std::vector<u32>& old_to_new_out);
std::vector<NodeWithTransform> flatten_nodes_from_all_scenes(const tinygltf::Model& model);
DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler);
} // namespace gltf_util

View File

@ -5,13 +5,13 @@ Disclaimer: custom levels are still in development and are missing most features
The first three steps are already done for "test zone", so this can be used as a starting point.
# 1: File Setup
To create a custom level, copy the layout of `custom_levels/test-zone`. See `test-zone.jsonc` for information on how to name things. The `.gd` file also contains the level name.
To create a custom level, copy the layout of `custom_assets/jak1/levels/test-zone`. See `test-zone.jsonc` for information on how to name things. The `.gd` file also contains the level name.
# 2: Modify the engine
Modify `goal_src/engine/level/level-info.gc` to add level info for each custom level. There is level info for `test-zone` at the bottom that can be used as an example.
Modify `goal_src/jak1/engine/level/level-info.gc` to add level info for each custom level. There is level info for `test-zone` at the bottom that can be used as an example.
# 3: Modify the build system
Modify `goal_src/game.gp` and add a custom level target:
Modify `goal_src/jak1/game.gp` and add a custom level target:
```lisp
(build-custom-level "test-zone")
;; the DGO file
@ -21,7 +21,7 @@ Modify `goal_src/game.gp` and add a custom level target:
# 4: Export the GLTF file from blender.
For now, all meshes are displayed and treated as ground collision. This causes buggy collision because walls shouldn't use "floor" mode.
Blender will create a `.glb` file, which must have the name specified in the `.jsonc` file and should be located in `custom_level/your_level`
Blender will create a `.glb` file, which must have the name specified in the `.jsonc` file and should be located in `custom_assets/jak1/levels/your_level`
# 5: Rebuild the game
Any time the `.glb` file is changed, you must rebuild the game. Launch the compiler (`goalc`) and run `(mi)` to rebuild everything. It's recommended to leave the compiler open - it will remember files that haven't changed and skip rebuilding them.

View File

@ -10,7 +10,7 @@
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/jak1/test-zone/test-zone2.glb",
"gltf_file": "custom_assets/jak1/levels/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,
@ -55,20 +55,22 @@
// adds a 'symbol' tag (using the 'type' and 'string' lump types works the same way):
// "symbol-list": ["symbol", "sym-1", "sym-2"]
// The base actor id for your custom level. If you have multiple levels this should be unique!
"base_id": 100,
// The base actor id for your custom level. If you have multiple levels, this should be unique!
"base_id": 100,
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// Note: You will still have to add them to your level's .gd file.
// Removed so that the release builds don't have to double-decompile the game
// "art_groups": ["plat-ag"],
"art_groups": [],
// Any textures you want to include in your custom level. This is mainly useful for things such as the zoomer HUD,
// which is not in the common level files and has no art group associated with it.
// To get a list of all the textures, you can extract all of the game's textures
// by setting "save_texture_pngs" to true in the decompiler config.
"textures": [
// If you have any custom models in the "custom_assets/jak1/models" folder that you want to use in your level, add them to this list.
// Note: Like with art groups, these should also be added to your level's .gd file.
"custom_models": ["test-actor"],
// Any textures you want to include in your custom level. This is mainly useful for things such as the zoomer HUD,
// which is not in the common level files and has no art group associated with it.
// To get a list of all the textures, you can extract all of the game's textures
// by setting "save_texture_pngs" to true in the decompiler config.
"textures": [
// all textures required for the zoomer HUD
// "zoomerhud",
// "zoomerhud-dial",
@ -130,6 +132,16 @@
"lump": {
"name": "test-eco"
}
},
{
"trans": [-5.41, 3.5, 28.42], // translation
"etype": "test-actor", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-7.41, 3.5, 28.42, 10], // bounding sphere
"lump": {
"name": "test-actor"
}
}
// {
// "trans": [-7.41, 3.5, 28.42], // translation

View File

@ -4,6 +4,8 @@
;; the actual file name still needs to be 8.3
("TSZ.DGO"
("static-screen.o"
"test-zone.go"
"test-zone-obs.o"
"plat-ag.go"
"test-actor-ag.go"
"test-zone.go"
))

Binary file not shown.

View File

@ -10,7 +10,7 @@
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/jak2/test-zone/test-zone2.glb",
"gltf_file": "custom_assets/jak2/levels/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,

View File

@ -10,7 +10,7 @@
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/jak3/test-zone/test-zone2.glb",
"gltf_file": "custom_assets/jak3/levels/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,

View File

@ -191,7 +191,8 @@ void decompile(const fs::path& iso_data_path,
}
// texture replacements
auto replacements_path = file_util::get_jak_project_dir() / "texture_replacements";
auto replacements_path = file_util::get_jak_project_dir() / "custom_assets" /
game_version_names[config.game_version] / "texture_replacements";
if (fs::exists(replacements_path)) {
tex_db.replace_textures(replacements_path);
}

View File

@ -19,6 +19,7 @@
#include "decompiler/level_extractor/extract_tfrag.h"
#include "decompiler/level_extractor/extract_tie.h"
#include "decompiler/level_extractor/fr3_to_gltf.h"
#include "goalc/build_actor/jak1/build_actor.h"
namespace decompiler {

View File

@ -322,7 +322,8 @@ int main(int argc, char** argv) {
tex_db.merge_textures(texture_merge_path);
}
auto replacements_path = file_util::get_jak_project_dir() / "texture_replacements";
auto replacements_path = file_util::get_jak_project_dir() / "custom_assets" /
game_version_names[config.game_version] / "texture_replacements";
if (fs::exists(replacements_path)) {
tex_db.replace_textures(replacements_path);
}

View File

@ -153,10 +153,21 @@
)
)
(defun custom-level-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak1/)"
(defun custom-actor-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_assets/jak1/models/)"
(let ((out-name (string-append "$OUT/iso/" output-name)))
(defstep :in (string-append "custom_levels/jak1/" desc-file-name)
(defstep :in (string-append "custom_assets/jak1/models/" desc-file-name)
:tool 'dgo
:out `(,out-name)
)
(set! *all-cgos* (cons out-name *all-cgos*))
)
)
(defun custom-level-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_assets/jak1/levels/)"
(let ((out-name (string-append "$OUT/iso/" output-name)))
(defstep :in (string-append "custom_assets/jak1/levels/" desc-file-name)
:tool 'dgo
:out `(,out-name)
)
@ -208,11 +219,16 @@
)
(defmacro build-custom-level (name)
(let* ((path (string-append "custom_levels/jak1/" name "/" name ".jsonc")))
(let* ((path (string-append "custom_assets/jak1/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
:tool 'build-level
:out '(,(string-append "$OUT/obj/" name ".go")))))
(defmacro build-actor (name)
(let* ((path (string-append "custom_assets/jak1/models/" name ".glb")))
`(defstep :in ,path
:tool 'build-actor
:out '(,(string-append "$OUT/obj/" name "-ag.go")))))
(defun copy-iso-file (name subdir ext)
(let* ((path (string-append "$ISO/" subdir name ext))
@ -1636,12 +1652,16 @@
;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set up the build system to build the level geometry
;; this path is relative to the custom_levels/jak1 folder
;; this path is relative to the custom_assets/jak1/levels/ folder
;; it should point to the .jsonc file that specifies the level.
(build-custom-level "test-zone")
;; the DGO file
(custom-level-cgo "TSZ.DGO" "test-zone/testzone.gd")
;; generate the art group for a custom actor.
;; requires a .glb model file in custom_assets/jak1/models
(build-actor "test-actor")
;;;;;;;;;;;;;;;;;;;;;
;; Game Engine Code
;;;;;;;;;;;;;;;;;;;;;
@ -2080,6 +2100,7 @@
(goal-src "pc/debug/default-menu-pc.gc" "anim-tester-x" "part-tester" "entity-debug")
(goal-src "pc/debug/pc-debug-common.gc" "pckernel-impl" "entity-h" "game-info-h" "level-h" "settings-h" "gsound-h" "target-util")
(goal-src "pc/debug/pc-debug-methods.gc" "pc-debug-common")
(goal-src "levels/test-zone/test-zone-obs.gc" "process-drawable")
(group-list "all-code"
`(,@(reverse *all-gc*))

View File

@ -0,0 +1,123 @@
;;-*-Lisp-*-
(in-package goal)
(deftype test-actor (process-drawable)
((root collide-shape-moving :override)
(birth-time time-frame)
(base vector :inline)
(old-base vector :inline)
(bob-offset int64)
(bob-amount float)
)
(:methods (init-collision! (_type_) none))
(:state-methods idle)
)
(def-art-elt test-actor-ag test-actor-lod0-jg 0)
(def-art-elt test-actor-ag test-actor-lod0-mg 1)
(def-art-elt test-actor-ag test-actor-idle-ja 2)
(defskelgroup *test-actor-sg* test-actor test-actor-lod0-jg test-actor-idle-ja
((test-actor-lod0-mg (meters 9999999)))
:bounds (static-spherem 0 0 0 4.5)
:texture-level 2
)
(defmethod init-collision! ((this test-actor))
(let ((cshape (new 'process 'collide-shape-moving this (collide-list-enum hit-by-player))))
(set! (-> cshape dynam) (copy *standard-dynamics* 'process))
(set! (-> cshape reaction) default-collision-reaction)
(set! (-> cshape no-reaction)
(the (function collide-shape-moving collide-shape-intersect vector vector none) nothing)
)
;; (let ((mesh (new 'process 'collide-shape-prim-mesh cshape (the uint 0) (the uint 0))))
;; (set! (-> mesh prim-core collide-as) (collide-kind enemy))
;; (set! (-> mesh collide-with) (collide-kind target))
;; (set! (-> mesh transform-index) 6)
;; (set-vector! (-> mesh local-sphere) 0.0 0.0 0.0 (meters 4.5))
;; (set-root-prim! cshape mesh)
;; )
(let ((sphere (new 'process 'collide-shape-prim-sphere cshape (the uint 0))))
(set! (-> sphere prim-core collide-as) (collide-kind enemy))
(set! (-> sphere collide-with) (collide-kind target))
(set! (-> sphere prim-core action) (collide-action solid))
(set! (-> sphere prim-core offense) (collide-offense normal-attack))
(set-vector! (-> sphere local-sphere) 0.0 0.0 0.0 (meters 2))
(set-root-prim! cshape sphere)
)
(set! (-> cshape nav-radius) (* 0.75 (-> cshape root-prim local-sphere w)))
(backup-collide-with-as cshape)
(set! (-> this root) cshape)
)
(none)
)
(defmethod init-from-entity! ((this test-actor) (e entity-actor))
(logior! (-> this mask) (process-mask enemy))
(init-collision! this)
(process-drawable-from-entity! this e)
(set! (-> this bob-amount) 1024.0)
(set! (-> this bob-offset) (+ (the int (-> this root trans x)) (the int (-> this root trans y)) (the int (-> this root trans z))))
(set-time! (-> this birth-time))
(vector-copy! (-> this base) (-> this root trans))
(vector-copy! (-> this old-base) (-> this root trans))
(initialize-skeleton this *test-actor-sg* '())
(logclear! (-> this mask) (process-mask actor-pause))
(transform-post)
(go-virtual idle :proc this)
(none)
)
(defbehavior test-actor-init-by-other test-actor ((pos vector))
(logior! (-> self mask) (process-mask enemy))
(init-collision! self)
(initialize-skeleton self *test-actor-sg* '())
(vector-copy! (-> self root trans) pos)
(quaternion-identity! (-> self root quat))
(vector-identity! (-> self root scale))
(set! (-> self bob-amount) 1024.0)
(set! (-> self bob-offset) (+ (the int (-> self root trans x)) (the int (-> self root trans y)) (the int (-> self root trans z))))
(set-time! (-> self birth-time))
(vector-copy! (-> self base) (-> self root trans))
(vector-copy! (-> self old-base) (-> self root trans))
(logclear! (-> self mask) (process-mask actor-pause))
(transform-post)
(go-virtual idle)
)
(defstate idle (test-actor)
:virtual #t
:event (behavior ((proc process) (argc int) (message symbol) (block event-message-block))
(case message
(('attack 'touch)
(if (= (-> proc type) target)
(send-event proc 'attack #f (static-attack-info ((shove-up (meters 2.5)) (shove-back (meters 7.5)))))
)
)
)
)
:code (behavior ()
(loop
(quaternion-rotate-y! (-> self root quat) (-> self root quat) (* (degrees 45) (seconds-per-frame)))
(let ((bob (-> self bob-amount)))
(when (< 0.0 bob)
(set! (-> self root trans y)
(+ (-> self base y)
(* bob (sin (* 109.22667 (the float (mod (+ (- (current-time) (-> self birth-time)) (-> self bob-offset)) (seconds 2))))))
)
)
(update-transforms! (-> self root))
)
)
; (dotimes (i (-> self node-list length))
; (let* ((joint (-> self node-list data i)) (jpos (vector<-cspace! (new-stack-vector0) joint)))
; (add-debug-sphere #t (bucket-id debug) jpos (meters 0.1) (static-rgba 0 #xff 0 #x40))
; (add-debug-text-sphere (!= (-> joint joint) #f) (bucket-id debug) jpos (meters 0.1) (-> joint joint name) (static-rgba 0 #xff 0 #x40))
; )
; )
(suspend)
)
)
:post transform-post
)

View File

@ -296,7 +296,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set up the build system to build the level geometry
;; this path is relative to the custom_levels/jak2 folder
;; this path is relative to the custom_assets/jak2/levels folder
;; it should point to the .jsonc file that specifies the level.
(build-custom-level "test-zone")
;; the DGO file

View File

@ -84,9 +84,9 @@
)
(defun custom-level-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak2/)"
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_assets/jak2/levels/)"
(let ((out-name (string-append "$OUT/iso/" output-name)))
(defstep :in (string-append "custom_levels/jak2/" desc-file-name)
(defstep :in (string-append "custom_assets/jak2/levels/" desc-file-name)
:tool 'dgo
:out `(,out-name)
)
@ -136,7 +136,7 @@
)
(defmacro build-custom-level (name)
(let* ((path (string-append "custom_levels/jak2/" name "/" name ".jsonc")))
(let* ((path (string-append "custom_assets/jak2/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
:tool 'build-level2
:out '(,(string-append "$OUT/obj/" name ".go")))))

View File

@ -237,9 +237,9 @@
))
(defun custom-level-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak3/)"
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_assets/jak3/levels/)"
(let ((out-name (string-append "$OUT/iso/" output-name)))
(defstep :in (string-append "custom_levels/jak3/" desc-file-name)
(defstep :in (string-append "custom_assets/jak3/levels/" desc-file-name)
:tool 'dgo
:out `(,out-name)
)
@ -248,7 +248,7 @@
)
(defmacro build-custom-level (name)
(let* ((path (string-append "custom_levels/jak3/" name "/" name ".jsonc")))
(let* ((path (string-append "custom_assets/jak3/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
:tool 'build-level3
:out '(,(string-append "$OUT/obj/" name ".go")))))

View File

@ -6,6 +6,7 @@ add_library(compiler
emitter/Register.cpp
debugger/disassemble.cpp
build_level/common/build_level.cpp
build_actor/common/MercExtract.cpp
build_level/jak1/build_level.cpp
build_level/jak2/build_level.cpp
build_level/jak3/build_level.cpp
@ -57,6 +58,7 @@ add_library(compiler
data_compiler/dir_tpages.cpp
data_compiler/game_count.cpp
data_compiler/DataObjectGenerator.cpp
build_actor/jak1/build_actor.cpp
debugger/Debugger.cpp
debugger/DebugInfo.cpp
listener/Listener.cpp
@ -80,8 +82,9 @@ add_executable(goalc main.cpp)
add_executable(goalc-simple simple_main.cpp)
add_executable(build_level build_level/main.cpp)
add_executable(build_actor build_actor/main.cpp)
target_link_libraries(goalc common Zydis compiler)
target_link_libraries(goalc-simple common Zydis compiler)
target_link_libraries(build_level common Zydis compiler)
target_link_libraries(build_actor common Zydis compiler)

View File

@ -0,0 +1,155 @@
#include "MercExtract.h"
#include "common/log/log.h"
void extract(const std::string& name,
MercExtractData& out,
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
u32 index_offset,
u32 vertex_offset,
u32 tex_offset) {
ASSERT(out.new_vertices.empty());
std::map<int, tfrag3::MercDraw> draw_by_material;
int mesh_count = 0;
int prim_count = 0;
for (const auto& n : all_nodes) {
const auto& node = model.nodes[n.node_idx];
if (node.mesh >= 0) {
const auto& mesh = model.meshes[node.mesh];
mesh_count++;
for (const auto& prim : mesh.primitives) {
prim_count++;
// extract index buffer
std::vector<u32> prim_indices = gltf_util::gltf_index_buffer(
model, prim.indices, out.new_vertices.size() + vertex_offset);
ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES, "Unsupported triangle mode");
// extract vertices
auto verts =
gltf_util::gltf_vertices(model, prim.attributes, n.w_T_node, true, true, mesh.name);
out.new_vertices.insert(out.new_vertices.end(), verts.vtx.begin(), verts.vtx.end());
out.new_colors.insert(out.new_colors.end(), verts.vtx_colors.begin(),
verts.vtx_colors.end());
out.normals.insert(out.normals.end(), verts.normals.begin(), verts.normals.end());
ASSERT(out.new_colors.size() == out.new_vertices.size());
// TODO: just putting it all in one material
auto& draw = draw_by_material[prim.material];
draw.mode = gltf_util::make_default_draw_mode(); // todo rm
draw.tree_tex_id = 0; // todo rm
draw.num_triangles += prim_indices.size() / 3;
// if (draw.vis_groups.empty()) {
// auto& grp = draw.vis_groups.emplace_back();
// grp.num_inds += prim_indices.size();
// grp.num_tris += draw.num_triangles;
// grp.vis_idx_in_pc_bvh = UINT32_MAX;
// } else {
// auto& grp = draw.vis_groups.back();
// grp.num_inds += prim_indices.size();
// grp.num_tris += draw.num_triangles;
// grp.vis_idx_in_pc_bvh = UINT32_MAX;
// }
draw.index_count = prim_indices.size();
draw.first_index = index_offset + out.new_indices.size();
out.new_indices.insert(out.new_indices.end(), prim_indices.begin(), prim_indices.end());
}
}
}
tfrag3::MercEffect e;
out.new_model.name = name;
out.new_model.max_bones = 120; // idk
out.new_model.max_draws = 200;
for (const auto& [mat_idx, d_] : draw_by_material) {
e.all_draws.push_back(d_);
auto& draw = e.all_draws.back();
draw.mode = gltf_util::make_default_draw_mode();
if (mat_idx == -1) {
lg::warn("Draw had a material index of -1, using default texture.");
draw.tree_tex_id = 0;
continue;
}
const auto& mat = model.materials[mat_idx];
int tex_idx = mat.pbrMetallicRoughness.baseColorTexture.index;
if (tex_idx == -1) {
lg::warn("Material {} has no texture, using default texture.", mat.name);
draw.tree_tex_id = 0;
continue;
}
const auto& tex = model.textures[tex_idx];
ASSERT(tex.sampler >= 0);
ASSERT(tex.source >= 0);
draw.mode = gltf_util::draw_mode_from_sampler(model.samplers.at(tex.sampler));
const auto& img = model.images[tex.source];
draw.tree_tex_id = tex_offset + texture_pool_add_texture(&out.tex_pool, img);
}
lg::info("total of {} unique materials", e.all_draws.size());
out.new_model.effects.push_back(e);
out.new_model.effects.push_back(e);
out.new_model.effects.push_back(e);
out.new_model.effects.push_back(e);
lg::info("Merged {} meshes and {} prims into {} vertices", mesh_count, prim_count,
out.new_vertices.size());
}
void merc_convert(MercSwapData& out, const MercExtractData& in) {
// easy
out.new_model = in.new_model;
out.new_indices = in.new_indices;
out.new_textures = in.tex_pool.textures_by_idx;
// convert vertices
for (size_t i = 0; i < in.new_vertices.size(); i++) {
const auto& y = in.new_vertices[i];
auto& x = out.new_vertices.emplace_back();
x.pos[0] = y.x;
x.pos[1] = y.y;
x.pos[2] = y.z;
x.normal[0] = in.normals.at(i).x();
x.normal[1] = in.normals.at(i).y();
x.normal[2] = in.normals.at(i).z();
x.weights[0] = 1.0f;
x.weights[1] = 0.0f;
x.weights[2] = 0.0f;
x.st[0] = y.s;
x.st[1] = y.t;
x.rgba[0] = in.new_colors[i][0];
x.rgba[1] = in.new_colors[i][1];
x.rgba[2] = in.new_colors[i][2];
x.rgba[3] = in.new_colors[i][3];
x.mats[0] = 3;
x.mats[1] = 0;
x.mats[2] = 0;
}
}
MercSwapData load_merc_model(u32 current_idx_count,
u32 current_vtx_count,
u32 current_tex_count,
const std::string& path,
const std::string& name) {
MercSwapData result;
lg::info("Reading gltf mesh: {}", path);
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path);
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
auto all_nodes = gltf_util::flatten_nodes_from_all_scenes(model);
MercExtractData extract_data;
extract(name, extract_data, model, all_nodes, current_idx_count, current_vtx_count,
current_tex_count);
merc_convert(result, extract_data);
return result;
}

View File

@ -0,0 +1,35 @@
#pragma once
#include "common/util/gltf_util.h"
struct MercExtractData {
gltf_util::TexturePool tex_pool;
std::vector<u32> new_indices;
std::vector<tfrag3::PreloadedVertex> new_vertices;
std::vector<math::Vector<u8, 4>> new_colors;
std::vector<math::Vector3f> normals;
tfrag3::MercModel new_model;
};
// Data produced by loading a replacement model
struct MercSwapData {
std::vector<u32> new_indices;
std::vector<tfrag3::MercVertex> new_vertices;
std::vector<tfrag3::Texture> new_textures;
tfrag3::MercModel new_model;
};
void extract(const std::string& name,
MercExtractData& out,
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
u32 index_offset,
u32 vertex_offset,
u32 tex_offset);
void merc_convert(MercSwapData& out, const MercExtractData& in);
MercSwapData load_merc_model(u32 current_idx_count,
u32 current_vtx_count,
u32 current_tex_count,
const std::string& path,
const std::string& name);

View File

@ -0,0 +1,33 @@
#pragma once
#include "common/custom_data/Tfrag3Data.h"
#include "decompiler/level_extractor/MercData.h"
#include "decompiler/level_extractor/common_formats.h"
#include "goalc/build_level/common/Entity.h"
#include "goalc/build_level/common/FileInfo.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
struct Art {
std::string name;
s32 length;
ResLump lump;
};
struct ArtElement : Art {
u8 pad[12];
};
struct MercEyeAnimFrame {
s8 pupil_trans_x;
s8 pupil_trans_y;
s8 blink;
s8 iris_scale;
s8 pupil_scale;
s8 lid_scale;
};
struct MercEyeAnimBlock {
s16 max_frame;
std::vector<MercEyeAnimFrame> frames;
};

View File

@ -0,0 +1,445 @@
#include "build_actor.h"
#include "common/log/log.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
namespace jak1 {
std::map<int, size_t> g_joint_map;
size_t Joint::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("joint");
size_t result = gen.current_offset_bytes();
gen.add_ref_to_string_in_pool(name);
gen.add_word(number);
if (parent == -1) {
gen.add_symbol_link("#f");
} else {
gen.link_word_to_byte(gen.add_word(0), g_joint_map[parent]);
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
gen.add_word_float(bind_pose(i, j));
}
}
return result;
}
size_t JointAnimCompressed::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("joint-anim-compressed");
size_t result = gen.current_offset_bytes();
gen.add_ref_to_string_in_pool(name);
gen.add_word((length << 16) + number);
// for (auto& word : data) {
// gen.add_word(word);
// }
return result;
}
size_t JointAnimCompressedFrame::generate(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
gen.add_word(offset_64); // 0
gen.add_word(offset_32); // 4
gen.add_word(offset_16); // 8
gen.add_word(reserved); // 12
gen.align(4);
return result;
}
size_t JointAnimCompressedHDR::generate(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
for (auto& bit : control_bits) {
gen.add_word(bit);
}
gen.add_word(num_joints);
gen.add_word(matrix_bits);
gen.align(4);
return result;
}
size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
hdr.generate(gen); // 0-64 (inline)
gen.add_word(offset_64); // 64
gen.add_word(offset_32); // 68
gen.add_word(offset_16); // 72
gen.add_word(reserved); // 76
// default joint poses (taken from money-idle)
for (size_t i = 0; i < 8; i++) {
gen.add_word_float(data[i].x());
gen.add_word_float(data[i].y());
gen.add_word_float(data[i].z());
gen.add_word_float(data[i].w());
}
gen.add_word(0);
gen.add_word(0x7fff0000);
gen.add_word(0x2250000);
gen.add_word(0x10001000);
gen.add_word(0x10000000);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.align(4);
return result;
}
size_t JointAnimCompressedControl::generate(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
gen.add_word(num_frames); // 0
gen.add_word(fixed_qwc); // 4
gen.add_word(frame_qwc); // 8
auto ja_fixed_slot = gen.add_word(0);
auto ja_frame_slot = gen.add_word(0);
gen.align(4);
gen.link_word_to_byte(ja_fixed_slot, fixed.generate(gen));
gen.link_word_to_byte(ja_frame_slot, frame[0].generate(gen));
return result;
}
size_t ArtJointGeo::generate_res_lump(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("res-lump");
size_t result = gen.current_offset_bytes();
return result;
}
size_t ArtJointGeo::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("art-joint-geo");
size_t result = gen.current_offset_bytes();
gen.add_word(0); // 4
gen.add_ref_to_string_in_pool(name); // 8
gen.add_word(length); // 12
auto res_slot = gen.add_word(0); // 16 (res-lump)
gen.add_word(0); // 20
gen.add_word(0);
gen.add_word(0);
std::vector<size_t> joint_slots;
for (size_t i = 0; i < length; i++) {
joint_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (size_t i = 0; i < length; i++) {
auto joint = data.at(i).generate(gen);
gen.link_word_to_byte(joint_slots.at(i), joint);
g_joint_map[data.at(i).number] = joint;
}
auto res_header = lump.generate_header(gen, "res-lump");
gen.link_word_to_byte(res_slot, res_header);
lump.generate_tag_list_and_data(gen, res_header);
return result;
}
size_t ArtJointAnim::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("art-joint-anim");
size_t result = gen.current_offset_bytes();
gen.add_symbol_link("#f"); // 4 (eye block)
gen.add_ref_to_string_in_pool(name); // 8
gen.add_word(length); // 12
gen.add_symbol_link("#f"); // 16 (res-lump)
gen.add_word_float(speed); // 20
gen.add_word_float(artist_base); // 24
gen.add_word_float(artist_step); // 28
gen.add_ref_to_string_in_pool(master_art_group_name); // 32
gen.add_word(master_art_group_index); // 36
gen.add_symbol_link("#f"); // 40 (blerc)
auto ctrl_slot = gen.add_word(0);
std::vector<size_t> frame_slots;
for (size_t i = 0; i < length; i++) {
frame_slots.push_back(gen.add_word(0));
}
gen.align(4);
gen.link_word_to_byte(ctrl_slot, frames.generate(gen));
for (size_t i = 0; i < length; i++) {
gen.link_word_to_byte(frame_slots.at(i), data.at(i).generate(gen));
}
return result;
}
static size_t gen_dummy_frag_geo(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
// frag geo stolen from money-lod0
static std::vector<u32> words = {
0xa4320c04, 0x8000026, 0x302a0000, 0x604, 0xa910, 0xac16, 0x100af1c,
0xb23d, 0x100b585, 0x100b870, 0xbb76, 0xbe52, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x0, 0x0, 0x89b78086, 0xf1a910a, 0x3a4b7000, 0x5f818086,
0xf1a1094, 0x29347014, 0x49648186, 0x91313, 0x4e5d8024, 0x354b8186, 0xf1a6416,
0x3a497024, 0x40538186, 0x91919, 0x647b8034, 0x24348186, 0xf1a371c, 0x647f7034,
0x495d8186, 0x91f1f, 0x7a9c8044, 0x35498086, 0xf1a2231, 0x8eb57044, 0x5f7b8186,
0x92525, 0x83ad8054, 0x5f7f8186, 0xf1a2828, 0x9fcc7054, 0x759c8186, 0x92b2b,
0x7aa38064, 0x1c257f86, 0x273b2e2e, 0x94b86040, 0xe198186, 0x2737d034, 0x71936038,
0xe188186, 0x273bcd3a, 0x57676030, 0x1c2a8186, 0x2737613d, 0x34456028, 0xe1b8086,
0x485d40c7, 0x2e3c5028, 0x293c8186, 0x485d5b43, 0x131a5020, 0x26398186, 0x6e804646,
0xf164020, 0x4b688186, 0x6e805549, 0x34018, 0x4c688186, 0x979f4c4c, 0x5073018,
0x72978086, 0x979f4fc1, 0x5073010, 0x73988086, 0x6e807352, 0x34010, 0x4c698186,
0x485d6d58, 0x5085018, 0x2f488086, 0x273b5e67, 0x21256020, 0x526d8186, 0x2737a66a,
0x13196018, 0x72988186, 0x485da070, 0x5085010, 0x98c78086, 0x6e80767f, 0xf164008,
0x95c48186, 0x979fc479, 0x13193008, 0xb0e68186, 0x979f7c7c, 0x2e3b3000, 0xb4ea8186,
0x6e808282, 0x2b394000, 0x95c48186, 0x485d9d85, 0x131b5008, 0xb0e68186, 0x485d8888,
0x2e3c5000, 0x8fbb8186, 0x2737978b, 0x212a6008, 0xa2db8186, 0x273b8e8e, 0x34486000,
0x6c998086, 0x273b9aa3, 0x13186010, 0x87f86, 0x485dcaca, 0x51685030, 0x75a37f86,
0x90707, 0x4e648000, 0x5f858186, 0x90d0d, 0x45538014, 0x0, 0x0,
0xcb01005f, 0xcb00fffa, 0xcb010064, 0x5101e01, 0x0, 0x0, 0x306,
0x60030000, 0x120, 0x0, 0x1cf02c14, 0x2008044, 0x0, 0x0,
0x34, 0x81010000, 0x0, 0x0, 0x8, 0x6d100000, 0x44,
0x80, 0x42, 0x30000, 0x86321604, 0x400001c, 0x2422440e, 0x4,
0x1004f28, 0x1008b4c, 0xb225, 0xca4c, 0x1000128, 0x1000422, 0x1000a2e,
0x10064ca, 0x6a34, 0x1006d2e, 0x70ca, 0x1007340, 0x7946, 0x7f4c,
0x100884c, 0x8e4f, 0x9479, 0x9a7c, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x0, 0x0, 0x131a8086,
0x215d0d9d, 0x87c45040, 0x38186, 0x4780a313, 0x65984038, 0x38186, 0x47806116,
0x3d684030, 0x5078186, 0x709fa919, 0x3e693030, 0x13198186, 0x709f5b1c, 0x1b3c3028,
0x21208186, 0x96bbaf1f, 0x21452028, 0x34428186, 0x96bf5522, 0xe242020, 0x3a408086,
0xb6da252e, 0x27411024, 0x647f8186, 0xb6da4628, 0x16261014, 0x647a8186, 0xcbf1402b,
0x32450014, 0x4e528086, 0xcbf1313a, 0x3b5b0024, 0x29268086, 0xb6da34b5, 0x51811034,
0x45458186, 0xcbf13737, 0x51860034, 0x64808186, 0xd3ff3d3d, 0x51800040, 0x7aa58186,
0xcbf14343, 0x3b520000, 0x8ebf8186, 0xb6dac749, 0x27401000, 0x71948186, 0x96bf854c,
0x132010, 0x57668186, 0x96bb8252, 0x122018, 0x2e3b8186, 0x709f7c58, 0x1a3020,
0xf168186, 0x4780765e, 0x18394028, 0x94bb8086, 0x96bb91c4, 0xe202008, 0xa7dc8086,
0x96bf97c1, 0x21422000, 0xf167f86, 0x4780a0a0, 0x8ac74040, 0x5078186, 0x709fbea6,
0x64983038, 0x13138186, 0x96bfb8ac, 0x446c2030, 0x13128186, 0x96bbbbbb, 0x5e9a2038,
0x34458186, 0x370707, 0x94d66048, 0x5088186, 0x215d6710, 0x64975038, 0xcb010064,
0xcb00ffd3, 0xcb010051, 0x5021000, 0x4088003, 0x10306060, 0x3, 0x0,
0x8d301104, 0x300001f, 0x2624430a, 0x4, 0x910a, 0x100a928, 0xc42b,
0x1006aa0, 0x70a6, 0x73bb, 0x9a07, 0xa00d, 0x100a3a0, 0x100a6bb,
0xac34, 0xb237, 0xb83d, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x8ccc8186,
0xf1a4c07, 0x16817074, 0x7bb58186, 0xf1a550a, 0x40b77064, 0x94d68186, 0x2737460d,
0x46bb6068, 0x81b88186, 0x273b8e10, 0x59db6060, 0x87c48186, 0x485d4013, 0x67e65060,
0x64978186, 0x485d8816, 0x75f85058, 0x65988186, 0x6e803a19, 0x7afd4058, 0x3d688186,
0x6e80821c, 0x7afd4050, 0x3e698186, 0x979f341f, 0x75f93050, 0x1b3c8186, 0x979f7c22,
0x67e73048, 0x21458086, 0xbdbb252e, 0x59e02048, 0xe248086, 0xbdbf2876, 0x46be2040,
0x27418186, 0xdddaaf2b, 0x40c01044, 0x446c8186, 0xbdbfc731, 0x67ed2050, 0x64988186,
0x979f3737, 0x75f93058, 0x8ac78186, 0x6e803d3d, 0x6bea4060, 0xa2e58186, 0x485d4343,
0x4cc45068, 0xa2e88186, 0x273b4949, 0x23996070, 0x679c7f86, 0x95252, 0x2ca38064,
0x517f8086, 0xf1a5894, 0x51cc7054, 0x5e938186, 0x27378b5b, 0x67e76058, 0x44678086,
0x273b5e97, 0x67e86050, 0x3e688186, 0x485d8561, 0x75f85050, 0x1b3c8186, 0x485d9d64,
0x67e55048, 0x18398186, 0x6e807f67, 0x6bea4048, 0x1a8186, 0x979f796d, 0x4cc53040,
0x3b5b8086, 0xf2f1b5be, 0x2cae0044, 0x51868186, 0xf2f1bbbb, 0x35bb0054, 0x51818186,
0xdddac1c1, 0x51da1054, 0x67a37f86, 0x90101, 0x648080, 0x70ad7f86, 0x94f04,
0x16858074, 0x0, 0x0, 0x0, 0xcb010051, 0xcb00fffa, 0xcb010016,
0x5021000, 0xc008003, 0x81860080, 0x0, 0x0, 0x92351604, 0x300001f,
0x2826450f, 0x4, 0xac31, 0x100af5e, 0x100c449, 0xa01, 0x1000d07,
0x6143, 0x100643d, 0x10079c1, 0x8537, 0x883d, 0x1008b07, 0x1008e49,
0x100b243, 0xb549, 0x100b837, 0xbe31, 0xc1c1, 0xc7bb, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x0, 0x0, 0x0, 0x808186, 0x707,
0x39800040, 0x2ab78186, 0x141a1010, 0xf4b7080, 0x51e78186, 0x2c379113, 0x2c6d6078,
0x43db8186, 0x2c3b1616, 0x9486080, 0x5ff88186, 0x4d5d9719, 0x26695078, 0x51e68186,
0x4d5d1c1c, 0x33c5080, 0x64fd8186, 0x73809d1f, 0x25684078, 0x55ea8186, 0x73802222,
0x394080, 0x5ff98186, 0x9c9fa325, 0x26683078, 0x51e68186, 0x9c9f2828, 0x33b3080,
0x51ee8186, 0xc2bba92b, 0x2c662078, 0x43dc8186, 0xc2bf2e2e, 0x9422080, 0x3bda8186,
0xe2da4631, 0x397f1074, 0x2abf8186, 0xe2da3434, 0xf401080, 0x1fbb8086, 0xf7f13740,
0x397a0074, 0x16a58186, 0xf7f13a3a, 0x23520080, 0x808186, 0xffffcd3d, 0x39800040,
0x16ae8186, 0xf7f1ca43, 0x4fa50064, 0x2ac08186, 0xe2da7649, 0x63bf1064, 0x51ed8186,
0xc2bfa64c, 0x46942070, 0x43e08186, 0xc2bb734f, 0x69bb2068, 0x5ff98186, 0x9c9fa052,
0x4c973070, 0x51e78186, 0x9c9f6d55, 0x6fc43068, 0x64fd8186, 0x73809a58, 0x4d984070,
0x55ea8186, 0x7380675b, 0x72c74068, 0x5ff88186, 0x4d5d945e, 0x4c985070, 0x36c58186,
0x9c9f826a, 0x8ae63060, 0x30be8186, 0xc2bf7c70, 0x7cdc2060, 0xd9a8086, 0xc2bb7fbb,
0x8aee2058, 0x16a37f86, 0x5090101, 0x23640000, 0x1fad7f86, 0x5090404, 0x39850074,
0x0, 0x0, 0x0, 0xcb010000, 0xcb00ffff, 0xcb010039, 0x5021000,
0x20001b, 0x6c00c102, 0x2, 0x0, 0x210f0904, 0x6, 0xb090b05,
0x4, 0x101, 0x707, 0x1307, 0x1c04, 0x1f07, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x0, 0x538186,
0x90d0d, 0x1f7b0034, 0x95d7f86, 0x91010, 0x359c0044, 0x1f7b8186, 0x91616,
0x3ead0054, 0x359c8186, 0x91919, 0x35a30064, 0x1f857f86, 0x90404, 0x530014,
0x9648186, 0x90a0a, 0x95d0024, 0x0, 0x0, 0xcb01001f, 0xcb00fffa,
0xcb01001f, 0x1021000, 0x223, 0x0, 0x0, 0x0};
for (auto& word : words) {
gen.add_word(word);
}
return result;
}
size_t gen_dummy_frag_ctrl(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
gen.add_word(0x1067232);
gen.add_word(0x54320603);
gen.add_word(0x5d300002);
gen.add_word(0x5d350002);
gen.add_word(0x120f0002);
gen.add_word(0x2);
gen.add_word(0x0);
gen.add_word(0x0);
return result;
}
size_t gen_dummy_extra_info(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
gen.add_word(0x1);
gen.add_word(0x0);
gen.add_word(0x0);
gen.add_word(0x0);
gen.add_word(0xb74ccccd);
gen.add_word(0x40400000);
gen.add_word(0x8066a1ff);
gen.add_word(0x0);
return result;
}
size_t generate_dummy_merc_ctrl(DataObjectGenerator& gen, const ArtGroup& ag) {
gen.align_to_basic();
gen.add_type_tag("merc-ctrl");
size_t result = gen.current_offset_bytes();
// excluding align and prejoint
auto joints = ((ArtJointGeo*)ag.elts.at(0))->length - 2;
gen.add_word(0); // 4
gen.add_ref_to_string_in_pool(ag.name + "-lod0"); // 8
gen.add_word(0); // 12
gen.add_symbol_link("#f"); // 16 (res-lump)
gen.add_word(joints); // 20 (num-joints)
gen.add_word(0x0); // 24 (pad)
gen.add_word(0x0); // 28 (pad)
gen.add_word(0x4188ee86); // 32-112 (xyz-scale)
gen.add_word(0xc780ff80); // 36 (st-magic)
gen.add_word(0x40798000); // 40 (st-out-a)
gen.add_word(0x40eb4000); // 44 (st-out-b)
gen.add_word(0x4780ff80); // 48 (st-vif-add)
gen.add_word(0x50000); // 52 ((st-int-off << 16) + st-int-scale)
gen.add_word(0x1); // 56 (effect-count)
gen.add_word(0x0); // 60 (blend-target-count)
gen.add_word(0xe00005); // 64 ((fragment-count << 16) + tri-count)
gen.add_word(0x860101); // 68
gen.add_word(0x86011b); // 72
gen.add_word(0x0); // 76
gen.add_word(0x0); // 80
gen.add_word(0x120101); // 84
gen.add_word(0x83002c); // 88
gen.add_word(0x3e780184); // 92
gen.add_word(0x0); // 96
gen.add_word(0x0); // 100
gen.add_word(0x0); // 104
gen.add_word(0x0); // 108
auto frag_geo_slot = gen.add_word(0); // 112-140 (effect)
auto frag_ctrl_slot = gen.add_word(0); // 116 (frag-ctrl)
gen.add_word(0x0); // 120 (blend-data)
gen.add_word(0x0); // 124 (blend-ctrl)
gen.add_word(0x50000); // 128
gen.add_word(0xe00000); // 132
gen.add_word(0x100011b); // 136
auto extra_info_slot = gen.add_word(0); // 140 (extra-info)
gen.link_word_to_byte(extra_info_slot, gen_dummy_extra_info(gen));
gen.link_word_to_byte(frag_ctrl_slot, gen_dummy_frag_ctrl(gen));
gen.link_word_to_byte(frag_geo_slot, gen_dummy_frag_geo(gen));
return result;
}
std::vector<u8> ArtGroup::save_object_file() const {
DataObjectGenerator gen;
gen.add_type_tag("art-group");
auto ag_words = 8 + length;
while (gen.words() < ag_words) {
gen.add_word(0);
}
auto file_info_slot = info.add_to_object_file(gen);
gen.link_word_to_byte(1, file_info_slot); // 4 (file-info)
gen.link_word_to_string_in_pool(name, 8 / 4); // 8 (name)
gen.set_word(12 / 4, length); // 12 (ag length)
gen.link_word_to_symbol("#f", 16 / 4); // 16 (res-lump)
gen.set_word(20 / 4, 0); // 20 (pad)
gen.set_word(24 / 4, 0);
gen.set_word(28 / 4, 0);
if (!elts.empty()) {
if (elts.at(0)) {
auto jgeo = (ArtJointGeo*)elts.at(0);
gen.link_word_to_byte(32 / 4, jgeo->generate(gen));
}
if (!elts.at(1)) {
gen.link_word_to_byte(36 / 4, generate_dummy_merc_ctrl(gen, *this));
}
if (elts.at(2)) {
auto ja = (ArtJointAnim*)elts.at(2);
gen.link_word_to_byte(40 / 4, ja->generate(gen));
}
}
return gen.generate_v4();
}
bool run_build_actor(const std::string& input_model,
const std::string& ag_out,
const std::string& output_prefix) {
std::string ag_name;
if (fs::exists(file_util::get_jak_project_dir() / input_model)) {
ag_name = fs::path(input_model).stem().string();
} else {
ASSERT_MSG(false, "Model file not found: " + input_model);
}
ArtGroup ag(ag_name);
std::vector<Joint> joints;
auto identity = math::Matrix4f::identity();
joints.emplace_back("align", 0, -1, identity);
joints.emplace_back("prejoint", 1, -1, identity);
// matrix stolen from "egg" joint from "money" art group
auto main_pose = math::Matrix4f::zero();
main_pose(0, 0) = 1.0f;
main_pose(0, 1) = -0.0f;
main_pose(0, 2) = 0.0f;
main_pose(0, 3) = -0.0f;
main_pose(1, 0) = -0.0f;
main_pose(1, 1) = 1.0f;
main_pose(1, 2) = -0.0f;
main_pose(1, 3) = 0.0f;
main_pose(2, 0) = 0.0f;
main_pose(2, 1) = -0.0f;
main_pose(2, 2) = 1.0f;
main_pose(2, 3) = -0.0f;
main_pose(3, 0) = -0.0f;
main_pose(3, 1) = -2194.1628418f;
main_pose(3, 2) = -0.0f;
main_pose(3, 3) = 1.0f;
Joint main("main", 2, 1, main_pose);
joints.emplace_back(main);
ArtJointGeo jgeo(ag.name, joints);
ArtJointAnim ja(ag.name, joints);
jgeo.lump.add_res(
std::make_unique<ResInt32>("texture-level", std::vector<s32>{2}, DEFAULT_RES_TIME));
// jgeo.lump.add_res(std::make_unique<ResVector>(
// "trans-offset", std::vector<math::Vector4f>{{0.0f, 2048.0f, 0.0f, 1.0f}},
// DEFAULT_RES_TIME));
jgeo.lump.add_res(
std::make_unique<ResInt32>("joint-channel", std::vector<s32>{0}, DEFAULT_RES_TIME));
jgeo.lump.add_res(std::make_unique<ResFloat>(
"lod-dist", std::vector<float>{5000.0f * METER_LENGTH, 6000.0f * METER_LENGTH},
DEFAULT_RES_TIME));
jgeo.lump.sort_res();
ag.elts.emplace_back(&jgeo);
// dummy merc-ctrl
ag.elts.emplace_back(nullptr);
ag.elts.emplace_back(&ja);
ag.length = ag.elts.size();
auto ag_file = ag.save_object_file();
lg::info("ag file size {} bytes", ag_file.size());
auto save_path = fs::path(ag_out);
file_util::create_dir_if_needed_for_file(ag_out);
lg::info("Saving to {}", save_path.string());
file_util::write_binary_file(file_util::get_jak_project_dir() / save_path, ag_file.data(),
ag_file.size());
return true;
}
} // namespace jak1

View File

@ -0,0 +1,210 @@
#pragma once
#include "common/util/gltf_util.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_level/collide/common/collide_common.h"
namespace jak1 {
struct Joint {
std::string name;
s32 number;
int parent;
math::Matrix4f bind_pose{};
Joint(const std::string& name, int number, int parent, math::Matrix4f bind_pose) {
this->name = name;
this->number = number;
this->parent = parent;
this->bind_pose = bind_pose;
}
size_t generate(DataObjectGenerator& gen) const;
};
// basic
struct JointAnim {
std::string name;
s16 number;
s16 length;
explicit JointAnim(const Joint& joint) {
this->name = joint.name;
number = joint.number;
length = 1;
}
};
// basic
struct JointAnimCompressed : JointAnim {
std::vector<u32> data;
explicit JointAnimCompressed(const Joint& joint) : JointAnim(joint) {
number = joint.number;
length = 1;
}
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimFrame {
math::Matrix4f matrices[2];
std::vector<math::Matrix4f> data;
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedHDR {
u32 control_bits[14];
u32 num_joints;
u32 matrix_bits;
JointAnimCompressedHDR() {
for (auto& bit : control_bits) {
bit = 0;
}
num_joints = 1;
matrix_bits = 0;
}
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedFixed {
JointAnimCompressedHDR hdr;
u32 offset_64;
u32 offset_32;
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
JointAnimCompressedFixed() {
offset_64 = 0;
offset_32 = 0x88;
offset_16 = 0x90;
reserved = 0;
data[0] = math::Vector4f(1.0f, 0.0f, 0.0f, 0.0f);
data[1] = math::Vector4f(0.0f, 1.0f, 0.0f, 0.0f);
data[2] = math::Vector4f(0.0f, 0.0f, 1.0f, 0.0f);
data[3] = math::Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
data[4] = math::Vector4f(1.0f, 0.0f, 0.0f, 0.0f);
data[5] = math::Vector4f(0.0f, 1.0f, 0.0f, 0.0f);
data[6] = math::Vector4f(0.0f, 0.0f, 1.0f, 0.0f);
data[7] = math::Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
}
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedFrame {
u32 offset_64;
u32 offset_32;
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
JointAnimCompressedFrame() {
offset_64 = 0;
offset_32 = 0;
offset_16 = 0;
reserved = 0;
}
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedControl {
u32 num_frames;
u32 fixed_qwc;
u32 frame_qwc;
JointAnimCompressedFixed fixed{};
JointAnimCompressedFrame frame[1];
JointAnimCompressedControl() {
num_frames = 1;
fixed_qwc = 0xf;
frame_qwc = 1;
fixed = JointAnimCompressedFixed();
frame[0] = JointAnimCompressedFrame();
}
size_t generate(DataObjectGenerator& gen) const;
};
struct CollideMeshTri {
u8 vert_idx[3];
u8 unused;
PatSurface pat;
};
struct CollideMesh {
s32 joint_id;
u32 num_tris;
u32 num_verts;
std::vector<math::Vector4f> vertices;
CollideMeshTri tris;
};
struct ArtJointGeo : ArtElement {
std::vector<Joint> data;
CollideMesh mesh;
ResLump lump;
explicit ArtJointGeo(const std::string& name, const std::vector<Joint>& joints) {
this->name = name + "-lod0";
length = joints.size();
for (auto& joint : joints) {
data.push_back(joint);
}
}
size_t generate(DataObjectGenerator& gen) const;
size_t generate_res_lump(DataObjectGenerator& gen) const;
size_t generate_mesh(DataObjectGenerator& gen) const;
};
struct ArtJointAnim : ArtElement {
MercEyeAnimBlock eye_anim_data;
float speed;
float artist_base;
float artist_step;
std::string master_art_group_name;
s32 master_art_group_index;
u8* blerc_data = nullptr;
JointAnimCompressedControl frames;
std::vector<JointAnimCompressed> data;
ArtJointAnim(const std::string& name, const std::vector<Joint>& joints) {
this->name = name + "-idle";
length = joints.size();
speed = 1.0f;
artist_base = 0.0f;
artist_step = 1.0f;
master_art_group_name = name;
master_art_group_index = 2;
frames = JointAnimCompressedControl();
for (auto& joint : joints) {
data.emplace_back(joint);
}
}
size_t generate(DataObjectGenerator& gen) const;
};
struct ArtGroup : Art {
FileInfo info;
std::vector<ArtElement*> elts;
std::map<int, size_t> joint_map;
explicit ArtGroup(const std::string& file_name) {
info.file_type = "art-group";
info.file_name = "/src/next/data/art-group6/" + file_name + "-ag.go";
name = file_name;
info.major_version = versions::jak1::ART_FILE_VERSION;
info.minor_version = 0;
info.tool_debug = "Created by OpenGOAL buildactor";
info.mdb_file_name = "Unknown";
info.maya_file_name = "Unknown";
}
std::vector<u8> save_object_file() const;
};
bool run_build_actor(const std::string& input_model,
const std::string& output_file,
const std::string& output_prefix);
} // namespace jak1

View File

@ -0,0 +1,63 @@
#include "common/log/log.h"
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "goalc/build_actor/jak1/build_actor.h"
#include "third-party/CLI11.hpp"
int main(int argc, char** argv) {
// logging
lg::set_stdout_level(lg::level::info);
lg::set_flush_level(lg::level::info);
lg::initialize();
// game version
std::string game, input_model, output_file;
fs::path project_path_override;
// path
if (!file_util::setup_project_path(std::nullopt)) {
return 1;
}
lg::info("Build Actor Tool", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR);
CLI::App app{"OpenGOAL Compiler / REPL"};
app.add_option("input-model", input_model,
"Input model file (for example: custom_assets/jak1/models/test.glb)")
->required();
app.add_option("output-file", output_file,
"Output *-ag.go file (for example: out/jak1/obj/test-ag.go)")
->required();
app.add_option("-g,--game", game, "Game version (only jak1 for now)")->required();
app.add_option("--proj-path", project_path_override,
"Specify the location of the 'data/' folder");
app.validate_positionals();
CLI11_PARSE(app, argc, argv)
GameVersion game_version = game_name_to_version(game);
if (!project_path_override.empty()) {
if (!fs::exists(project_path_override)) {
lg::error("Error: project path override '{}' does not exist", project_path_override.string());
return 1;
}
if (!file_util::setup_project_path(project_path_override)) {
lg::error("Could not setup project path!");
return 1;
}
} else if (!file_util::setup_project_path(std::nullopt)) {
return 1;
}
switch (game_version) {
case GameVersion::Jak1:
jak1::run_build_actor(input_model, output_file, "jak1/");
break;
default:
ASSERT_NOT_REACHED_MSG("unsupported game version");
}
return 0;
}

View File

@ -1,12 +0,0 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include "common/custom_data/Tfrag3Data.h"
struct TexturePool {
std::unordered_map<std::string, int> textures_by_name;
std::vector<tfrag3::Texture> textures_by_idx;
};

View File

@ -6,8 +6,6 @@
#include "common/custom_data/Tfrag3Data.h"
#include "goalc/build_level/common/TexturePool.h"
class DataObjectGenerator;
struct DrawableTreeTfrag {

View File

@ -41,3 +41,23 @@ std::vector<decompiler::ObjectFileRecord> find_art_groups(
}
return art_groups;
}
void add_model_to_level(GameVersion version, const std::string& name, tfrag3::Level& lvl) {
lg::info("custom level: adding custom model {}", name);
auto glb = name + ".glb";
auto merc_data = load_merc_model(lvl.merc_data.indices.size(), lvl.merc_data.vertices.size(),
lvl.textures.size(),
fs::path(file_util::get_jak_project_dir() / "custom_assets" /
game_version_names[version] / "models" / glb)
.string(),
name + "-lod0");
for (auto& idx : merc_data.new_indices) {
lvl.merc_data.indices.push_back(idx);
}
for (auto& vert : merc_data.new_vertices) {
lvl.merc_data.vertices.push_back(vert);
}
lvl.merc_data.models.push_back(merc_data.new_model);
lvl.textures.insert(lvl.textures.end(), merc_data.new_textures.begin(),
merc_data.new_textures.end());
}

View File

@ -9,6 +9,7 @@
#include "common/util/string_util.h"
#include "decompiler/level_extractor/extract_level.h"
#include <goalc/build_actor/common/MercExtract.h>
void save_pc_data(const std::string& nickname, tfrag3::Level& data, const fs::path& fr3_output_dir);
std::vector<std::string> get_build_level_deps(const std::string& input_file);
@ -16,3 +17,4 @@ std::vector<decompiler::ObjectFileRecord> find_art_groups(
std::vector<std::string>& processed_ags,
const std::vector<std::string>& custom_level_ag,
const std::vector<decompiler::ObjectFileRecord>& dgo_files);
void add_model_to_level(GameVersion version, const std::string& name, tfrag3::Level& lvl);

View File

@ -11,474 +11,20 @@
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "common/util/Timer.h"
#include "common/util/gltf_util.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
namespace gltf_mesh_extract {
namespace {
/*!
* Convert a GLTF index buffer to std::vector<u32>
*/
template <typename T>
std::vector<u32> index_list_to_u32(const u8* data, u32 num_verts, u32 offset, u32 stride) {
std::vector<u32> result;
result.reserve(num_verts);
for (u32 i = 0; i < num_verts; i++) {
T val;
memcpy(&val, data, sizeof(T));
result.push_back(offset + val);
data += stride;
}
return result;
}
/*!
* Convert a GLTF position buffer or similar to std::vector<Vec3f>
*/
std::vector<math::Vector3f> extract_vec3f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector3f> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
memcpy(&result.emplace_back(), data, sizeof(math::Vector3f));
data += stride;
}
return result;
}
std::vector<math::Vector2f> extract_vec2f(const u8* data, u32 count, u32 stride) {
std::vector<math::Vector2f> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
memcpy(&result.emplace_back(), data, sizeof(math::Vector2f));
data += stride;
}
return result;
}
/*!
* Convert a GLTF color buffer (float format) to u8 colors.
*/
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_float(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<float, 4> temp;
memcpy(&temp, data, sizeof(math::Vector<float, 4>));
data += stride;
result.emplace_back(temp.x() * 255, temp.y() * 255, temp.z() * 255, temp.w() * 255);
}
return result;
}
/*!
* Convert a GLTF color buffer (u16 format) to u8 colors.
*/
std::vector<math::Vector<u8, 4>> extract_color_from_vec4_u16(const u8* data,
u32 count,
u32 stride) {
std::vector<math::Vector<u8, 4>> result;
result.reserve(count);
for (u32 i = 0; i < count; i++) {
math::Vector<u16, 4> temp;
memcpy(&temp, data, sizeof(math::Vector<u16, 4>));
data += stride;
result.emplace_back(temp.x() >> 8, temp.y() >> 8, temp.z() >> 8, temp.w() >> 8);
}
return result;
}
/*!
* Convert a GLTF index buffer
*/
std::vector<u32> gltf_index_buffer(const tinygltf::Model& model,
int indices_idx,
u32 index_offset) {
const auto& indices_accessor = model.accessors[indices_idx];
const auto& buffer_view = model.bufferViews[indices_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + indices_accessor.byteOffset;
const auto stride = indices_accessor.ByteStride(buffer_view);
const auto count = indices_accessor.count;
switch (indices_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_BYTE:
return index_list_to_u32<s8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
return index_list_to_u32<u8>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_SHORT:
return index_list_to_u32<s16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
return index_list_to_u32<u16>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_INT:
return index_list_to_u32<s32>(data_ptr, count, index_offset, stride);
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
return index_list_to_u32<u32>(data_ptr, count, index_offset, stride);
default:
ASSERT_MSG(false, "unsupported component type");
}
}
struct ExtractedVertices {
std::vector<tfrag3::PreloadedVertex> vtx;
std::vector<math::Vector<u8, 4>> vtx_colors;
std::vector<math::Vector3f> normals;
};
/*!
* Extract positions, colors, and normals from a mesh.
*/
ExtractedVertices gltf_vertices(const tinygltf::Model& model,
const std::map<std::string, int>& attributes,
const math::Matrix4f& w_T_local,
bool get_colors,
bool get_normals,
const std::string& debug_name) {
std::vector<tfrag3::PreloadedVertex> result;
std::vector<math::Vector<u8, 4>> vtx_colors;
{
const auto& position_attrib = attributes.find("POSITION");
ASSERT_MSG(position_attrib != attributes.end(), "Did not find position attribute.");
const auto attrib_accessor = model.accessors[position_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr = buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "POSITION wasn't vec3");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"POSITION wasn't float");
// for (auto& attrib : attributes) {
// lg::print("attrib: {}\n", attrib.first);
//}
auto mesh_verts = extract_vec3f(data_ptr, count, byte_stride);
result.reserve(mesh_verts.size());
for (auto& vert : mesh_verts) {
auto& new_vert = result.emplace_back();
math::Vector4f v_in(vert.x(), vert.y(), vert.z(), 1);
math::Vector4f v_w = w_T_local * v_in;
new_vert.x = v_w.x() * 4096;
new_vert.y = v_w.y() * 4096;
new_vert.z = v_w.z() * 4096;
}
}
if (get_colors) {
const auto& color_attrib = attributes.find("COLOR_0");
if (color_attrib == attributes.end()) {
lg::error("Mesh {} didn't have any colors, using white", debug_name);
for (size_t i = 0; i < result.size(); i++) {
vtx_colors.emplace_back(0x80, 0x80, 0x80, 0xff);
}
} else {
const auto attrib_accessor = model.accessors[color_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC4, "COLOR_0 wasn't vec4");
std::vector<math::Vector<u8, 4>> colors;
switch (attrib_accessor.componentType) {
case TINYGLTF_COMPONENT_TYPE_FLOAT:
colors = extract_color_from_vec4_float(data_ptr, count, byte_stride);
break;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
colors = extract_color_from_vec4_u16(data_ptr, count, byte_stride);
break;
default:
lg::die("Unknown component type for COLOR_0: {}", attrib_accessor.componentType);
}
vtx_colors.insert(vtx_colors.end(), colors.begin(), colors.end());
}
}
bool got_texture = false;
{
const auto& texcoord_attrib = attributes.find("TEXCOORD_0");
if (texcoord_attrib != attributes.end()) {
const auto attrib_accessor = model.accessors[texcoord_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC2, "TEXCOORD wasn't vec2");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"TEXCOORD wasn't float");
auto mesh_verts = extract_vec2f(data_ptr, count, byte_stride);
ASSERT(mesh_verts.size() == result.size());
got_texture = true;
for (size_t i = 0; i < mesh_verts.size(); i++) {
result[i].s = mesh_verts[i].x();
result[i].t = mesh_verts[i].y();
}
} else {
if (!get_normals) {
// don't warn if we're just getting collision
lg::warn("No texcoord attribute for mesh: {}", debug_name);
}
}
}
std::vector<math::Vector3f> normals;
if (get_normals) {
const auto& normal_attrib = attributes.find("NORMAL");
if (normal_attrib != attributes.end()) {
const auto attrib_accessor = model.accessors[normal_attrib->second];
const auto& buffer_view = model.bufferViews[attrib_accessor.bufferView];
const auto& buffer = model.buffers[buffer_view.buffer];
const auto data_ptr =
buffer.data.data() + buffer_view.byteOffset + attrib_accessor.byteOffset;
const auto byte_stride = attrib_accessor.ByteStride(buffer_view);
const auto count = attrib_accessor.count;
ASSERT_MSG(attrib_accessor.type == TINYGLTF_TYPE_VEC3, "NORMAL wasn't vec3");
ASSERT_MSG(attrib_accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT,
"NORMAL wasn't float");
normals = extract_vec3f(data_ptr, count, byte_stride);
for (auto& nrm : normals) {
math::Vector4f nrm4(nrm.x(), nrm.y(), nrm.z(), 0.f);
nrm = (w_T_local * nrm4).xyz();
}
ASSERT(normals.size() == result.size());
} else {
lg::error("No NORMAL attribute for mesh: {}", debug_name);
}
}
for (auto& v : result) {
v.color_index = 0;
if (!got_texture) {
v.s = 0;
v.t = 0;
}
}
// TODO: other properties
return {result, vtx_colors, normals};
}
DrawMode make_default_draw_mode() {
DrawMode mode;
mode.set_depth_write_enable(true);
mode.set_depth_test(GsTest::ZTest::GEQUAL);
mode.set_alpha_blend(DrawMode::AlphaBlend::DISABLED);
mode.set_aref(0);
mode.set_alpha_fail(GsTest::AlphaFail::KEEP);
mode.set_clamp_s_enable(false);
mode.set_clamp_t_enable(false);
mode.disable_filt(); // for checkerboard...
mode.enable_tcc(); // ?
mode.disable_at();
mode.enable_zt();
mode.disable_ab();
mode.disable_decal();
mode.enable_fog();
return mode;
}
int texture_pool_debug_checker(TexturePool* pool) {
const auto& existing = pool->textures_by_name.find("DEBUG_CHECKERBOARD");
if (existing == pool->textures_by_name.end()) {
size_t idx = pool->textures_by_idx.size();
pool->textures_by_name["DEBUG_CHECKERBOARD"] = idx;
auto& tex = pool->textures_by_idx.emplace_back();
tex.w = 16;
tex.h = 16;
tex.debug_name = "DEBUG_CHECKERBOARD";
tex.debug_tpage_name = "DEBUG";
tex.load_to_pool = false;
tex.combo_id = 0; // doesn't matter, not a pool tex
tex.data.resize(16 * 16);
u32 c0 = 0xa0303030;
u32 c1 = 0xa0e0e0e0;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
tex.data[i * 16 + j] = (((i / 4) & 1) ^ ((j / 4) & 1)) ? c1 : c0;
}
}
return idx;
} else {
return existing->second;
}
}
int texture_pool_add_texture(TexturePool* pool, const tinygltf::Image& tex) {
const auto& existing = pool->textures_by_name.find(tex.name);
if (existing != pool->textures_by_name.end()) {
lg::info("Reusing image: {}", tex.name);
return existing->second;
} else {
lg::info("adding new texture: {}, size {} kB", tex.name, tex.width * tex.height * 4 / 1024);
}
ASSERT(tex.bits == 8);
ASSERT(tex.component == 4);
ASSERT(tex.pixel_type == TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE);
size_t idx = pool->textures_by_idx.size();
pool->textures_by_name[tex.name] = idx;
auto& tt = pool->textures_by_idx.emplace_back();
tt.w = tex.width;
tt.h = tex.height;
tt.debug_name = tex.name;
tt.debug_tpage_name = "custom-level";
tt.load_to_pool = false;
tt.combo_id = 0; // doesn't matter, not a pool tex
tt.data.resize(tt.w * tt.h);
ASSERT(tex.image.size() >= tt.data.size());
memcpy(tt.data.data(), tex.image.data(), tt.data.size() * 4);
return idx;
}
} // namespace
math::Matrix4f affine_translation(const math::Vector3f& translation) {
math::Matrix4f result = math::Matrix4f::identity();
result(0, 3) = translation[0];
result(1, 3) = translation[1];
result(2, 3) = translation[2];
result(3, 3) = 1;
return result;
}
math::Matrix4f affine_scale(const math::Vector3f& scale) {
math::Matrix4f result = math::Matrix4f::zero();
result(0, 0) = scale[0];
result(1, 1) = scale[1];
result(2, 2) = scale[2];
result(3, 3) = 1;
return result;
}
math::Matrix4f affine_rot_qxyzw(const math::Vector4f& quat) {
math::Matrix4f result = math::Matrix4f::zero();
result(3, 3) = 1;
result(0, 0) = 1.0 - 2.0 * (quat.y() * quat.y() + quat.z() * quat.z());
result(0, 1) = 2.0 * (quat.x() * quat.y() - quat.z() * quat.w());
result(0, 2) = 2.0 * (quat.x() * quat.z() + quat.y() * quat.w());
result(1, 0) = 2.0 * (quat.x() * quat.y() + quat.z() * quat.w());
result(1, 1) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.z() * quat.z());
result(1, 2) = 2.0 * (quat.y() * quat.z() - quat.x() * quat.w());
result(2, 0) = 2.0 * (quat.x() * quat.z() - quat.y() * quat.w());
result(2, 1) = 2.0 * (quat.y() * quat.z() + quat.x() * quat.w());
result(2, 2) = 1.0 - 2.0 * (quat.x() * quat.x() + quat.y() * quat.y());
return result;
}
math::Vector3f vector3f_from_gltf(const std::vector<double>& in) {
ASSERT(in.size() == 3);
return math::Vector3f{in[0], in[1], in[2]};
}
math::Vector4f vector4f_from_gltf(const std::vector<double>& in) {
ASSERT(in.size() == 4);
return math::Vector4f{in[0], in[1], in[2], in[3]};
}
math::Matrix4f matrix_from_node(const tinygltf::Node& node) {
if (!node.matrix.empty()) {
math::Matrix4f result;
for (int i = 0; i < 16; i++) {
result.data()[i] = node.matrix[i];
}
return result;
} else {
// from trs
math::Matrix4f t, r, s;
if (!node.translation.empty()) {
t = affine_translation(vector3f_from_gltf(node.translation));
} else {
t = math::Matrix4f::identity();
}
if (!node.rotation.empty()) {
r = affine_rot_qxyzw(vector4f_from_gltf(node.rotation));
} else {
r = math::Matrix4f::identity();
}
if (!node.scale.empty()) {
s = affine_scale(vector3f_from_gltf(node.scale));
} else {
s = math::Matrix4f::identity();
}
return t * r * s;
}
}
struct NodeWithTransform {
int node_idx;
math::Matrix4f w_T_node;
};
/*!
* Recursively walk the tree of nodes, flatten, and compute w_T_node for each.
*/
void node_find_helper(const tinygltf::Model& model,
const math::Matrix4f& w_T_parent,
int node_idx,
std::vector<NodeWithTransform>* out) {
const auto& node = model.nodes.at(node_idx);
math::Matrix4f w_T_node = w_T_parent * matrix_from_node(node);
out->push_back({node_idx, w_T_node});
for (auto& child : node.children) {
node_find_helper(model, w_T_node, child, out);
}
}
std::vector<NodeWithTransform> flatten_nodes_from_all_scenes(const tinygltf::Model& model) {
std::vector<NodeWithTransform> out;
for (auto& scene : model.scenes) {
for (auto& nidx : scene.nodes) {
math::Matrix4f identity = math::Matrix4f::identity();
node_find_helper(model, identity, nidx, &out);
}
}
return out;
}
void dedup_vertices(const std::vector<tfrag3::PreloadedVertex>& vertices_in,
std::vector<tfrag3::PreloadedVertex>& vertices_out,
std::vector<u32>& old_to_new_out) {
ASSERT(vertices_out.empty());
ASSERT(old_to_new_out.empty());
old_to_new_out.resize(vertices_in.size(), -1);
std::unordered_map<tfrag3::PreloadedVertex, u32, tfrag3::PreloadedVertex::hash> vtx_to_new;
for (size_t in_idx = 0; in_idx < vertices_in.size(); in_idx++) {
auto& vtx = vertices_in[in_idx];
const auto& lookup = vtx_to_new.find(vtx);
if (lookup == vtx_to_new.end()) {
// first time seeing this one
size_t new_idx = vertices_out.size();
vertices_out.push_back(vtx);
old_to_new_out[in_idx] = new_idx;
vtx_to_new[vtx] = new_idx;
} else {
old_to_new_out[in_idx] = lookup->second;
}
}
}
void dedup_vertices(TfragOutput& data) {
Timer timer;
size_t original_size = data.vertices.size();
std::vector<tfrag3::PreloadedVertex> new_verts;
std::vector<u32> old_to_new;
dedup_vertices(data.vertices, new_verts, old_to_new);
gltf_util::dedup_vertices(data.vertices, new_verts, old_to_new);
data.vertices = std::move(new_verts);
for (auto& draw : data.strip_draws) {
@ -492,41 +38,6 @@ void dedup_vertices(TfragOutput& data) {
data.vertices.size(), 100.f * data.vertices.size() / original_size);
}
DrawMode draw_mode_from_sampler(const tinygltf::Sampler& sampler) {
DrawMode mode = make_default_draw_mode();
if (sampler.magFilter == TINYGLTF_TEXTURE_FILTER_NEAREST) {
ASSERT(sampler.minFilter == TINYGLTF_TEXTURE_FILTER_NEAREST);
mode.set_filt_enable(false);
} else {
ASSERT(sampler.minFilter != TINYGLTF_TEXTURE_FILTER_NEAREST);
mode.set_filt_enable(true);
}
switch (sampler.wrapS) {
case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
mode.set_clamp_s_enable(true);
break;
case TINYGLTF_TEXTURE_WRAP_REPEAT:
mode.set_clamp_s_enable(false);
break;
default:
ASSERT(false);
}
switch (sampler.wrapT) {
case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
mode.set_clamp_t_enable(true);
break;
case TINYGLTF_TEXTURE_WRAP_REPEAT:
mode.set_clamp_t_enable(false);
break;
default:
ASSERT(false);
}
return mode;
}
void extract(const Input& in,
TfragOutput& out,
const tinygltf::Model& model,

View File

@ -5,13 +5,16 @@
#include "common/custom_data/Tfrag3Data.h"
#include "goalc/build_level/collide/common/collide_common.h"
#include "goalc/build_level/common/TexturePool.h"
namespace gltf_util {
struct TexturePool;
}
namespace gltf_mesh_extract {
struct Input {
std::string filename;
TexturePool* tex_pool = nullptr;
gltf_util::TexturePool* tex_pool = nullptr;
bool get_colors = true;
bool auto_wall_enable = true;
float auto_wall_angle = 30.f;

View File

@ -1,5 +1,7 @@
#include "build_level.h"
#include "common/util/gltf_util.h"
#include "decompiler/extractor/extractor_util.h"
#include "decompiler/level_extractor/extract_merc.h"
#include "goalc/build_level/collide/jak1/collide_bvh.h"
@ -15,9 +17,9 @@ bool run_build_level(const std::string& input_file,
const std::string& output_prefix) {
auto level_json = parse_commented_json(
file_util::read_text_file(file_util::get_file_path({input_file})), input_file);
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
TexturePool tex_pool; // pc level texture pool
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
gltf_util::TexturePool tex_pool; // pc level texture pool
// process input mesh from blender
gltf_mesh_extract::Input mesh_extract_in;
@ -202,6 +204,14 @@ bool run_build_level(const std::string& input_file,
}
}
// add custom models to fr3
if (level_json.contains("custom_models") && !level_json.at("custom_models").empty()) {
auto models = level_json.at("custom_models").get<std::vector<std::string>>();
for (auto& name : models) {
add_model_to_level(GameVersion::Jak1, name, pc_level);
}
}
// Save the PC level
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");

View File

@ -1,5 +1,7 @@
#include "build_level.h"
#include "common/util/gltf_util.h"
#include "decompiler/extractor/extractor_util.h"
#include "decompiler/level_extractor/extract_merc.h"
#include "goalc/build_level/collide/jak2/collide.h"
@ -14,9 +16,9 @@ bool run_build_level(const std::string& input_file,
const std::string& output_prefix) {
auto level_json = parse_commented_json(
file_util::read_text_file(file_util::get_file_path({input_file})), input_file);
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
TexturePool tex_pool; // pc level texture pool
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
gltf_util::TexturePool tex_pool; // pc level texture pool
// process input mesh from blender
gltf_mesh_extract::Input mesh_extract_in;

View File

@ -14,9 +14,9 @@ bool run_build_level(const std::string& input_file,
const std::string& output_prefix) {
auto level_json = parse_commented_json(
file_util::read_text_file(file_util::get_file_path({input_file})), input_file);
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
TexturePool tex_pool; // pc level texture pool
LevelFile file; // GOAL level file
tfrag3::Level pc_level; // PC level file
gltf_util::TexturePool tex_pool; // pc level texture pool
// process input mesh from blender
gltf_mesh_extract::Input mesh_extract_in;

View File

@ -28,8 +28,9 @@ int main(int argc, char** argv) {
lg::info("Build Level Tool", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR);
CLI::App app{"OpenGOAL Compiler / REPL"};
app.add_option("input-json", input_json,
"Input JSON file (for example, custom_levels/jak2/test-zone/test-zone.jsonc)")
app.add_option(
"input-json", input_json,
"Input JSON file (for example, custom_assets/jak2/levels/test-zone/test-zone.jsonc)")
->required();
app.add_option("output-file", output_file,
"Output .go file, (for example out/jak2/obj/test-zone.go)")

View File

@ -105,6 +105,7 @@ MakeSystem::MakeSystem(const std::optional<REPL::Config> repl_config, const std:
add_tool<BuildLevelTool>();
add_tool<BuildLevel2Tool>();
add_tool<BuildLevel3Tool>();
add_tool<BuildActorTool>();
}
/*!

View File

@ -4,6 +4,7 @@
#include "common/util/DgoWriter.h"
#include "common/util/FileUtil.h"
#include "goalc/build_actor/jak1/build_actor.h"
#include "goalc/build_level/jak1/build_level.h"
#include "goalc/build_level/jak2/build_level.h"
#include "goalc/build_level/jak3/build_level.h"
@ -295,4 +296,23 @@ bool BuildLevel3Tool::run(const ToolInput& task, const PathMap& path_map) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
return jak3::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix);
}
BuildActorTool::BuildActorTool() : Tool("build-actor") {}
bool BuildActorTool::needs_run(const ToolInput& task, const PathMap& path_map) {
(void)path_map;
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
// std::vector<std::string> deps{};
// return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
return true;
}
bool BuildActorTool::run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
return jak1::run_build_actor(task.input.at(0), task.output.at(0), path_map.output_prefix);
}

View File

@ -92,3 +92,10 @@ class BuildLevel3Tool : public Tool {
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
class BuildActorTool : public Tool {
public:
BuildActorTool();
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};