custom levels: etie and build actor support for jak2/3 (#3851)

Custom levels for Jak 2/3 now support envmapped TIE geometry. The TIE
extract was also changed to ignore materials that have the specular flag
set, but are missing a roughness texture.

Jak 2/3 now also support the `build-actor` tool.

The `build-custom-level` and `build-actor` macros now have a few new
options:

- Both now have a `force-run` option (`#f` by default) that, when set to
`#t`, will always run level/art group generation even if the output
files are up to date.
- `build-custom-level` has a `gen-fr3` option (`#t` by default) that,
when set to `#f`, will skip generating the FR3 file for the custom level
and only generate the GOAL level file to skip the potentially slow
process of finding and adding art groups and textures. Useful for when
you want to temporarily edit only the GOAL side of the level (such as
entity placement, etc.).
- `build-actor` has a `texture-bucket` option (default 0) which will
determine what DMA sink group the model will be placed in, which is
useful to determine the draw order of the model. Previously, this was
omitted, resulting in shadows not drawing over custom actors because the
actors were put in a bucket that is drawn after shadows (this behavior
can be restored with `:texture-bucket #f`).
This commit is contained in:
Hat Kid
2025-02-01 18:04:26 +01:00
committed by GitHub
parent 23b86d95c3
commit 710f3ac117
46 changed files with 3023 additions and 532 deletions
+3 -2
View File
@@ -685,14 +685,15 @@ bool material_has_envmap(const tinygltf::Material& mat) {
}
bool envmap_is_valid(const tinygltf::Material& mat) {
if (material_has_envmap(mat) && mat.pbrMetallicRoughness.metallicRoughnessTexture.index < 0) {
auto envmap = material_has_envmap(mat);
if (envmap && mat.pbrMetallicRoughness.metallicRoughnessTexture.index < 0) {
lg::warn(fmt::format(
"Material \"{}\" has specular property set, but is missing a metallic roughness texture, "
"ignoring envmap!",
mat.name));
return false;
}
return true;
return envmap;
}
std::optional<int> find_single_skin(const tinygltf::Model& model,
@@ -56,6 +56,10 @@
// Removed so that the release builds don't have to double-decompile the game
// "art_groups": ["prsn-torture-ag"],
// If you have any custom models in the "custom_assets/jak2/models/custom_levels" 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 textures which are not in the common level files and have no art group associated with them.
// To get a list of all the textures, you can extract all of the game's textures
@@ -99,6 +103,17 @@
"lump": {
"name": "test-torture"
}
},
{
"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": [5.41, 3.5, 28.42, 10], // bounding sphere
"lump": {
"name": "test-actor"
}
}
]
}
@@ -5,5 +5,7 @@
("TSZ.DGO"
(
"prison-obs.o"
"test-zone-obs.o"
"test-actor-ag.go"
"test-zone.go"
))
@@ -55,6 +55,10 @@
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// "art_groups": [],
// If you have any custom models in the "custom_assets/jak3/models/custom_levels" 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 textures which are not in the common level files and have no art group associated with them.
// To get a list of all the textures, you can extract all of the game's textures
@@ -87,6 +91,17 @@
"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": [5.41, 3.5, 28.42, 10], // bounding sphere
"lump": {
"name": "test-actor"
}
}
]
}
@@ -3,6 +3,8 @@
;; the actual file name still needs to be 8.3
("TSZ.DGO"
("test-zone.go"
)
)
(
"test-zone-obs.o"
"test-actor-ag.go"
"test-zone.go"
))
+4 -4
View File
@@ -218,15 +218,15 @@
)
)
(defmacro build-custom-level (name)
(defmacro build-custom-level (name &key (force-run #f) &key (gen-fr3 #t))
(let* ((path (string-append "custom_assets/jak1/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
`(defstep :in '(,path ,(symbol->string force-run) ,(symbol->string gen-fr3))
:tool 'build-level
:out '(,(string-append "$OUT/obj/" name ".go")))))
(defmacro build-actor (name &key (gen-mesh #f))
(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0))
(let* ((path (string-append "custom_assets/jak1/models/custom_levels/" name ".glb")))
`(defstep :in '(,path ,(symbol->string gen-mesh))
`(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket)))
:tool 'build-actor
:out '(,(string-append "$OUT/obj/" name "-ag.go")))))
+25
View File
@@ -420,5 +420,30 @@
)
)
(defmacro def-actor (name &key (idle #f) &key (lods #f) &key (art (idle-ja)) &key (joints ()) &key (shadow 0) &key bounds &key (longest-edge 0.0) &key (texture-level 0) &key (sort 0) &key (version 7) &key (origin-joint-index 0) &key (shadow-joint-index 0) &key (light-index 0))
`(begin
(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-lod0-jg" name) 0)
(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-lod0-mg" name) 1)
,@(apply-i (lambda (x i)
`(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-{}" name x) ,(+ i 2)))
art)
,@(apply-i (lambda (x i) `(def-joint-node ,(string->symbol-format "{}-lod0-jg" name) ,(symbol->string x) ,(1+ i))) joints)
(defskelgroup ,(string->symbol-format "*{}-sg*" name)
,name
,(string->symbol-format "{}-lod0-jg" name)
,(if idle (string->symbol-format "{}-{}" name idle) (string->symbol-format "{}-{}" name (car art)))
,(if lods
`(,@(apply (lambda (x) `(,(string->symbol-format "{}-{}-mg" name (car x)) (meters ,(cadr x)))) lods))
`((,(string->symbol-format "{}-lod0-mg" name) (meters 999999))))
:version ,version
:shadow ,shadow
:bounds (static-spherem ,@bounds)
:longest-edge ,longest-edge
:texture-level ,texture-level
:sort ,sort
:origin-joint-index ,origin-joint-index
:shadow-joint-index ,shadow-joint-index
:light-index ,light-index)))
(import "goal_src/jak2/engine/data/art-elts.gc")
(import "goal_src/jak2/engine/data/joint-nodes.gc")
+6
View File
@@ -300,8 +300,14 @@
;; it should point to the .jsonc file that specifies the level.
(build-custom-level "test-zone")
;; the DGO file
(goal-src "levels/test-zone/test-zone-obs.gc" "process-focusable")
(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/custom_levels
;; to also generate a collide-mesh, add :gen-mesh #t
(build-actor "test-actor" :force-run #t :gen-mesh #t)
;;;;;;;;;;;;;;;;;;;;;
;; ANIMATIONS
;;;;;;;;;;;;;;;;;;;;;
@@ -0,0 +1,101 @@
;;-*-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_) object))
(:state-methods
idle))
(def-actor test-actor
:bounds (0 0 0 5))
(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) cshape-reaction-default)
(set! (-> cshape no-reaction) (the (function collide-shape-moving collide-query vector vector object) nothing))
(let ((mesh (new 'process 'collide-shape-prim-mesh cshape (the uint 0) (the uint 0))))
(set! (-> mesh prim-core collide-as) (collide-spec pusher))
(set! (-> mesh collide-with) (collide-spec jak player-list))
(set! (-> mesh prim-core action) (collide-action solid rideable))
(set! (-> mesh transform-index) 0)
(set! (-> cshape total-prims) (the uint 1))
(set! (-> cshape root-prim) mesh)
(set-vector! (-> mesh local-sphere) 0.0 (meters 0) 0.0 (meters 5)))
(pusher-init cshape)
(set! (-> cshape nav-radius) (* 0.75 (-> cshape root-prim local-sphere w)))
(let ((prim (-> cshape root-prim)))
(set! (-> cshape backup-collide-as) (-> prim prim-core collide-as))
(set! (-> cshape backup-collide-with) (-> prim prim-core collide-with))
)
(set! (-> this root) cshape)))
(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)))))
; )
#t)))
: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))))
; (debug-draw-tris (-> (res-lump-struct (-> self draw art-group data 0 extra) 'collide-mesh-group (array collide-mesh)) 0) self 2)
; (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)
+8 -2
View File
@@ -135,12 +135,18 @@
)
)
(defmacro build-custom-level (name)
(defmacro build-custom-level (name &key (force-run #f) &key (gen-fr3 #t))
(let* ((path (string-append "custom_assets/jak2/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
`(defstep :in '(,path ,(symbol->string force-run) ,(symbol->string gen-fr3))
:tool 'build-level2
:out '(,(string-append "$OUT/obj/" name ".go")))))
(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0))
(let* ((path (string-append "custom_assets/jak2/models/custom_levels/" name ".glb")))
`(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket)))
:tool 'build-actor2
:out '(,(string-append "$OUT/obj/" name "-ag.go")))))
(defmacro group (name &rest stuff)
`(defstep :in ""
:tool 'group
+27
View File
@@ -486,5 +486,32 @@ Each process-drawable has a draw-control."
)
)
(defmacro def-actor (name &key (idle #f) &key (lods #f) &key (art (idle-ja)) &key (joints ()) &key (shadow 0) &key bounds &key (longest-edge 0.0) &key (texture-level 0) &key (sort 0) &key (version 7) &key (origin-joint-index 0) &key (shadow-joint-index 0) &key (light-index 0) &key (global-effects 0) &key (clothing ()))
`(begin
(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-lod0-jg" name) 0)
(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-lod0-mg" name) 1)
,@(apply-i (lambda (x i)
`(def-art-elt ,(string->symbol-format "{}-ag" name) ,(string->symbol-format "{}-{}" name x) ,(+ i 2)))
art)
,@(apply-i (lambda (x i) `(def-joint-node ,(string->symbol-format "{}-lod0-jg" name) ,(symbol->string x) ,(1+ i))) joints)
(defskelgroup ,(string->symbol-format "*{}-sg*" name)
,name
,(string->symbol-format "{}-lod0-jg" name)
,(if idle (string->symbol-format "{}-{}" name idle) (string->symbol-format "{}-{}" name (car art)))
,(if lods
`(,@(apply (lambda (x) `(,(string->symbol-format "{}-{}-mg" name (car x)) (meters ,(cadr x)))) lods))
`((,(string->symbol-format "{}-lod0-mg" name) (meters 999999))))
:version ,version
:shadow ,shadow
:bounds (static-spherem ,@bounds)
:longest-edge ,longest-edge
:texture-level ,texture-level
:sort ,sort
:origin-joint-index ,origin-joint-index
:shadow-joint-index ,shadow-joint-index
:light-index ,light-index
:global-effects ,global-effects
:clothing ,clothing)))
(import "goal_src/jak3/engine/data/art-elts.gc")
(import "goal_src/jak3/engine/data/joint-nodes.gc")
+6
View File
@@ -403,8 +403,14 @@
;; it should point to the .jsonc file that specifies the level.
(build-custom-level "test-zone")
;; the DGO file
(goal-src "levels/test-zone/test-zone-obs.gc" "process-focusable")
(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/jak3/models/custom_levels
;; to also generate a collide-mesh, add :gen-mesh #t
(build-actor "test-actor" :gen-mesh #t)
;;;;;;;;;;;;;;;;;;;;;
;; ANIMATIONS
;;;;;;;;;;;;;;;;;;;;;
@@ -0,0 +1,100 @@
;;-*-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_) object))
(:state-methods
idle))
(def-actor test-actor
:bounds (0 0 0 5))
(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) cshape-reaction-default)
(set! (-> cshape no-reaction) (the (function collide-shape-moving collide-query vector vector object) nothing))
(let ((mesh (new 'process 'collide-shape-prim-mesh cshape (the uint 0) (the uint 0))))
(set! (-> mesh prim-core collide-as) (collide-spec pusher))
(set! (-> mesh collide-with) (collide-spec jak player-list))
(set! (-> mesh prim-core action) (collide-action solid rideable))
(set! (-> mesh transform-index) 0)
(set! (-> cshape total-prims) (the uint 1))
(set! (-> cshape root-prim) mesh)
(set-vector! (-> mesh local-sphere) 0.0 (meters 0) 0.0 (meters 5)))
(pusher-init cshape)
(set! (-> cshape nav-radius) (* 0.75 (-> cshape root-prim local-sphere w)))
(let ((prim (-> cshape root-prim)))
(set! (-> cshape backup-collide-as) (-> prim prim-core collide-as))
(set! (-> cshape backup-collide-with) (-> prim prim-core collide-with))
)
(set! (-> this root) cshape)))
(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))
(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)))))
; )
#t)))
: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))))
; (debug-draw-tris (-> (res-lump-struct (-> self draw art-group data 0 extra) 'collide-mesh-group (array collide-mesh)) 0) self 2)
; (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)
+9 -3
View File
@@ -247,8 +247,14 @@
)
)
(defmacro build-custom-level (name)
(defmacro build-custom-level (name &key (force-run #f) &key (gen-fr3 #t))
(let* ((path (string-append "custom_assets/jak3/levels/" name "/" name ".jsonc")))
`(defstep :in ,path
`(defstep :in '(,path ,(symbol->string force-run) ,(symbol->string gen-fr3))
:tool 'build-level3
:out '(,(string-append "$OUT/obj/" name ".go")))))
:out '(,(string-append "$OUT/obj/" name ".go")))))
(defmacro build-actor (name &key (gen-mesh #f) &key (force-run #f) &key (texture-bucket 0))
(let* ((path (string-append "custom_assets/jak3/models/custom_levels/" name ".glb")))
`(defstep :in '(,path ,(symbol->string gen-mesh) ,(symbol->string force-run) ,(if (integer? texture-bucket) (int->string texture-bucket) (symbol->string texture-bucket)))
:tool 'build-actor3
:out '(,(string-append "$OUT/obj/" name "-ag.go")))))
+3
View File
@@ -60,7 +60,10 @@ add_library(compiler
data_compiler/dir_tpages.cpp
data_compiler/game_count.cpp
data_compiler/DataObjectGenerator.cpp
build_actor/common/build_actor.cpp
build_actor/jak1/build_actor.cpp
build_actor/jak2/build_actor.cpp
build_actor/jak3/build_actor.cpp
debugger/Debugger.cpp
debugger/DebugInfo.cpp
listener/Listener.cpp
+199 -1
View File
@@ -180,7 +180,7 @@ gltf_util::MercSwapData load_merc_model(u32 current_idx_count,
return result;
}
std::vector<jak1::CollideMesh> gen_collide_mesh_from_model(
std::vector<jak1::CollideMesh> gen_collide_mesh_from_model_jak1(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx) {
@@ -278,3 +278,201 @@ std::vector<jak1::CollideMesh> gen_collide_mesh_from_model(
}
return cmeshes;
}
std::vector<jak2::CollideMesh> gen_collide_mesh_from_model_jak2(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx) {
// data for a single primitive
struct PrimWork {
std::string mesh_name;
std::vector<math::Vector4f> verts;
std::vector<u32> indices;
gltf_mesh_extract::PatResult pat;
};
std::vector<std::vector<PrimWork>> mesh_data;
int mesh_count = 0;
// int prim_count = 0;
for (const auto& n : all_nodes) {
const auto& node = model.nodes[n.node_idx];
gltf_mesh_extract::PatResult mesh_default_collide =
gltf_mesh_extract::custom_props_to_pat(node.extras, node.name);
if (node.mesh >= 0) {
const auto& mesh = model.meshes[node.mesh];
mesh_count++;
std::vector<PrimWork> prims;
for (const auto& prim : mesh.primitives) {
// get material
const auto& mat_idx = prim.material;
gltf_mesh_extract::PatResult pat = mesh_default_collide;
if (mat_idx != -1) {
const auto& mat = model.materials[mat_idx];
auto mat_pat = gltf_mesh_extract::custom_props_to_pat(mat.extras, mat.name);
if (mat_pat.set) {
pat = mat_pat;
}
}
if (pat.set && pat.ignore) {
continue; // skip, no collide here
}
auto& prim_data = prims.emplace_back();
prim_data.mesh_name = mesh.name;
prim_data.pat = pat;
// prim_count++;
// extract index buffer
std::vector<u32> prim_indices = gltf_util::gltf_index_buffer(model, prim.indices, 0);
ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES,
fmt::format("Mesh {}: Unsupported triangle mode {}", mesh.name, prim.mode));
// extract vertices
auto verts =
gltf_util::gltf_vertices(model, prim.attributes, n.w_T_node, true, true, mesh.name);
ASSERT_MSG(verts.vtx.size() <= 255,
fmt::format("primitive of mesh {} has too many vertices (max 255, actual {})",
mesh.name, verts.vtx.size()));
prim_data.verts.reserve(verts.vtx.size());
for (auto& vert : verts.vtx) {
prim_data.verts.emplace_back(vert.x, vert.y, vert.z, 1.0);
}
prim_data.indices = prim_indices;
mesh_data.push_back(prims);
}
}
}
std::vector<jak2::CollideMesh> cmeshes;
cmeshes.reserve(mesh_count);
// we extracted all of the prim data for each mesh, now combine
for (size_t p = 0; p < mesh_data.size(); p++) {
auto& prims = mesh_data.at(p);
std::vector<math::Vector4f> verts;
std::vector<jak2::CollideMeshTri> tris;
int vert_count = 0;
for (auto& prim : prims) {
if (prim.pat.ignore) {
continue;
}
vert_count += verts.size();
verts.insert(verts.end(), prim.verts.begin(), prim.verts.end());
for (size_t i = 0; i < prim.indices.size(); i += 3) {
auto& tri = tris.emplace_back();
tri.vert_idx[0] = prim.indices.at(i);
tri.vert_idx[1] = prim.indices.at(i + 1);
tri.vert_idx[2] = prim.indices.at(i + 2);
tri.pat = jak2_pat(prim.pat.pat);
}
}
ASSERT_MSG(vert_count <= 255, fmt::format("Mesh {} has too many vertices (max 255, actual {})",
prims.at(p).mesh_name, vert_count));
auto& cmesh = cmeshes.emplace_back();
// TODO joint idx as a custom property in blender?
cmesh.joint_id = joint_idx;
cmesh.vertices.reserve(255);
cmesh.vertices.insert(cmesh.vertices.begin(), verts.begin(), verts.end());
cmesh.num_verts = cmesh.vertices.size();
cmesh.num_tris = tris.size();
cmesh.tris.reserve(cmesh.num_tris);
for (size_t j = 0; j < cmesh.num_tris; j++) {
cmesh.tris.push_back(tris.at(j));
}
}
return cmeshes;
}
std::vector<jak3::CollideMesh> gen_collide_mesh_from_model_jak3(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx) {
// data for a single primitive
struct PrimWork {
std::string mesh_name;
std::vector<math::Vector4f> verts;
std::vector<u32> indices;
gltf_mesh_extract::PatResult pat;
};
std::vector<std::vector<PrimWork>> mesh_data;
int mesh_count = 0;
// int prim_count = 0;
for (const auto& n : all_nodes) {
const auto& node = model.nodes[n.node_idx];
gltf_mesh_extract::PatResult mesh_default_collide =
gltf_mesh_extract::custom_props_to_pat(node.extras, node.name);
if (node.mesh >= 0) {
const auto& mesh = model.meshes[node.mesh];
mesh_count++;
std::vector<PrimWork> prims;
for (const auto& prim : mesh.primitives) {
// get material
const auto& mat_idx = prim.material;
gltf_mesh_extract::PatResult pat = mesh_default_collide;
if (mat_idx != -1) {
const auto& mat = model.materials[mat_idx];
auto mat_pat = gltf_mesh_extract::custom_props_to_pat(mat.extras, mat.name);
if (mat_pat.set) {
pat = mat_pat;
}
}
if (pat.set && pat.ignore) {
continue; // skip, no collide here
}
auto& prim_data = prims.emplace_back();
prim_data.mesh_name = mesh.name;
prim_data.pat = pat;
// prim_count++;
// extract index buffer
std::vector<u32> prim_indices = gltf_util::gltf_index_buffer(model, prim.indices, 0);
ASSERT_MSG(prim.mode == TINYGLTF_MODE_TRIANGLES,
fmt::format("Mesh {}: Unsupported triangle mode {}", mesh.name, prim.mode));
// extract vertices
auto verts =
gltf_util::gltf_vertices(model, prim.attributes, n.w_T_node, true, true, mesh.name);
ASSERT_MSG(verts.vtx.size() <= 255,
fmt::format("primitive of mesh {} has too many vertices (max 255, actual {})",
mesh.name, verts.vtx.size()));
prim_data.verts.reserve(verts.vtx.size());
for (auto& vert : verts.vtx) {
prim_data.verts.emplace_back(vert.x, vert.y, vert.z, 1.0);
}
prim_data.indices = prim_indices;
mesh_data.push_back(prims);
}
}
}
std::vector<jak3::CollideMesh> cmeshes;
cmeshes.reserve(mesh_count);
// we extracted all of the prim data for each mesh, now combine
for (size_t p = 0; p < mesh_data.size(); p++) {
auto& prims = mesh_data.at(p);
std::vector<math::Vector4f> verts;
std::vector<jak3::CollideMeshTri> tris;
int vert_count = 0;
for (auto& prim : prims) {
if (prim.pat.ignore) {
continue;
}
vert_count += verts.size();
verts.insert(verts.end(), prim.verts.begin(), prim.verts.end());
for (size_t i = 0; i < prim.indices.size(); i += 3) {
auto& tri = tris.emplace_back();
tri.vert_idx[0] = prim.indices.at(i);
tri.vert_idx[1] = prim.indices.at(i + 1);
tri.vert_idx[2] = prim.indices.at(i + 2);
tri.pat = jak3_pat(prim.pat.pat);
}
}
ASSERT_MSG(vert_count <= 255, fmt::format("Mesh {} has too many vertices (max 255, actual {})",
prims.at(p).mesh_name, vert_count));
auto& cmesh = cmeshes.emplace_back();
// TODO joint idx as a custom property in blender?
cmesh.joint_id = joint_idx;
cmesh.vertices.reserve(255);
cmesh.vertices.insert(cmesh.vertices.begin(), verts.begin(), verts.end());
cmesh.num_verts = cmesh.vertices.size();
cmesh.num_tris = tris.size();
cmesh.tris.reserve(cmesh.num_tris);
for (size_t j = 0; j < cmesh.num_tris; j++) {
cmesh.tris.push_back(tris.at(j));
}
}
return cmeshes;
}
+12 -1
View File
@@ -3,6 +3,8 @@
#include "common/util/gltf_util.h"
#include "goalc/build_actor/jak1/build_actor.h"
#include "goalc/build_actor/jak2/build_actor.h"
#include "goalc/build_actor/jak3/build_actor.h"
void extract(const std::string& name,
gltf_util::MercExtractData& out,
@@ -17,7 +19,16 @@ gltf_util::MercSwapData load_merc_model(u32 current_idx_count,
u32 current_tex_count,
const std::string& path,
const std::string& name);
std::vector<jak1::CollideMesh> gen_collide_mesh_from_model(
std::vector<jak1::CollideMesh> gen_collide_mesh_from_model_jak1(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx);
std::vector<jak2::CollideMesh> gen_collide_mesh_from_model_jak2(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx);
std::vector<jak3::CollideMesh> gen_collide_mesh_from_model_jak3(
const tinygltf::Model& model,
const std::vector<gltf_util::NodeWithTransform>& all_nodes,
int joint_idx);
+171
View File
@@ -0,0 +1,171 @@
#include "build_actor.h"
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "goalc/build_actor/common/MercExtract.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
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;
}
/*!
* Load tinygltf::Model from a .glb file (binary format), fatal error if it fails.
*/
tinygltf::Model load_gltf_model(const fs::path& path) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path.string());
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
return model;
}
/*!
* Extract the "skeleton" structure from a GLTF model's skin. This requires that the skin's joints
* are topologically sorted (parents always have lower index than children).
*/
std::vector<GltfJoint> extract_skeleton(const tinygltf::Model& model, int skin_idx) {
const auto& skin = model.skins.at(skin_idx);
lg::info("skin name is {}", skin.name);
lg::info("skeleton root is {}", skin.skeleton);
auto inverse_bind_matrices = extract_mat4(model, skin.inverseBindMatrices);
ASSERT(inverse_bind_matrices.size() == skin.joints.size());
std::map<int, int> node_to_joint;
std::map<int, int> joint_to_node;
std::vector<GltfJoint> joints;
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// auto ibm = inverse_bind_matrices[i];
// lg::info(" joint {}", joint_node_idx);
// lg::info(" {}", joint_node.name);
// lg::info("\n{}", ibm.to_string_aligned());
node_to_joint[joint_node_idx] = i;
joint_to_node[i] = joint_node_idx;
auto& gjoint = joints.emplace_back();
gjoint.bind_pose_T_w = inverse_bind_matrices[i];
gjoint.name = joint_node.name;
gjoint.gltf_node_index = joint_node_idx;
}
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// set up children
for (int child_node_idx : joint_node.children) {
int child_joint_idx = node_to_joint.at(child_node_idx);
joints.at(i).children.push_back(child_joint_idx);
auto& child = joints.at(child_joint_idx);
ASSERT(child.parent == -1);
child.parent = i;
ASSERT(child_joint_idx > (int)i);
}
}
ASSERT(joints.at(0).parent == -1);
// for (auto& joint : joints) {
// if (joint.parent == -1) {
// lg::warn("parentless {}", joint.name);
// } else {
// lg::info("joint {}, child of {}", joint.name, joints.at(joint.parent).name);
// }
// }
lg::info("total of {} joints", joints.size());
return joints;
}
/*!
* Convert from GLTF joint format to game joint format.
* @param joint_index the index of the joint, in the GLTF file.
* @param prefix_joint_count number of joints to be inserted before GLTF joints in the game
* @param parent_of_gltf the parent game joint of all GLTF joints.
*/
Joint convert_joint(const GltfJoint& joint,
int joint_index,
int prefix_joint_count,
int parent_of_gltf) {
// node matrix is p_T_myself
// p_T_myself = parent_bind_pose_T_w * w_T_bind_pose
int parent;
if (joint.parent == -1) {
parent = parent_of_gltf;
} else {
parent = joint.parent + prefix_joint_count;
}
math::Matrix4f fixed_matrix = joint.bind_pose_T_w;
for (int i = 0; i < 3; i++) {
fixed_matrix(i, 3) *= 4096;
}
return Joint(joint.name, joint_index + prefix_joint_count, parent, fixed_matrix.transposed());
}
constexpr int kGltfToGameJointOffset = 1;
/*!
* Convert GTLF joint list to game joint list.
* Currently, this inserts a single "align" joint and places the root joint of the GLTF as the
* prejoint. However, we might want to change this, to allow GLTF files to specify "align" at some
* point.
*/
std::vector<Joint> convert_joints(const std::vector<GltfJoint>& gjoints) {
std::vector<Joint> joints;
joints.emplace_back("align", 0, -1, math::Matrix4f::identity());
ASSERT(kGltfToGameJointOffset == joints.size());
for (int gjoint_idx = 0; gjoint_idx < int(gjoints.size()); gjoint_idx++) {
// using -1 as the parent index since gltf's shouldn't be child of align.
joints.push_back(convert_joint(gjoints[gjoint_idx], gjoint_idx, kGltfToGameJointOffset, -1));
}
return joints;
}
std::vector<anim::CompressedAnim> process_anim(const tinygltf::Model& model,
const std::vector<GltfJoint>& gjoints) {
if (model.animations.empty()) {
lg::warn("no animations detected!"); // TODO: make up a dummy one
return {};
}
std::map<int, int> node_to_joint;
for (size_t i = 0; i < gjoints.size(); i++) {
node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset;
}
std::vector<anim::CompressedAnim> ret;
for (auto& anim : model.animations) {
lg::info("Processing animation {}", anim.name);
ret.push_back(
anim::compress_animation(anim::extract_anim_from_gltf(model, anim, node_to_joint, 60)));
}
return ret;
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "common/util/gltf_util.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_level/collide/common/collide_common.h"
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;
};
extern std::map<int, size_t> g_joint_map;
struct BuildActorParams {
bool gen_collide_mesh = false;
s8 texture_bucket = -1;
s32 texture_level = -1;
s32 joint_channel = -1;
math::Vector4f trans_offset = {0.f, 0.f, 0.f, 1.f};
std::vector<float> lod_dist{};
};
struct GltfJoint {
math::Matrix4f bind_pose_T_w; // inverse bind pose
std::string name;
int gltf_node_index = 0;
int parent = -1;
std::vector<int> children;
};
tinygltf::Model load_gltf_model(const fs::path& path);
std::vector<GltfJoint> extract_skeleton(const tinygltf::Model& model, int skin_idx);
std::vector<Joint> convert_joints(const std::vector<GltfJoint>& gjoints);
std::vector<anim::CompressedAnim> process_anim(const tinygltf::Model& model,
const std::vector<GltfJoint>& gjoints);
+23 -182
View File
@@ -138,27 +138,6 @@ ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<J
}
}
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");
@@ -281,16 +260,25 @@ void ArtJointGeo::add_res() {
lump.add_res(
std::make_unique<ResRef>("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME));
}
// 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));
if (texture_bucket != -1) {
lump.add_res(std::make_unique<ResUint8>(
"texture-bucket", std::vector<u8>{static_cast<u8>(texture_bucket)}, DEFAULT_RES_TIME));
}
if (texture_level != -1) {
lump.add_res(std::make_unique<ResInt32>("texture-level", std::vector<s32>{texture_level},
DEFAULT_RES_TIME));
}
if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) {
lump.add_res(std::make_unique<ResVector>(
"trans-offset", std::vector<math::Vector4f>{trans_offset}, DEFAULT_RES_TIME));
}
if (joint_channel != -1) {
lump.add_res(std::make_unique<ResInt32>("joint-channel", std::vector<s32>{joint_channel},
DEFAULT_RES_TIME));
}
if (!lod_dist.empty()) {
lump.add_res(std::make_unique<ResFloat>("lod-dist", lod_dist, DEFAULT_RES_TIME));
}
lump.sort_res();
}
@@ -569,159 +557,12 @@ int ArtGroup::get_joint_idx(const std::string& name) {
return -1;
}
/*!
* Load tinygltf::Model from a .glb file (binary format), fatal error if it fails.
*/
tinygltf::Model load_gltf_model(const fs::path& path) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool res = loader.LoadBinaryFromFile(&model, &err, &warn, path.string());
ASSERT_MSG(warn.empty(), warn.c_str());
ASSERT_MSG(err.empty(), err.c_str());
ASSERT_MSG(res, "Failed to load GLTF file!");
return model;
}
struct GltfJoint {
math::Matrix4f bind_pose_T_w; // inverse bind pose
std::string name;
int gltf_node_index = 0;
int parent = -1;
std::vector<int> children;
};
/*!
* Extract the "skeleton" structure from a GLTF model's skin. This requires that the skin's joints
* are topologically sorted (parents always have lower index than children).
*/
std::vector<GltfJoint> extract_skeleton(const tinygltf::Model& model, int skin_idx) {
const auto& skin = model.skins.at(skin_idx);
lg::info("skin name is {}", skin.name);
lg::info("skeleton root is {}", skin.skeleton);
auto inverse_bind_matrices = extract_mat4(model, skin.inverseBindMatrices);
ASSERT(inverse_bind_matrices.size() == skin.joints.size());
std::map<int, int> node_to_joint;
std::map<int, int> joint_to_node;
std::vector<GltfJoint> joints;
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// auto ibm = inverse_bind_matrices[i];
// lg::info(" joint {}", joint_node_idx);
// lg::info(" {}", joint_node.name);
// lg::info("\n{}", ibm.to_string_aligned());
node_to_joint[joint_node_idx] = i;
joint_to_node[i] = joint_node_idx;
auto& gjoint = joints.emplace_back();
gjoint.bind_pose_T_w = inverse_bind_matrices[i];
gjoint.name = joint_node.name;
gjoint.gltf_node_index = joint_node_idx;
}
for (size_t i = 0; i < skin.joints.size(); i++) {
auto joint_node_idx = skin.joints[i];
const auto& joint_node = model.nodes.at(joint_node_idx);
// set up children
for (int child_node_idx : joint_node.children) {
int child_joint_idx = node_to_joint.at(child_node_idx);
joints.at(i).children.push_back(child_joint_idx);
auto& child = joints.at(child_joint_idx);
ASSERT(child.parent == -1);
child.parent = i;
ASSERT(child_joint_idx > (int)i);
}
}
ASSERT(joints.at(0).parent == -1);
// for (auto& joint : joints) {
// if (joint.parent == -1) {
// lg::warn("parentless {}", joint.name);
// } else {
// lg::info("joint {}, child of {}", joint.name, joints.at(joint.parent).name);
// }
// }
lg::info("total of {} joints", joints.size());
return joints;
}
/*!
* Convert from GLTF joint format to game joint format.
* @param joint_index the index of the joint, in the GLTF file.
* @param prefix_joint_count number of joints to be inserted before GLTF joints in the game
* @param parent_of_gltf the parent game joint of all GLTF joints.
*/
Joint convert_joint(const GltfJoint& joint,
int joint_index,
int prefix_joint_count,
int parent_of_gltf) {
// node matrix is p_T_myself
// p_T_myself = parent_bind_pose_T_w * w_T_bind_pose
int parent;
if (joint.parent == -1) {
parent = parent_of_gltf;
} else {
parent = joint.parent + prefix_joint_count;
}
math::Matrix4f fixed_matrix = joint.bind_pose_T_w;
for (int i = 0; i < 3; i++) {
fixed_matrix(i, 3) *= 4096;
}
return Joint(joint.name, joint_index + prefix_joint_count, parent, fixed_matrix.transposed());
}
constexpr int kGltfToGameJointOffset = 1;
/*!
* Convert GTLF joint list to game joint list.
* Currently, this inserts a single "align" joint and places the root joint of the GLTF as the
* prejoint. However, we might want to change this, to allow GLTF files to specify "align" at some
* point.
*/
std::vector<Joint> convert_joints(const std::vector<GltfJoint>& gjoints) {
std::vector<Joint> joints;
joints.emplace_back("align", 0, -1, math::Matrix4f::identity());
ASSERT(kGltfToGameJointOffset == joints.size());
for (int gjoint_idx = 0; gjoint_idx < int(gjoints.size()); gjoint_idx++) {
// using -1 as the parent index since gltf's shouldn't be child of align.
joints.push_back(convert_joint(gjoints[gjoint_idx], gjoint_idx, kGltfToGameJointOffset, -1));
}
return joints;
}
std::vector<anim::CompressedAnim> process_anim(const tinygltf::Model& model,
const std::vector<GltfJoint>& gjoints) {
if (model.animations.empty()) {
lg::warn("no animations detected!"); // TODO: make up a dummy one
return {};
}
std::map<int, int> node_to_joint;
for (size_t i = 0; i < gjoints.size(); i++) {
node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset;
}
std::vector<anim::CompressedAnim> ret;
for (auto& anim : model.animations) {
lg::info("Processing animation {}", anim.name);
ret.push_back(
anim::compress_animation(anim::extract_anim_from_gltf(model, anim, node_to_joint, 60)));
}
return ret;
}
/*!
* Build GOAL format data for an actor. This doesn't generate the data for the .FR3.
*/
bool run_build_actor(const std::string& mdl_name,
const std::string& ag_out,
bool gen_collide_mesh) {
const BuildActorParams& params) {
std::string ag_name;
if (fs::exists(file_util::get_jak_project_dir() / mdl_name)) {
ag_name = fs::path(mdl_name).stem().string();
@@ -766,11 +607,11 @@ bool run_build_actor(const std::string& mdl_name,
}
std::vector<CollideMesh> mesh;
if (gen_collide_mesh) {
mesh = gen_collide_mesh_from_model(model, all_nodes, 3);
if (params.gen_collide_mesh) {
mesh = gen_collide_mesh_from_model_jak1(model, all_nodes, 3);
}
std::shared_ptr<ArtJointGeo> jgeo = std::make_shared<ArtJointGeo>(ag.name, mesh, joints);
std::shared_ptr<ArtJointGeo> jgeo = std::make_shared<ArtJointGeo>(ag.name, mesh, joints, params);
ag.elts.emplace_back(jgeo);
// dummy merc-ctrl
+23 -18
View File
@@ -4,6 +4,7 @@
#include "goalc/build_actor/common/animation_processing.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_actor/common/build_actor.h"
#include "goalc/build_level/collide/common/collide_common.h"
namespace jak1 {
@@ -13,22 +14,6 @@ namespace jak1 {
// the "compressed" format, which combines data for all joints into a single structure.
// By jak 2, they had cleaned this up, but for Jak 1, we have to deal with this weirdness.
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
// This is one of those weird leftover types.
// There's one of these per-joint, per-animation, and all it's useful for is storing the
@@ -153,21 +138,41 @@ struct CollideMesh {
}
};
struct BuildActorParams {
bool gen_collide_mesh = false;
s8 texture_bucket = -1;
s32 texture_level = -1;
s32 joint_channel = -1;
math::Vector4f trans_offset = {0.f, 0.f, 0.f, 1.f};
std::vector<float> lod_dist{};
};
struct ArtJointGeo : ArtElement {
std::vector<Joint> data;
std::vector<CollideMesh> cmeshes;
ResLump lump;
size_t mesh_slot;
s8 texture_bucket;
s32 texture_level;
s32 joint_channel;
math::Vector4f trans_offset;
std::vector<float> lod_dist;
explicit ArtJointGeo(const std::string& name,
std::vector<CollideMesh> cmeshes,
std::vector<Joint>& joints) {
std::vector<Joint>& joints,
const BuildActorParams& params) {
this->name = name + "-lod0";
length = joints.size();
for (auto& joint : joints) {
data.push_back(joint);
}
this->cmeshes = std::move(cmeshes);
this->texture_bucket = params.texture_bucket;
this->texture_level = params.texture_level;
this->joint_channel = params.joint_channel;
this->trans_offset = params.trans_offset;
this->lod_dist = params.lod_dist;
}
void add_res();
size_t generate(DataObjectGenerator& gen);
@@ -225,5 +230,5 @@ struct ArtGroup : Art {
bool run_build_actor(const std::string& input_model,
const std::string& output_file,
bool gen_collide_mesh);
const BuildActorParams& params);
} // namespace jak1
+632
View File
@@ -0,0 +1,632 @@
#include "build_actor.h"
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "goalc/build_actor/common/MercExtract.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
namespace jak2 {
JointAnimCompressedHDR::JointAnimCompressedHDR(const anim::CompressedAnim& anim) {
matrix_bits = 0;
if (anim.matrix_animated[0]) {
matrix_bits |= 1;
}
if (anim.matrix_animated[1]) {
matrix_bits |= 2;
}
for (auto& word : control_bits) {
word = 0;
}
for (size_t i = 0; i < anim.joint_metadata.size(); i++) {
const int word_idx = i / 8;
if (word_idx >= 14) {
lg::error("too many joints!!");
break;
}
u32 val = 0;
const auto& metadata = anim.joint_metadata[i];
if (metadata.animated_trans) {
val |= (0b1);
}
if (metadata.animated_quat) {
val |= (0b10);
}
if (metadata.animated_scale) {
val |= (0b100);
}
if (metadata.big_trans_mode) {
val |= (0b1000);
}
val = (val << (4 * (i % 8)));
control_bits[word_idx] |= val;
}
num_joints = 2 + anim.joint_metadata.size();
}
JointAnimCompressedFixed::JointAnimCompressedFixed(const anim::CompressedAnim& anim) : hdr(anim) {
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)anim.fixed.data64.data();
const u8* u32_src = (const u8*)anim.fixed.data32.data();
const u8* u16_src = (const u8*)anim.fixed.data16.data();
const int u64_size = anim.fixed.data64.size() * sizeof(u64);
const int u32_size = anim.fixed.data32.size() * sizeof(u32);
const int u16_size = anim.fixed.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("fixed sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
ASSERT(u64_size + u32_size + u16_size <= 133 * 4 * 4);
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedFrame::JointAnimCompressedFrame(const anim::CompressedFrame& frame) {
reserved = 0;
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)frame.data64.data();
const u8* u32_src = (const u8*)frame.data32.data();
const u8* u16_src = (const u8*)frame.data16.data();
const int u64_size = frame.data64.size() * sizeof(u64);
const int u32_size = frame.data32.size() * sizeof(u32);
const int u16_size = frame.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("frame sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedControl::JointAnimCompressedControl(const anim::CompressedAnim& anim)
: fixed(anim) {
num_frames = anim.frames.size();
for (auto& in_frame : anim.frames) {
frame.emplace_back(in_frame);
}
fixed_qwc = fixed.num_data_qw_used;
frame_qwc = frame.at(0).num_data_qw_used;
}
ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints)
: frames(anim) {
this->name = anim.name;
length = joints.size();
speed = 1.0f;
artist_base = 0.0f;
artist_step = 1.0f;
master_art_group_name = name;
master_art_group_index = 2;
}
size_t JointAnimCompressedFrame::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
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
for (u32 i = 0; i < num_data_qw_used; i++) {
for (int j = 0; j < 4; j++) {
gen.add_word_float(data[i][j]);
}
}
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);
return result;
}
size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
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 (int i = 0; i < num_data_qw_used; 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());
}
// -- not sure what this is, part of the dummy data maybe?
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 {
gen.align(16);
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);
std::vector<int> ja_frame_slots;
for (u32 i = 0; i < num_frames; i++) {
ja_frame_slots.push_back(gen.add_word(0));
}
gen.link_word_to_byte(ja_fixed_slot, fixed.generate(gen));
ASSERT(num_frames == frame.size());
for (size_t i = 0; i < num_frames; i++) {
gen.link_word_to_byte(ja_frame_slots[i], frame[i].generate(gen));
}
return result;
}
size_t CollideMesh::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("collide-mesh");
size_t result = gen.current_offset_bytes();
// gen.add_word(0xffffffff - joint_id); // 4 (joint-id)
gen.add_word(joint_id); // 4 (joint-id)
gen.add_word(num_tris); // 8 (num-tris)
gen.add_word(num_verts); // 12 (num-verts)
auto vertices_slot = gen.add_word(0); // 16 (vertex-data)
gen.add_word(0); // 20 (pad)
gen.add_word(0); // 24 (pad)
gen.add_word(0); // 28 (pad)
for (auto& tri : tris) {
u32 word = (tri.vert_idx[0] & 0xff) + ((tri.vert_idx[1] << 8) & 0xff00) +
((tri.vert_idx[2] << 16) & 0xff0000);
gen.add_word(word); // 0 (tris | vertex-index | unused)
gen.add_word(tri.pat.val); // 4 (pat)
}
// vertex data start
gen.link_word_to_byte(vertices_slot, gen.current_offset_bytes());
for (auto& vert : vertices) {
gen.add_word_float(vert.x());
gen.add_word_float(vert.y());
gen.add_word_float(vert.z());
gen.add_word_float(vert.w());
}
return result;
};
void ArtJointGeo::add_res() {
if (!cmeshes.empty()) {
lump.add_res(
std::make_unique<ResRef>("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME));
}
if (texture_bucket != -1) {
lump.add_res(std::make_unique<ResUint8>(
"texture-bucket", std::vector<u8>{static_cast<u8>(texture_bucket)}, DEFAULT_RES_TIME));
}
if (texture_level != -1) {
lump.add_res(std::make_unique<ResInt32>("texture-level", std::vector<s32>{texture_level},
DEFAULT_RES_TIME));
}
if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) {
lump.add_res(std::make_unique<ResVector>(
"trans-offset", std::vector<math::Vector4f>{trans_offset}, DEFAULT_RES_TIME));
}
if (joint_channel != -1) {
lump.add_res(std::make_unique<ResInt32>("joint-channel", std::vector<s32>{joint_channel},
DEFAULT_RES_TIME));
}
if (!lod_dist.empty()) {
lump.add_res(std::make_unique<ResFloat>("lod-dist", lod_dist, DEFAULT_RES_TIME));
}
lump.sort_res();
}
size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const {
std::vector<size_t> data_slots;
std::vector<size_t> content_slots;
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
gen.add_word(cmeshes.size()); // 0 (length)
gen.add_word(cmeshes.size()); // 4 (allocated-length)
gen.add_type_tag("collide-mesh"); // 8 (content-type)
content_slots.reserve(cmeshes.size());
for (auto& data : cmeshes) {
(void)data;
content_slots.push_back(gen.add_word(0)); // 12 (data)
}
gen.align(4);
for (size_t i = 0; i < content_slots.size(); i++) {
data_slots.push_back(cmeshes.at(i).generate(gen));
}
for (size_t i = 0; i < content_slots.size(); i++) {
gen.link_word_to_byte(content_slots.at(i), data_slots.at(i));
}
return result;
}
size_t ArtJointGeo::generate(DataObjectGenerator& gen) {
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 (int i = 0; i < length; i++) {
joint_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (int 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;
}
mesh_slot = generate_mesh(gen);
add_res();
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 (int 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));
return result;
}
static size_t gen_dummy_frag_geo(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
// frag geo stolen from money-lod2
static std::vector<u32> words = {
0x51180504, 0x1000013, 0x1b150000, 0x604, 0x5225, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0xb4e48086, 0x757d3a0a, 0x1e324000, 0x1e328186,
0x757d3410, 0x1c4020, 0x432e8186, 0x251313, 0x71a28044, 0x1c8186, 0x757d2e16,
0x96ce4040, 0x719b8186, 0x151919, 0x71c08064, 0x96ce8186, 0x757d281c, 0xb4e44060,
0x71d28186, 0x251f1f, 0x435e8080, 0xb4e48186, 0x757d4322, 0x1e324080, 0x71cc7f86,
0xffdb4025, 0x71ad0064, 0x435c8186, 0xffea582b, 0x71bc0044, 0x43348186, 0xffdb5b31,
0x43530024, 0x71a48086, 0xffea3755, 0x43440000, 0x71a47f86, 0xffea3d3d, 0x43440080,
0x432e7f86, 0x254646, 0x71a20044, 0x43657f86, 0x154949, 0x43400024, 0x719b8186,
0x154c4c, 0x71c00064, 0x71d28186, 0x254f4f, 0x435e0000, 0x71d27f86, 0x250707,
0x435e8000, 0x43658186, 0x150d0d, 0x43408024, 0x0, 0x0, 0x0,
0xcb01005a, 0xcb00fffa, 0xcb01005a, 0x2101e01, 0x0, 0x0, 0x306,
0x4030000, 0x120, 0x0, 0x1cf02c14, 0x66c801d, 0x0, 0x0,
0x34, 0x0, 0x0, 0x0, 0x8, 0x0, 0x44,
0x80, 0x42, 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(0x1063918);
gen.add_word(0x603);
gen.add_word(0x0);
gen.add_word(0x0);
return result;
}
size_t gen_dummy_frag_ctrl_for_uploads(DataObjectGenerator& gen, int n) {
size_t result = gen.current_offset_bytes();
std::vector<u8> packed_frag_ctrls;
// this is still a bit of a hack - the dummy_frag_ctrl above has 4 fragments, so we need to
// provide fragment controls for each. The PC merc renderer will de-duplicate bone uploads over
// all effects and fragments, so we just need to have a single fragment that asks for all bones,
// and everything will work out.
for (int k = 0; k < 4; k++) {
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
if (k == 0) { // for the first frag, do all matrix uploads.
// note that these are bogus destination addresses, but nothing uses it on PC
packed_frag_ctrls.push_back(n);
for (int i = 0; i < n; i++) {
packed_frag_ctrls.push_back(i);
packed_frag_ctrls.push_back(i);
}
} else {
// remaining frags can have empty matrix upload lists.
packed_frag_ctrls.push_back(0);
}
}
std::vector<u32> u32s((packed_frag_ctrls.size() + 3) / 4);
memcpy(u32s.data(), packed_frag_ctrls.data(), packed_frag_ctrls.size());
for (auto x : u32s) {
gen.add_word(x);
}
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;
}
void generate_merc_effects(DataObjectGenerator& gen, int effect_count, int joints) {
struct EffectLocs {
size_t frag_geo;
size_t frag_ctrl;
size_t extra_info;
};
std::vector<EffectLocs> locs;
for (int i = 0; i < effect_count; i++) {
EffectLocs loc{};
loc.frag_geo = gen.add_word(0); // 112-140 (effect)
loc.frag_ctrl = gen.add_word(0); // 116 (frag-ctrl)
gen.add_word(0x0); // 120 (blend-data)
gen.add_word(0x0); // 124 (blend-ctrl)
gen.add_word(0x10000); // 128
gen.add_word(0x140000); // 132
gen.add_word(0x100001d); // 136
loc.extra_info = gen.add_word(0); // 140 (extra-info)
locs.push_back(loc);
}
for (auto& loc : locs) {
gen.link_word_to_byte(loc.extra_info, gen_dummy_extra_info(gen));
gen.link_word_to_byte(loc.frag_ctrl, gen_dummy_frag_ctrl_for_uploads(gen, joints + 3));
gen.link_word_to_byte(loc.frag_geo, gen_dummy_frag_geo(gen));
}
}
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).get())->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 (seg-table)
gen.add_word(0x0); // 28 (pad)
gen.add_word(0x4185c8ac); // 32-160 (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(ag.merc_effect_count); // 56 (effect-count)
gen.add_word(0x0); // 60 (blend-target-count)
gen.add_word((0x14 * ag.merc_effect_count << 16) +
ag.merc_effect_count); // 64 ((fragment-count << 16) + tri-count)
gen.add_word(0x130101); // 68
gen.add_word(0x13001d); // 72
gen.add_word(0x0); // 76
gen.add_word(0x0); // 80
gen.add_word(0x10101); // 84
gen.add_word(0x130000); // 88
gen.add_word(0x3f319ca9); // 92
gen.add_word(0x0); // 96
gen.add_word(0x0); // 100
gen.add_word(0x0); // 104
gen.add_word(0x0); // 108
gen.add_word(0x0); // 112
gen.add_word(0x0); // 116
gen.add_word(0x0); // 120
gen.add_word(0x0); // 124
gen.add_word(0x0); // 128
gen.add_word(0x0); // 132
gen.add_word(0x0); // 136
gen.add_word(0x0); // 140
gen.add_word(0x0); // 144
gen.add_word(0x0); // 148
gen.add_word(0x0); // 152
gen.add_word(0x0); // 156
generate_merc_effects(gen, ag.merc_effect_count, joints);
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).get();
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));
}
for (size_t i = 2; i < elts.size(); i++) {
auto ja = (ArtJointAnim*)elts.at(i).get();
gen.link_word_to_byte((32 + i * 4) / 4, ja->generate(gen));
}
}
return gen.generate_v4();
}
int ArtGroup::get_joint_idx(const std::string& name) {
for (auto& elt : this->elts) {
if (elt && typeid(*elt) == typeid(ArtJointGeo)) {
auto jgeo = (ArtJointGeo*)elt.get();
for (auto& joint : jgeo->data) {
if (joint.name == name) {
return joint.number;
}
}
}
}
return -1;
}
/*!
* Build GOAL format data for an actor. This doesn't generate the data for the .FR3.
*/
bool run_build_actor(const std::string& mdl_name,
const std::string& ag_out,
const BuildActorParams& params) {
std::string ag_name;
if (fs::exists(file_util::get_jak_project_dir() / mdl_name)) {
ag_name = fs::path(mdl_name).stem().string();
} else {
ASSERT_MSG(false, "Model file not found: " + mdl_name);
}
// Load GLTF file to check for a skeleton
tinygltf::Model model = load_gltf_model(file_util::get_jak_project_dir() / mdl_name);
auto all_nodes = flatten_nodes_from_all_scenes(model);
auto skin_idx = find_single_skin(model, all_nodes);
if (skin_idx) {
lg::info("GLTF file contained a skin, this actor will have a real skeleton");
}
std::vector<anim::CompressedAnim> user_anims;
ArtGroup ag(ag_name);
std::vector<Joint> joints;
MercExtractData extract_data;
extract("test", extract_data, model, all_nodes, 0, 0, 0);
ag.merc_effect_count = extract_data.new_model.effects.size();
// MercSwapData out;
// merc_convert(out, extract_data);
// Set up joints:
if (skin_idx) {
// read skeleton data out of GLTF
auto skeleton_joints = extract_skeleton(model, *skin_idx);
// convert to game format
joints = convert_joints(skeleton_joints);
// get animation from user.
user_anims = process_anim(model, skeleton_joints);
} else {
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::identity();
main_pose(3, 1) = -2194.1628418f;
Joint main("main", 2, 1, main_pose);
joints.emplace_back(main);
}
std::vector<CollideMesh> mesh;
if (params.gen_collide_mesh) {
mesh = gen_collide_mesh_from_model_jak2(model, all_nodes, 3);
}
std::shared_ptr<ArtJointGeo> jgeo = std::make_shared<ArtJointGeo>(ag.name, mesh, joints, params);
ag.elts.emplace_back(jgeo);
// dummy merc-ctrl
ag.elts.emplace_back(nullptr);
if (!user_anims.empty()) {
for (auto& anim : user_anims) {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(anim, joints));
}
} else {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(ag.name, joints));
}
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 jak2
+215
View File
@@ -0,0 +1,215 @@
#pragma once
#include "common/util/gltf_util.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_actor/common/build_actor.h"
#include "goalc/build_level/collide/common/collide_common.h"
namespace jak2 {
// basic
// Header for a compressed joint animation - this tells the decompressor how to read
// the data in the animation.
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;
}
explicit JointAnimCompressedHDR(const anim::CompressedAnim& anim);
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];
int num_data_qw_used = 0;
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);
num_data_qw_used = 8;
}
JointAnimCompressedFixed(const anim::CompressedAnim& anim);
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedFrame {
u32 offset_64;
u32 offset_32;
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
u32 num_data_qw_used = 0;
JointAnimCompressedFrame() {
offset_64 = 0;
offset_32 = 0;
offset_16 = 0;
reserved = 0;
}
JointAnimCompressedFrame(const anim::CompressedFrame& frame);
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedControl {
u32 num_frames;
u32 fixed_qwc;
u32 frame_qwc;
JointAnimCompressedFixed fixed;
std::vector<JointAnimCompressedFrame> frame;
JointAnimCompressedControl() {
num_frames = 1;
fixed_qwc = 0xf;
frame_qwc = 1;
frame.emplace_back();
}
JointAnimCompressedControl(const anim::CompressedAnim& anim);
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;
std::vector<CollideMeshTri> tris;
size_t generate(DataObjectGenerator& gen) const;
size_t calc_data_size() const {
// (size-of collide-mesh) + type ptr = 36
return 36 + 16 * vertices.size() + sizeof(CollideMeshTri) * tris.size();
}
};
struct BuildActorParams {
bool gen_collide_mesh = false;
s8 texture_bucket = -1;
s32 texture_level = -1;
s32 joint_channel = -1;
math::Vector4f trans_offset = {0.f, 0.f, 0.f, 1.f};
std::vector<float> lod_dist{};
};
struct ArtJointGeo : ArtElement {
std::vector<Joint> data;
std::vector<CollideMesh> cmeshes;
ResLump lump;
size_t mesh_slot;
s8 texture_bucket;
s32 texture_level;
s32 joint_channel;
math::Vector4f trans_offset;
std::vector<float> lod_dist;
explicit ArtJointGeo(const std::string& name,
std::vector<CollideMesh> cmeshes,
std::vector<Joint>& joints,
const BuildActorParams& params) {
this->name = name + "-lod0";
length = joints.size();
for (auto& joint : joints) {
data.push_back(joint);
}
this->cmeshes = std::move(cmeshes);
this->texture_bucket = params.texture_bucket;
this->texture_level = params.texture_level;
this->joint_channel = params.joint_channel;
this->trans_offset = params.trans_offset;
this->lod_dist = params.lod_dist;
}
void add_res();
size_t generate(DataObjectGenerator& gen);
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;
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;
// for (auto& joint : joints) {
// data.emplace_back(joint, 1);
// }
}
ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints);
size_t generate(DataObjectGenerator& gen) const;
};
struct ArtGroup : Art {
FileInfo info;
std::vector<std::shared_ptr<ArtElement>> elts;
std::map<int, size_t> joint_map;
int merc_effect_count;
explicit ArtGroup(const std::string& file_name) {
info.file_type = "art-group";
info.file_name = "/src/next/data/art-group7/" + file_name + "-ag.go";
name = file_name;
info.major_version = versions::jak2::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;
int get_joint_idx(const std::string& name);
};
bool run_build_actor(const std::string& input_model,
const std::string& output_file,
const BuildActorParams& params);
} // namespace jak2
+644
View File
@@ -0,0 +1,644 @@
#include "build_actor.h"
#include "common/log/log.h"
#include "common/math/geometry.h"
#include "goalc/build_actor/common/MercExtract.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "third-party/tiny_gltf/tiny_gltf.h"
using namespace gltf_util;
namespace jak3 {
JointAnimCompressedHDR::JointAnimCompressedHDR(const anim::CompressedAnim& anim) {
matrix_bits = 0;
if (anim.matrix_animated[0]) {
matrix_bits |= 1;
}
if (anim.matrix_animated[1]) {
matrix_bits |= 2;
}
for (auto& word : control_bits) {
word = 0;
}
for (size_t i = 0; i < anim.joint_metadata.size(); i++) {
const int word_idx = i / 8;
if (word_idx >= 14) {
lg::error("too many joints!!");
break;
}
u32 val = 0;
const auto& metadata = anim.joint_metadata[i];
if (metadata.animated_trans) {
val |= (0b1);
}
if (metadata.animated_quat) {
val |= (0b10);
}
if (metadata.animated_scale) {
val |= (0b100);
}
if (metadata.big_trans_mode) {
val |= (0b1000);
}
val = (val << (4 * (i % 8)));
control_bits[word_idx] |= val;
}
num_joints = 2 + anim.joint_metadata.size();
}
JointAnimCompressedFixed::JointAnimCompressedFixed(const anim::CompressedAnim& anim) : hdr(anim) {
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)anim.fixed.data64.data();
const u8* u32_src = (const u8*)anim.fixed.data32.data();
const u8* u16_src = (const u8*)anim.fixed.data16.data();
const int u64_size = anim.fixed.data64.size() * sizeof(u64);
const int u32_size = anim.fixed.data32.size() * sizeof(u32);
const int u16_size = anim.fixed.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("fixed sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
ASSERT(u64_size + u32_size + u16_size <= 133 * 4 * 4);
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedFrame::JointAnimCompressedFrame(const anim::CompressedFrame& frame) {
reserved = 0;
u8* dest = (u8*)data;
const u8* u64_src = (const u8*)frame.data64.data();
const u8* u32_src = (const u8*)frame.data32.data();
const u8* u16_src = (const u8*)frame.data16.data();
const int u64_size = frame.data64.size() * sizeof(u64);
const int u32_size = frame.data32.size() * sizeof(u32);
const int u16_size = frame.data16.size() * sizeof(u16);
if (u64_size + u32_size + u16_size > 133 * 4 * 4) {
lg::die("frame sizes are {} and {}", 133 * 4 * 4, u64_size + u32_size + u16_size);
}
offset_64 = 0;
memcpy(dest, u64_src, u64_size);
dest += u64_size;
offset_32 = offset_64 + u64_size;
memcpy(dest, u32_src, u32_size);
dest += u32_size;
offset_16 = offset_32 + u32_size;
memcpy(dest, u16_src, u16_size);
reserved = 0;
num_data_qw_used = (15 + u64_size + u32_size + u16_size) / 16;
ASSERT(num_data_qw_used <= 133);
}
JointAnimCompressedControl::JointAnimCompressedControl(const anim::CompressedAnim& anim)
: fixed(anim) {
num_frames = anim.frames.size();
for (auto& in_frame : anim.frames) {
frame.emplace_back(in_frame);
}
fixed_qwc = fixed.num_data_qw_used;
frame_qwc = frame.at(0).num_data_qw_used;
}
ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints)
: frames(anim) {
this->name = anim.name;
length = joints.size();
speed = 1.0f;
artist_base = 0.0f;
artist_step = 1.0f;
master_art_group_name = name;
master_art_group_index = 2;
}
size_t JointAnimCompressedFrame::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
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
for (u32 i = 0; i < num_data_qw_used; i++) {
for (int j = 0; j < 4; j++) {
gen.add_word_float(data[i][j]);
}
}
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);
return result;
}
size_t JointAnimCompressedFixed::generate(DataObjectGenerator& gen) const {
gen.align(16); // might have been 8, but 16 doesn't hurt.
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 (int i = 0; i < num_data_qw_used; 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());
}
// -- not sure what this is, part of the dummy data maybe?
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 {
gen.align(16);
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);
std::vector<int> ja_frame_slots;
for (u32 i = 0; i < num_frames; i++) {
ja_frame_slots.push_back(gen.add_word(0));
}
gen.link_word_to_byte(ja_fixed_slot, fixed.generate(gen));
ASSERT(num_frames == frame.size());
for (size_t i = 0; i < num_frames; i++) {
gen.link_word_to_byte(ja_frame_slots[i], frame[i].generate(gen));
}
return result;
}
size_t CollideMesh::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("collide-mesh");
size_t result = gen.current_offset_bytes();
// gen.add_word(0xffffffff - joint_id); // 4 (joint-id)
gen.add_word(joint_id); // 4 (joint-id)
gen.add_word(num_tris); // 8 (num-tris)
gen.add_word(num_verts); // 12 (num-verts)
auto vertices_slot = gen.add_word(0); // 16 (vertex-data)
gen.add_word(0); // 20 (pad)
gen.add_word(0); // 24 (pad)
gen.add_word(0); // 28 (pad)
for (auto& tri : tris) {
u32 word = (tri.vert_idx[0] & 0xff) + ((tri.vert_idx[1] << 8) & 0xff00) +
((tri.vert_idx[2] << 16) & 0xff0000);
gen.add_word(word); // 0 (tris | vertex-index | unused)
gen.add_word(tri.pat.val); // 4 (pat)
}
// vertex data start
gen.link_word_to_byte(vertices_slot, gen.current_offset_bytes());
for (auto& vert : vertices) {
gen.add_word_float(vert.x());
gen.add_word_float(vert.y());
gen.add_word_float(vert.z());
gen.add_word_float(vert.w());
}
return result;
};
void ArtJointGeo::add_res() {
if (!cmeshes.empty()) {
lump.add_res(
std::make_unique<ResRef>("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME));
}
if (texture_bucket != -1) {
lump.add_res(std::make_unique<ResUint8>(
"texture-bucket", std::vector<u8>{static_cast<u8>(texture_bucket)}, DEFAULT_RES_TIME));
}
if (texture_level != -1) {
lump.add_res(std::make_unique<ResInt32>("texture-level", std::vector<s32>{texture_level},
DEFAULT_RES_TIME));
}
if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) {
lump.add_res(std::make_unique<ResVector>(
"trans-offset", std::vector<math::Vector4f>{trans_offset}, DEFAULT_RES_TIME));
}
if (joint_channel != -1) {
lump.add_res(std::make_unique<ResInt32>("joint-channel", std::vector<s32>{joint_channel},
DEFAULT_RES_TIME));
}
if (!lod_dist.empty()) {
lump.add_res(std::make_unique<ResFloat>("lod-dist", lod_dist, DEFAULT_RES_TIME));
}
lump.sort_res();
}
size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const {
std::vector<size_t> data_slots;
std::vector<size_t> content_slots;
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
gen.add_word(cmeshes.size()); // 0 (length)
gen.add_word(cmeshes.size()); // 4 (allocated-length)
gen.add_type_tag("collide-mesh"); // 8 (content-type)
content_slots.reserve(cmeshes.size());
for (auto& data : cmeshes) {
(void)data;
content_slots.push_back(gen.add_word(0)); // 12 (data)
}
gen.align(4);
for (size_t i = 0; i < content_slots.size(); i++) {
data_slots.push_back(cmeshes.at(i).generate(gen));
}
for (size_t i = 0; i < content_slots.size(); i++) {
gen.link_word_to_byte(content_slots.at(i), data_slots.at(i));
}
return result;
}
size_t ArtJointGeo::generate(DataObjectGenerator& gen) {
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_ref_to_string_in_pool(master_art_group_name); // 20
gen.add_word(master_art_group_index); // 24
gen.add_word(0); // 28
gen.add_word(0); // 32
gen.add_word(0); // 36
gen.add_word(0); // 40
gen.add_word(0); // 44
std::vector<size_t> joint_slots;
for (int i = 0; i < length; i++) {
joint_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (int 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;
}
mesh_slot = generate_mesh(gen);
add_res();
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_word(0); // 4
gen.add_ref_to_string_in_pool(name); // 8
gen.add_word(length); // 12
gen.add_symbol_link("#f"); // 16 (res-lump)
gen.add_ref_to_string_in_pool(master_art_group_name); // 20
gen.add_word(master_art_group_index); // 24
gen.add_symbol_link("#f"); // 28 (eye block)
gen.add_symbol_link("#f"); // 32 (blerc)
auto ctrl_slot = gen.add_word(0); // 36 (frames)
gen.add_word(0); // 40
gen.add_word(0); // 44
gen.add_word_float(speed); // 48
gen.add_word_float(artist_base); // 52
gen.add_word_float(artist_step); // 56
std::vector<size_t> frame_slots;
for (int 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));
return result;
}
static size_t gen_dummy_frag_geo(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
// frag geo stolen from money-lod2
static std::vector<u32> words = {
0x51180504, 0x1000013, 0x1b150000, 0x604, 0x5225, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080, 0x80808080,
0x80808080, 0x80808080, 0x80808080, 0xb4e48086, 0x757d3a0a, 0x1e324000, 0x1e328186,
0x757d3410, 0x1c4020, 0x432e8186, 0x251313, 0x71a28044, 0x1c8186, 0x757d2e16,
0x96ce4040, 0x719b8186, 0x151919, 0x71c08064, 0x96ce8186, 0x757d281c, 0xb4e44060,
0x71d28186, 0x251f1f, 0x435e8080, 0xb4e48186, 0x757d4322, 0x1e324080, 0x71cc7f86,
0xffdb4025, 0x71ad0064, 0x435c8186, 0xffea582b, 0x71bc0044, 0x43348186, 0xffdb5b31,
0x43530024, 0x71a48086, 0xffea3755, 0x43440000, 0x71a47f86, 0xffea3d3d, 0x43440080,
0x432e7f86, 0x254646, 0x71a20044, 0x43657f86, 0x154949, 0x43400024, 0x719b8186,
0x154c4c, 0x71c00064, 0x71d28186, 0x254f4f, 0x435e0000, 0x71d27f86, 0x250707,
0x435e8000, 0x43658186, 0x150d0d, 0x43408024, 0x0, 0x0, 0x0,
0xcb01005a, 0xcb00fffa, 0xcb01005a, 0x2101e01, 0x0, 0x0, 0x306,
0x4030000, 0x120, 0x0, 0x1cf02c14, 0x66c801d, 0x0, 0x0,
0x34, 0x0, 0x0, 0x0, 0x8, 0x0, 0x44,
0x80, 0x42, 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(0x1063918);
gen.add_word(0x603);
gen.add_word(0x0);
gen.add_word(0x0);
return result;
}
size_t gen_dummy_frag_ctrl_for_uploads(DataObjectGenerator& gen, int n) {
size_t result = gen.current_offset_bytes();
std::vector<u8> packed_frag_ctrls;
// this is still a bit of a hack - the dummy_frag_ctrl above has 4 fragments, so we need to
// provide fragment controls for each. The PC merc renderer will de-duplicate bone uploads over
// all effects and fragments, so we just need to have a single fragment that asks for all bones,
// and everything will work out.
for (int k = 0; k < 4; k++) {
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
packed_frag_ctrls.push_back(0);
if (k == 0) { // for the first frag, do all matrix uploads.
// note that these are bogus destination addresses, but nothing uses it on PC
packed_frag_ctrls.push_back(n);
for (int i = 0; i < n; i++) {
packed_frag_ctrls.push_back(i);
packed_frag_ctrls.push_back(i);
}
} else {
// remaining frags can have empty matrix upload lists.
packed_frag_ctrls.push_back(0);
}
}
std::vector<u32> u32s((packed_frag_ctrls.size() + 3) / 4);
memcpy(u32s.data(), packed_frag_ctrls.data(), packed_frag_ctrls.size());
for (auto x : u32s) {
gen.add_word(x);
}
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;
}
void generate_merc_effects(DataObjectGenerator& gen, int effect_count, int joints) {
struct EffectLocs {
size_t frag_geo;
size_t frag_ctrl;
size_t extra_info;
};
std::vector<EffectLocs> locs;
for (int i = 0; i < effect_count; i++) {
EffectLocs loc{};
loc.frag_geo = gen.add_word(0); // 112-140 (effect)
loc.frag_ctrl = gen.add_word(0); // 116 (frag-ctrl)
gen.add_word(0x0); // 120 (blend-data)
gen.add_word(0x0); // 124 (blend-ctrl)
gen.add_word(0x10000); // 128
gen.add_word(0x140000); // 132
gen.add_word(0x100001d); // 136
loc.extra_info = gen.add_word(0); // 140 (extra-info)
locs.push_back(loc);
}
for (auto& loc : locs) {
gen.link_word_to_byte(loc.extra_info, gen_dummy_extra_info(gen));
gen.link_word_to_byte(loc.frag_ctrl, gen_dummy_frag_ctrl_for_uploads(gen, joints + 3));
gen.link_word_to_byte(loc.frag_geo, gen_dummy_frag_geo(gen));
}
}
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).get())->length - 2;
gen.add_word(0); // 4
gen.add_ref_to_string_in_pool(ag.name + "-lod0"); // 8
gen.add_word(joints); // 12 (num-joints)
gen.add_symbol_link("#f"); // 16 (res-lump)
gen.add_ref_to_string_in_pool(ag.name); // 20 (master-art-group-name)
gen.add_word(1); // 24 (master-art-group-index)
gen.add_word(0x0); // 28 (seg-table)
gen.add_word(0x0); // 32 (pad?)
gen.add_word(0x0); // 36 (pad?)
gen.add_word(0x0); // 40 (pad?)
gen.add_word(0x0); // 44 (pad?)
gen.add_word(0x4185c8ac); // 48-160 (xyz-scale)
gen.add_word(0xc780ff80); // 52 (st-magic)
gen.add_word(0x40798000); // 56 (st-out-a)
gen.add_word(0x40eb4000); // 60 (st-out-b)
gen.add_word(0x4780ff80); // 64 (st-vif-add)
gen.add_word(0x50000); // 68 ((st-int-off << 16) + st-int-scale)
gen.add_word(ag.merc_effect_count); // 72 (effect-count)
gen.add_word(0x0); // 76 (blend-target-count)
gen.add_word((0x14 * ag.merc_effect_count << 16) +
ag.merc_effect_count); // 80 ((fragment-count << 16) + tri-count)
gen.add_word(0x130101); // 84
gen.add_word(0x13001d); // 88
gen.add_word(0x0); // 92
gen.add_word(0x0); // 96
gen.add_word(0x10101); // 100
gen.add_word(0x130000); // 104
gen.add_word(0x3f319ca9); // 108
gen.add_word(0x0); // 112
gen.add_word(0x0); // 116
gen.add_word(0x0); // 120
gen.add_word(0x0); // 124
gen.add_word(0x0); // 128
gen.add_word(0x0); // 132
gen.add_word(0x0); // 136
gen.add_word(0x0); // 140
gen.add_word(0x0); // 144
gen.add_word(0x0); // 148
gen.add_word(0x0); // 152
gen.add_word(0x0); // 156
gen.add_word(0x0); // 160
gen.add_word(0x0); // 164
gen.add_word(0x0); // 168
gen.add_word(0x0); // 172
generate_merc_effects(gen, ag.merc_effect_count, joints);
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).get();
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));
}
for (size_t i = 2; i < elts.size(); i++) {
auto ja = (ArtJointAnim*)elts.at(i).get();
gen.link_word_to_byte((32 + i * 4) / 4, ja->generate(gen));
}
}
return gen.generate_v4();
}
int ArtGroup::get_joint_idx(const std::string& name) {
for (auto& elt : this->elts) {
if (elt && typeid(*elt) == typeid(ArtJointGeo)) {
auto jgeo = (ArtJointGeo*)elt.get();
for (auto& joint : jgeo->data) {
if (joint.name == name) {
return joint.number;
}
}
}
}
return -1;
}
/*!
* Build GOAL format data for an actor. This doesn't generate the data for the .FR3.
*/
bool run_build_actor(const std::string& mdl_name,
const std::string& ag_out,
const BuildActorParams& params) {
std::string ag_name;
if (fs::exists(file_util::get_jak_project_dir() / mdl_name)) {
ag_name = fs::path(mdl_name).stem().string();
} else {
ASSERT_MSG(false, "Model file not found: " + mdl_name);
}
// Load GLTF file to check for a skeleton
tinygltf::Model model = load_gltf_model(file_util::get_jak_project_dir() / mdl_name);
auto all_nodes = flatten_nodes_from_all_scenes(model);
auto skin_idx = find_single_skin(model, all_nodes);
if (skin_idx) {
lg::info("GLTF file contained a skin, this actor will have a real skeleton");
}
std::vector<anim::CompressedAnim> user_anims;
ArtGroup ag(ag_name);
std::vector<Joint> joints;
MercExtractData extract_data;
extract("test", extract_data, model, all_nodes, 0, 0, 0);
ag.merc_effect_count = extract_data.new_model.effects.size();
// MercSwapData out;
// merc_convert(out, extract_data);
// Set up joints:
if (skin_idx) {
// read skeleton data out of GLTF
auto skeleton_joints = extract_skeleton(model, *skin_idx);
// convert to game format
joints = convert_joints(skeleton_joints);
// get animation from user.
user_anims = process_anim(model, skeleton_joints);
} else {
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::identity();
main_pose(3, 1) = -2194.1628418f;
Joint main("main", 2, 1, main_pose);
joints.emplace_back(main);
}
std::vector<CollideMesh> mesh;
if (params.gen_collide_mesh) {
mesh = gen_collide_mesh_from_model_jak3(model, all_nodes, 3);
}
std::shared_ptr<ArtJointGeo> jgeo = std::make_shared<ArtJointGeo>(ag.name, mesh, joints, params);
ag.elts.emplace_back(jgeo);
// dummy merc-ctrl
ag.elts.emplace_back(nullptr);
if (!user_anims.empty()) {
for (auto& anim : user_anims) {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(anim, joints));
}
} else {
ag.elts.emplace_back(std::make_shared<ArtJointAnim>(ag.name, joints));
}
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 jak3
+218
View File
@@ -0,0 +1,218 @@
#pragma once
#include "common/util/gltf_util.h"
#include "goalc/build_actor/common/animation_processing.h"
#include "goalc/build_actor/common/art_types.h"
#include "goalc/build_actor/common/build_actor.h"
#include "goalc/build_level/collide/common/collide_common.h"
namespace jak3 {
struct ArtElement3 : Art {
std::string master_art_group_name;
s32 master_art_group_index;
u8 pad[20];
};
// basic
// Header for a compressed joint animation - this tells the decompressor how to read
// the data in the animation.
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;
}
explicit JointAnimCompressedHDR(const anim::CompressedAnim& anim);
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];
int num_data_qw_used = 0;
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);
num_data_qw_used = 8;
}
JointAnimCompressedFixed(const anim::CompressedAnim& anim);
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedFrame {
u32 offset_64;
u32 offset_32;
u32 offset_16;
u32 reserved;
math::Vector4f data[133];
u32 num_data_qw_used = 0;
JointAnimCompressedFrame() {
offset_64 = 0;
offset_32 = 0;
offset_16 = 0;
reserved = 0;
}
JointAnimCompressedFrame(const anim::CompressedFrame& frame);
size_t generate(DataObjectGenerator& gen) const;
};
struct JointAnimCompressedControl {
u32 num_frames;
u32 fixed_qwc;
u32 frame_qwc;
JointAnimCompressedFixed fixed;
std::vector<JointAnimCompressedFrame> frame;
JointAnimCompressedControl() {
num_frames = 1;
fixed_qwc = 0xf;
frame_qwc = 1;
frame.emplace_back();
}
JointAnimCompressedControl(const anim::CompressedAnim& anim);
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;
std::vector<CollideMeshTri> tris;
size_t generate(DataObjectGenerator& gen) const;
size_t calc_data_size() const {
// (size-of collide-mesh) + type ptr = 36
return 36 + 16 * vertices.size() + sizeof(CollideMeshTri) * tris.size();
}
};
struct BuildActorParams {
bool gen_collide_mesh = false;
s8 texture_bucket = -1;
s32 texture_level = -1;
s32 joint_channel = -1;
math::Vector4f trans_offset = {0.f, 0.f, 0.f, 1.f};
std::vector<float> lod_dist{};
};
struct ArtJointGeo : ArtElement3 {
std::vector<Joint> data;
std::vector<CollideMesh> cmeshes;
ResLump lump;
size_t mesh_slot;
s8 texture_bucket;
s32 texture_level;
s32 joint_channel;
math::Vector4f trans_offset;
std::vector<float> lod_dist;
explicit ArtJointGeo(const std::string& name,
std::vector<CollideMesh> cmeshes,
std::vector<Joint>& joints,
const BuildActorParams& params) {
this->name = name + "-lod0";
master_art_group_name = name;
master_art_group_index = 0;
length = joints.size();
for (auto& joint : joints) {
data.push_back(joint);
}
this->cmeshes = std::move(cmeshes);
this->texture_bucket = params.texture_bucket;
this->texture_level = params.texture_level;
this->joint_channel = params.joint_channel;
this->trans_offset = params.trans_offset;
this->lod_dist = params.lod_dist;
}
void add_res();
size_t generate(DataObjectGenerator& gen);
size_t generate_mesh(DataObjectGenerator& gen) const;
};
struct ArtJointAnim : ArtElement3 {
MercEyeAnimBlock eye_anim_data;
float speed;
float artist_base;
float artist_step;
u8* blerc_data = nullptr;
JointAnimCompressedControl frames;
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;
}
ArtJointAnim(const anim::CompressedAnim& anim, const std::vector<Joint>& joints);
size_t generate(DataObjectGenerator& gen) const;
};
struct ArtGroup : Art {
FileInfo info;
std::vector<std::shared_ptr<ArtElement3>> elts;
std::map<int, size_t> joint_map;
int merc_effect_count;
explicit ArtGroup(const std::string& file_name) {
info.file_type = "art-group";
info.file_name = "/src/next/data/art-group8/" + file_name + "-ag.go";
name = file_name;
info.major_version = versions::jak3::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;
int get_joint_idx(const std::string& name);
};
bool run_build_actor(const std::string& input_model,
const std::string& output_file,
const BuildActorParams& params);
} // namespace jak3
+36 -6
View File
@@ -2,7 +2,9 @@
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "goalc/build_actor/jak1/build_actor.h"
#include "jak1/build_actor.h"
#include "jak2/build_actor.h"
#include "jak3/build_actor.h"
#include "third-party/CLI11.hpp"
@@ -14,8 +16,11 @@ int main(int argc, char** argv) {
// game version
std::string game, mdl_name, output_file;
bool gen_collide_mesh = false;
fs::path project_path_override;
bool gen_collide_mesh = false;
s8 texture_bucket = -1;
s32 texture_level = -1;
s32 joint_channel = -1;
// path
if (!file_util::setup_project_path(std::nullopt)) {
@@ -31,10 +36,14 @@ int main(int argc, char** argv) {
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("-g,--game", game, "Game version (jak1, jak2, jak3)")->required();
app.add_option("--proj-path", project_path_override,
"Specify the location of the 'data/' folder");
app.add_flag("-m,--mesh", gen_collide_mesh, "Whether to generate a collide-mesh for this model");
app.add_option("--texture-bucket", texture_bucket, "Texture bucket to set for this model");
app.add_option("--texture-level", texture_level, "Texture level to set for this model");
app.add_option("--joint-channel", joint_channel,
"Amount of joint channels to set for this model");
app.validate_positionals();
CLI11_PARSE(app, argc, argv)
@@ -54,9 +63,30 @@ int main(int argc, char** argv) {
}
switch (game_version) {
case GameVersion::Jak1:
jak1::run_build_actor(mdl_name, output_file, gen_collide_mesh);
break;
case GameVersion::Jak1: {
jak1::BuildActorParams params;
params.gen_collide_mesh = gen_collide_mesh;
params.texture_bucket = texture_bucket;
params.texture_level = texture_level;
params.joint_channel = joint_channel;
jak1::run_build_actor(mdl_name, output_file, params);
} break;
case GameVersion::Jak2: {
jak2::BuildActorParams params;
params.gen_collide_mesh = gen_collide_mesh;
params.texture_bucket = texture_bucket;
params.texture_level = texture_level;
params.joint_channel = joint_channel;
jak2::run_build_actor(mdl_name, output_file, params);
} break;
case GameVersion::Jak3: {
jak3::BuildActorParams params;
params.gen_collide_mesh = gen_collide_mesh;
params.texture_bucket = texture_bucket;
params.texture_level = texture_level;
params.joint_channel = joint_channel;
jak3::run_build_actor(mdl_name, output_file, params);
} break;
default:
ASSERT_NOT_REACHED_MSG("unsupported game version");
}
@@ -1,6 +1,7 @@
#pragma once
#include "common/common_types.h"
#include "common/math/Vector.h"
#include "common/util/Assert.h"
struct CollideVertex {
float x, y, z;
@@ -530,4 +531,262 @@ struct CollideFace {
math::Vector3f v[3];
PatSurface pat;
};
} // namespace jak3
} // namespace jak3
/*!
* Convert jak1-format PatSurface to Jak 2.
*/
inline jak2::PatSurface jak2_pat(jak1::PatSurface jak1) {
jak2::PatSurface result;
switch (jak1.get_mode()) {
case jak1::PatSurface::Mode::GROUND:
result.set_mode(jak2::PatSurface::Mode::GROUND);
break;
case jak1::PatSurface::Mode::WALL:
result.set_mode(jak2::PatSurface::Mode::WALL);
break;
case jak1::PatSurface::Mode::OBSTACLE:
result.set_mode(jak2::PatSurface::Mode::OBSTACLE);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_material()) {
case jak1::PatSurface::Material::STONE:
result.set_material(jak2::PatSurface::Material::STONE);
break;
case jak1::PatSurface::Material::ICE:
result.set_material(jak2::PatSurface::Material::ICE);
break;
case jak1::PatSurface::Material::QUICKSAND:
result.set_material(jak2::PatSurface::Material::QUICKSAND);
break;
case jak1::PatSurface::Material::WATERBOTTOM:
result.set_material(jak2::PatSurface::Material::WATERBOTTOM);
break;
case jak1::PatSurface::Material::TAR:
result.set_material(jak2::PatSurface::Material::TAR);
break;
case jak1::PatSurface::Material::SAND:
result.set_material(jak2::PatSurface::Material::SAND);
break;
case jak1::PatSurface::Material::WOOD:
result.set_material(jak2::PatSurface::Material::WOOD);
break;
case jak1::PatSurface::Material::GRASS:
result.set_material(jak2::PatSurface::Material::GRASS);
break;
case jak1::PatSurface::Material::PCMETAL:
result.set_material(jak2::PatSurface::Material::PCMETAL);
break;
case jak1::PatSurface::Material::SNOW:
result.set_material(jak2::PatSurface::Material::SNOW);
break;
case jak1::PatSurface::Material::DEEPSNOW:
result.set_material(jak2::PatSurface::Material::DEEPSNOW);
break;
case jak1::PatSurface::Material::HOTCOALS:
result.set_material(jak2::PatSurface::Material::HOTCOALS);
break;
case jak1::PatSurface::Material::LAVA:
result.set_material(jak2::PatSurface::Material::LAVA);
break;
case jak1::PatSurface::Material::CRWOOD:
result.set_material(jak2::PatSurface::Material::CRWOOD);
break;
case jak1::PatSurface::Material::GRAVEL:
result.set_material(jak2::PatSurface::Material::GRAVEL);
break;
case jak1::PatSurface::Material::DIRT:
result.set_material(jak2::PatSurface::Material::DIRT);
break;
case jak1::PatSurface::Material::METAL:
result.set_material(jak2::PatSurface::Material::METAL);
break;
case jak1::PatSurface::Material::STRAW:
result.set_material(jak2::PatSurface::Material::STRAW);
break;
case jak1::PatSurface::Material::TUBE:
result.set_material(jak2::PatSurface::Material::TUBE);
break;
case jak1::PatSurface::Material::SWAMP:
result.set_material(jak2::PatSurface::Material::SWAMP);
break;
case jak1::PatSurface::Material::STOPPROJ:
result.set_material(jak2::PatSurface::Material::STOPPROJ);
break;
case jak1::PatSurface::Material::ROTATE:
result.set_material(jak2::PatSurface::Material::ROTATE);
break;
case jak1::PatSurface::Material::NEUTRAL:
result.set_material(jak2::PatSurface::Material::NEUTRAL);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_event()) {
case jak1::PatSurface::Event::NONE:
result.set_event(jak2::PatSurface::Event::NONE);
break;
case jak1::PatSurface::Event::DEADLY:
result.set_event(jak2::PatSurface::Event::DEADLY);
break;
case jak1::PatSurface::Event::ENDLESSFALL:
result.set_event(jak2::PatSurface::Event::ENDLESSFALL);
break;
case jak1::PatSurface::Event::BURN:
result.set_event(jak2::PatSurface::Event::BURN);
break;
case jak1::PatSurface::Event::DEADLYUP:
result.set_event(jak2::PatSurface::Event::DEADLYUP);
break;
case jak1::PatSurface::Event::BURNUP:
result.set_event(jak2::PatSurface::Event::BURNUP);
break;
case jak1::PatSurface::Event::MELT:
result.set_event(jak2::PatSurface::Event::MELT);
break;
default:
ASSERT_NOT_REACHED();
}
result.set_noentity(jak1.get_noentity());
result.set_nocamera(jak1.get_nocamera());
result.set_noedge(jak1.get_noedge());
result.set_nolineofsight(jak1.get_nolineofsight());
return result;
}
/*!
* Convert jak1-format PatSurface to Jak 3.
*/
inline jak3::PatSurface jak3_pat(jak1::PatSurface jak1) {
jak3::PatSurface result;
switch (jak1.get_mode()) {
case jak1::PatSurface::Mode::GROUND:
result.set_mode(jak3::PatSurface::Mode::GROUND);
break;
case jak1::PatSurface::Mode::WALL:
result.set_mode(jak3::PatSurface::Mode::WALL);
break;
case jak1::PatSurface::Mode::OBSTACLE:
result.set_mode(jak3::PatSurface::Mode::OBSTACLE);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_material()) {
case jak1::PatSurface::Material::STONE:
result.set_material(jak3::PatSurface::Material::STONE);
break;
case jak1::PatSurface::Material::ICE:
result.set_material(jak3::PatSurface::Material::ICE);
break;
case jak1::PatSurface::Material::QUICKSAND:
result.set_material(jak3::PatSurface::Material::QUICKSAND);
break;
case jak1::PatSurface::Material::WATERBOTTOM:
result.set_material(jak3::PatSurface::Material::WATERBOTTOM);
break;
case jak1::PatSurface::Material::TAR:
result.set_material(jak3::PatSurface::Material::TAR);
break;
case jak1::PatSurface::Material::SAND:
result.set_material(jak3::PatSurface::Material::SAND);
break;
case jak1::PatSurface::Material::WOOD:
result.set_material(jak3::PatSurface::Material::WOOD);
break;
case jak1::PatSurface::Material::GRASS:
result.set_material(jak3::PatSurface::Material::GRASS);
break;
case jak1::PatSurface::Material::PCMETAL:
result.set_material(jak3::PatSurface::Material::PCMETAL);
break;
case jak1::PatSurface::Material::SNOW:
result.set_material(jak3::PatSurface::Material::SNOW);
break;
case jak1::PatSurface::Material::DEEPSNOW:
result.set_material(jak3::PatSurface::Material::DEEPSNOW);
break;
case jak1::PatSurface::Material::HOTCOALS:
result.set_material(jak3::PatSurface::Material::HOTCOALS);
break;
case jak1::PatSurface::Material::LAVA:
result.set_material(jak3::PatSurface::Material::LAVA);
break;
case jak1::PatSurface::Material::CRWOOD:
result.set_material(jak3::PatSurface::Material::CRWOOD);
break;
case jak1::PatSurface::Material::GRAVEL:
result.set_material(jak3::PatSurface::Material::GRAVEL);
break;
case jak1::PatSurface::Material::DIRT:
result.set_material(jak3::PatSurface::Material::DIRT);
break;
case jak1::PatSurface::Material::METAL:
result.set_material(jak3::PatSurface::Material::METAL);
break;
case jak1::PatSurface::Material::STRAW:
result.set_material(jak3::PatSurface::Material::STRAW);
break;
case jak1::PatSurface::Material::TUBE:
result.set_material(jak3::PatSurface::Material::TUBE);
break;
case jak1::PatSurface::Material::SWAMP:
result.set_material(jak3::PatSurface::Material::SWAMP);
break;
case jak1::PatSurface::Material::STOPPROJ:
result.set_material(jak3::PatSurface::Material::STOPPROJ);
break;
case jak1::PatSurface::Material::ROTATE:
result.set_material(jak3::PatSurface::Material::ROTATE);
break;
case jak1::PatSurface::Material::NEUTRAL:
result.set_material(jak3::PatSurface::Material::NEUTRAL);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_event()) {
case jak1::PatSurface::Event::NONE:
result.set_event(jak3::PatSurface::Event::NONE);
break;
case jak1::PatSurface::Event::DEADLY:
result.set_event(jak3::PatSurface::Event::DEADLY);
break;
case jak1::PatSurface::Event::ENDLESSFALL:
result.set_event(jak3::PatSurface::Event::ENDLESSFALL);
break;
case jak1::PatSurface::Event::BURN:
result.set_event(jak3::PatSurface::Event::BURN);
break;
case jak1::PatSurface::Event::DEADLYUP:
result.set_event(jak3::PatSurface::Event::DEADLYUP);
break;
case jak1::PatSurface::Event::BURNUP:
result.set_event(jak3::PatSurface::Event::BURNUP);
break;
case jak1::PatSurface::Event::MELT:
result.set_event(jak3::PatSurface::Event::MELT);
break;
default:
ASSERT_NOT_REACHED();
}
result.set_noentity(jak1.get_noentity());
result.set_nocamera(jak1.get_nocamera());
result.set_noedge(jak1.get_noedge());
result.set_nolineofsight(jak1.get_nolineofsight());
return result;
}
-129
View File
@@ -135,135 +135,6 @@ bool bounding_box_bounding_box(const BoundingBox& a, const BoundingBox& b) {
return true;
}
/*!
* Convert jak1-format PatSurface to Jak 2.
*/
jak2::PatSurface jak2_pat(jak1::PatSurface jak1) {
jak2::PatSurface result;
switch (jak1.get_mode()) {
case jak1::PatSurface::Mode::GROUND:
result.set_mode(jak2::PatSurface::Mode::GROUND);
break;
case jak1::PatSurface::Mode::WALL:
result.set_mode(jak2::PatSurface::Mode::WALL);
break;
case jak1::PatSurface::Mode::OBSTACLE:
result.set_mode(jak2::PatSurface::Mode::OBSTACLE);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_material()) {
case jak1::PatSurface::Material::STONE:
result.set_material(jak2::PatSurface::Material::STONE);
break;
case jak1::PatSurface::Material::ICE:
result.set_material(jak2::PatSurface::Material::ICE);
break;
case jak1::PatSurface::Material::QUICKSAND:
result.set_material(jak2::PatSurface::Material::QUICKSAND);
break;
case jak1::PatSurface::Material::WATERBOTTOM:
result.set_material(jak2::PatSurface::Material::WATERBOTTOM);
break;
case jak1::PatSurface::Material::TAR:
result.set_material(jak2::PatSurface::Material::TAR);
break;
case jak1::PatSurface::Material::SAND:
result.set_material(jak2::PatSurface::Material::SAND);
break;
case jak1::PatSurface::Material::WOOD:
result.set_material(jak2::PatSurface::Material::WOOD);
break;
case jak1::PatSurface::Material::GRASS:
result.set_material(jak2::PatSurface::Material::GRASS);
break;
case jak1::PatSurface::Material::PCMETAL:
result.set_material(jak2::PatSurface::Material::PCMETAL);
break;
case jak1::PatSurface::Material::SNOW:
result.set_material(jak2::PatSurface::Material::SNOW);
break;
case jak1::PatSurface::Material::DEEPSNOW:
result.set_material(jak2::PatSurface::Material::DEEPSNOW);
break;
case jak1::PatSurface::Material::HOTCOALS:
result.set_material(jak2::PatSurface::Material::HOTCOALS);
break;
case jak1::PatSurface::Material::LAVA:
result.set_material(jak2::PatSurface::Material::LAVA);
break;
case jak1::PatSurface::Material::CRWOOD:
result.set_material(jak2::PatSurface::Material::CRWOOD);
break;
case jak1::PatSurface::Material::GRAVEL:
result.set_material(jak2::PatSurface::Material::GRAVEL);
break;
case jak1::PatSurface::Material::DIRT:
result.set_material(jak2::PatSurface::Material::DIRT);
break;
case jak1::PatSurface::Material::METAL:
result.set_material(jak2::PatSurface::Material::METAL);
break;
case jak1::PatSurface::Material::STRAW:
result.set_material(jak2::PatSurface::Material::STRAW);
break;
case jak1::PatSurface::Material::TUBE:
result.set_material(jak2::PatSurface::Material::TUBE);
break;
case jak1::PatSurface::Material::SWAMP:
result.set_material(jak2::PatSurface::Material::SWAMP);
break;
case jak1::PatSurface::Material::STOPPROJ:
result.set_material(jak2::PatSurface::Material::STOPPROJ);
break;
case jak1::PatSurface::Material::ROTATE:
result.set_material(jak2::PatSurface::Material::ROTATE);
break;
case jak1::PatSurface::Material::NEUTRAL:
result.set_material(jak2::PatSurface::Material::NEUTRAL);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_event()) {
case jak1::PatSurface::Event::NONE:
result.set_event(jak2::PatSurface::Event::NONE);
break;
case jak1::PatSurface::Event::DEADLY:
result.set_event(jak2::PatSurface::Event::DEADLY);
break;
case jak1::PatSurface::Event::ENDLESSFALL:
result.set_event(jak2::PatSurface::Event::ENDLESSFALL);
break;
case jak1::PatSurface::Event::BURN:
result.set_event(jak2::PatSurface::Event::BURN);
break;
case jak1::PatSurface::Event::DEADLYUP:
result.set_event(jak2::PatSurface::Event::DEADLYUP);
break;
case jak1::PatSurface::Event::BURNUP:
result.set_event(jak2::PatSurface::Event::BURNUP);
break;
case jak1::PatSurface::Event::MELT:
result.set_event(jak2::PatSurface::Event::MELT);
break;
default:
ASSERT_NOT_REACHED();
}
result.set_noentity(jak1.get_noentity());
result.set_nocamera(jak1.get_nocamera());
result.set_noedge(jak1.get_noedge());
result.set_nolineofsight(jak1.get_nolineofsight());
return result;
}
/*!
* Construct a collide hash from a jak1 format mesh by converting to jak 2.
*/
-129
View File
@@ -136,135 +136,6 @@ bool bounding_box_bounding_box(const BoundingBox& a, const BoundingBox& b) {
return true;
}
/*!
* Convert jak1-format PatSurface to Jak 3.
*/
jak3::PatSurface jak3_pat(jak1::PatSurface jak1) {
jak3::PatSurface result;
switch (jak1.get_mode()) {
case jak1::PatSurface::Mode::GROUND:
result.set_mode(jak3::PatSurface::Mode::GROUND);
break;
case jak1::PatSurface::Mode::WALL:
result.set_mode(jak3::PatSurface::Mode::WALL);
break;
case jak1::PatSurface::Mode::OBSTACLE:
result.set_mode(jak3::PatSurface::Mode::OBSTACLE);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_material()) {
case jak1::PatSurface::Material::STONE:
result.set_material(jak3::PatSurface::Material::STONE);
break;
case jak1::PatSurface::Material::ICE:
result.set_material(jak3::PatSurface::Material::ICE);
break;
case jak1::PatSurface::Material::QUICKSAND:
result.set_material(jak3::PatSurface::Material::QUICKSAND);
break;
case jak1::PatSurface::Material::WATERBOTTOM:
result.set_material(jak3::PatSurface::Material::WATERBOTTOM);
break;
case jak1::PatSurface::Material::TAR:
result.set_material(jak3::PatSurface::Material::TAR);
break;
case jak1::PatSurface::Material::SAND:
result.set_material(jak3::PatSurface::Material::SAND);
break;
case jak1::PatSurface::Material::WOOD:
result.set_material(jak3::PatSurface::Material::WOOD);
break;
case jak1::PatSurface::Material::GRASS:
result.set_material(jak3::PatSurface::Material::GRASS);
break;
case jak1::PatSurface::Material::PCMETAL:
result.set_material(jak3::PatSurface::Material::PCMETAL);
break;
case jak1::PatSurface::Material::SNOW:
result.set_material(jak3::PatSurface::Material::SNOW);
break;
case jak1::PatSurface::Material::DEEPSNOW:
result.set_material(jak3::PatSurface::Material::DEEPSNOW);
break;
case jak1::PatSurface::Material::HOTCOALS:
result.set_material(jak3::PatSurface::Material::HOTCOALS);
break;
case jak1::PatSurface::Material::LAVA:
result.set_material(jak3::PatSurface::Material::LAVA);
break;
case jak1::PatSurface::Material::CRWOOD:
result.set_material(jak3::PatSurface::Material::CRWOOD);
break;
case jak1::PatSurface::Material::GRAVEL:
result.set_material(jak3::PatSurface::Material::GRAVEL);
break;
case jak1::PatSurface::Material::DIRT:
result.set_material(jak3::PatSurface::Material::DIRT);
break;
case jak1::PatSurface::Material::METAL:
result.set_material(jak3::PatSurface::Material::METAL);
break;
case jak1::PatSurface::Material::STRAW:
result.set_material(jak3::PatSurface::Material::STRAW);
break;
case jak1::PatSurface::Material::TUBE:
result.set_material(jak3::PatSurface::Material::TUBE);
break;
case jak1::PatSurface::Material::SWAMP:
result.set_material(jak3::PatSurface::Material::SWAMP);
break;
case jak1::PatSurface::Material::STOPPROJ:
result.set_material(jak3::PatSurface::Material::STOPPROJ);
break;
case jak1::PatSurface::Material::ROTATE:
result.set_material(jak3::PatSurface::Material::ROTATE);
break;
case jak1::PatSurface::Material::NEUTRAL:
result.set_material(jak3::PatSurface::Material::NEUTRAL);
break;
default:
ASSERT_NOT_REACHED();
}
switch (jak1.get_event()) {
case jak1::PatSurface::Event::NONE:
result.set_event(jak3::PatSurface::Event::NONE);
break;
case jak1::PatSurface::Event::DEADLY:
result.set_event(jak3::PatSurface::Event::DEADLY);
break;
case jak1::PatSurface::Event::ENDLESSFALL:
result.set_event(jak3::PatSurface::Event::ENDLESSFALL);
break;
case jak1::PatSurface::Event::BURN:
result.set_event(jak3::PatSurface::Event::BURN);
break;
case jak1::PatSurface::Event::DEADLYUP:
result.set_event(jak3::PatSurface::Event::DEADLYUP);
break;
case jak1::PatSurface::Event::BURNUP:
result.set_event(jak3::PatSurface::Event::BURNUP);
break;
case jak1::PatSurface::Event::MELT:
result.set_event(jak3::PatSurface::Event::MELT);
break;
default:
ASSERT_NOT_REACHED();
}
result.set_noentity(jak1.get_noentity());
result.set_nocamera(jak1.get_nocamera());
result.set_noedge(jak1.get_noedge());
result.set_nolineofsight(jak1.get_nolineofsight());
return result;
}
/*!
* Construct a collide hash from a jak1 format mesh by converting to jak 3.
*/
@@ -83,7 +83,7 @@ void dedup_tie_vertices(TieOutput& data) {
bool prim_needs_tie(const tinygltf::Model& model, const tinygltf::Primitive& prim) {
if (prim.material >= 0) {
auto mat = model.materials.at(prim.material);
return mat.extensions.contains("KHR_materials_specular");
return envmap_is_valid(mat);
}
return false;
}
+14 -6
View File
@@ -17,7 +17,8 @@
namespace jak1 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix) {
const std::string& output_prefix,
bool gen_fr3) {
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
@@ -129,8 +130,8 @@ bool run_build_level(const std::string& input_file,
// Add textures and models
// TODO remove hardcoded config settings
if ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty())) {
if (gen_fr3 && ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty()))) {
lg::info("Looking for ISO path...");
const auto iso_folder = file_util::get_iso_dir_for_game(GameVersion::Jak1);
lg::info("Found ISO path: {}", iso_folder.string());
@@ -173,6 +174,11 @@ bool run_build_level(const std::string& input_file,
auto textures_out = file_util::get_jak_project_dir() / "decompiler_out/jak1/textures";
file_util::create_dir_if_needed(textures_out);
db.process_tpages(tex_db, textures_out, config, "");
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);
}
std::vector<std::string> processed_art_groups;
@@ -313,7 +319,7 @@ 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()) {
if (gen_fr3 && 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);
@@ -329,8 +335,10 @@ bool run_build_level(const std::string& input_file,
file_util::write_binary_file(save_path, result.data(), result.size());
// Save the PC level
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
if (gen_fr3) {
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
}
return true;
}
+2 -1
View File
@@ -5,5 +5,6 @@
namespace jak1 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix);
const std::string& output_prefix,
bool gen_fr3);
} // namespace jak1
+5
View File
@@ -24,6 +24,7 @@ size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
int num_trees = 0;
num_trees += tfrags.size();
num_trees += ties.size();
gen.add_word(num_trees << 16);
gen.add_word(0);
gen.add_word(0);
@@ -47,6 +48,10 @@ size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const {
// gen.set_word(tree_word++, tfrag.add_to_object_file(gen));
gen.link_word_to_byte(tree_word++, tfrag.add_to_object_file(gen));
}
for (auto& tie : ties) {
gen.link_word_to_byte(tree_word++, tie.add_to_object_file(gen));
}
}
return result;
+1 -2
View File
@@ -9,6 +9,7 @@
#include "goalc/build_level/collide/common/collide_common.h"
#include "goalc/build_level/collide/jak2/collide.h"
#include "goalc/build_level/common/Tfrag.h"
#include "goalc/build_level/common/Tie.h"
#include "goalc/build_level/jak2/Entity.h"
#include "goalc/build_level/jak2/FileInfo.h"
@@ -17,8 +18,6 @@ struct VisibilityString {
std::vector<u8> bytes;
};
struct DrawableTreeInstanceTie {};
struct DrawableTreeActor {};
struct DrawableTreeInstanceShrub {};
+29 -7
View File
@@ -13,7 +13,8 @@
namespace jak2 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix) {
const std::string& output_prefix,
bool gen_fr3) {
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
@@ -71,6 +72,13 @@ bool run_build_level(const std::string& input_file,
// TFRAG
file.drawable_trees.tfrags.emplace_back("drawable-tree-tfrag", "drawable-inline-array-tfrag");
tfrag_from_gltf(mesh_extract_out.tfrag, pc_level.tfrag_trees[0]);
// TIE
if (!mesh_extract_out.tie.base_draws.empty()) {
file.drawable_trees.ties.emplace_back();
tie_from_gltf(mesh_extract_out.tie, pc_level.tie_trees[0]);
}
pc_level.textures = std::move(tex_pool.textures_by_idx);
// COLLIDE
@@ -90,8 +98,8 @@ bool run_build_level(const std::string& input_file,
// Add textures and models
// TODO remove hardcoded config settings
if ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty())) {
if (gen_fr3 && ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty()))) {
lg::info("Looking for ISO path...");
const auto iso_folder = file_util::get_iso_dir_for_game(GameVersion::Jak2);
lg::info("Found ISO path: {}", iso_folder.string());
@@ -134,9 +142,14 @@ bool run_build_level(const std::string& input_file,
auto textures_out = file_util::get_jak_project_dir() / "decompiler_out/jak2/textures";
file_util::create_dir_if_needed(textures_out);
db.process_tpages(tex_db, textures_out, config, "");
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);
}
// find all art groups used by the custom level in other dgos
if (level_json.contains("art_groups") && !level_json.at("art_groups").empty()) {
if (gen_fr3 && level_json.contains("art_groups") && !level_json.at("art_groups").empty()) {
for (auto& dgo : config.dgo_names) {
std::vector<std::string> processed_art_groups;
// remove "DGO/" prefix
@@ -190,10 +203,19 @@ bool run_build_level(const std::string& input_file,
}
}
// Save the PC level
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
// add custom models to fr3
if (gen_fr3 && 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::Jak2, name, pc_level);
}
}
// Save the PC level
if (gen_fr3) {
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
}
return true;
}
} // namespace jak2
+2 -1
View File
@@ -5,5 +5,6 @@
namespace jak2 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix);
const std::string& output_prefix,
bool gen_fr3);
} // namespace jak2
+5
View File
@@ -24,6 +24,7 @@ size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const {
size_t result = gen.current_offset_bytes();
int num_trees = 0;
num_trees += tfrags.size();
num_trees += ties.size();
gen.add_word(num_trees << 16);
gen.add_word(0);
gen.add_word(0);
@@ -47,6 +48,10 @@ size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const {
// gen.set_word(tree_word++, tfrag.add_to_object_file(gen));
gen.link_word_to_byte(tree_word++, tfrag.add_to_object_file(gen));
}
for (auto& tie : ties) {
gen.link_word_to_byte(tree_word++, tie.add_to_object_file(gen));
}
}
return result;
+1 -2
View File
@@ -9,6 +9,7 @@
#include "goalc/build_level/collide/common/collide_common.h"
#include "goalc/build_level/collide/jak3/collide.h"
#include "goalc/build_level/common/Tfrag.h"
#include "goalc/build_level/common/Tie.h"
#include "goalc/build_level/jak3/Entity.h"
#include "goalc/build_level/jak3/FileInfo.h"
@@ -17,8 +18,6 @@ struct VisibilityString {
std::vector<u8> bytes;
};
struct DrawableTreeInstanceTie {};
struct DrawableTreeActor {};
struct DrawableTreeInstanceShrub {};
+29 -7
View File
@@ -11,7 +11,8 @@
namespace jak3 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix) {
const std::string& output_prefix,
bool gen_fr3) {
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
@@ -69,6 +70,13 @@ bool run_build_level(const std::string& input_file,
// TFRAG
file.drawable_trees.tfrags.emplace_back("drawable-tree-tfrag", "drawable-inline-array-tfrag");
tfrag_from_gltf(mesh_extract_out.tfrag, pc_level.tfrag_trees[0]);
// TIE
if (!mesh_extract_out.tie.base_draws.empty()) {
file.drawable_trees.ties.emplace_back();
tie_from_gltf(mesh_extract_out.tie, pc_level.tie_trees[0]);
}
pc_level.textures = std::move(tex_pool.textures_by_idx);
// COLLIDE
@@ -88,8 +96,8 @@ bool run_build_level(const std::string& input_file,
// Add textures and models
// TODO remove hardcoded config settings
if ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty())) {
if (gen_fr3 && ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
(level_json.contains("textures") && !level_json.at("textures").empty()))) {
lg::info("Looking for ISO path...");
const auto iso_folder = file_util::get_iso_dir_for_game(GameVersion::Jak3);
lg::info("Found ISO path: {}", iso_folder.string());
@@ -132,9 +140,14 @@ bool run_build_level(const std::string& input_file,
auto textures_out = file_util::get_jak_project_dir() / "decompiler_out/jak3/textures";
file_util::create_dir_if_needed(textures_out);
db.process_tpages(tex_db, textures_out, config, "");
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);
}
// find all art groups used by the custom level in other dgos
if (level_json.contains("art_groups") && !level_json.at("art_groups").empty()) {
if (gen_fr3 && level_json.contains("art_groups") && !level_json.at("art_groups").empty()) {
for (auto& dgo : config.dgo_names) {
std::vector<std::string> processed_art_groups;
// remove "DGO/" prefix
@@ -188,10 +201,19 @@ bool run_build_level(const std::string& input_file,
}
}
// Save the PC level
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
// add custom models to fr3
if (gen_fr3 && 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::Jak3, name, pc_level);
}
}
// Save the PC level
if (gen_fr3) {
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
}
return true;
}
} // namespace jak3
+2 -1
View File
@@ -5,5 +5,6 @@
namespace jak3 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix);
const std::string& output_prefix,
bool gen_fr3);
} // namespace jak3
+5 -3
View File
@@ -18,6 +18,7 @@ int main(int argc, char** argv) {
// game version
std::string game, input_json, output_file;
bool gen_fr3 = true;
fs::path project_path_override;
// path
@@ -38,6 +39,7 @@ int main(int argc, char** argv) {
app.add_option("-g,--game", game, "Game version (jak1, jak2 or jak3)")->required();
app.add_option("--proj-path", project_path_override,
"Specify the location of the 'data/' folder");
app.add_flag("--fr3", gen_fr3, "Generate FR3 file for this level (default true)");
app.validate_positionals();
CLI11_PARSE(app, argc, argv);
@@ -58,13 +60,13 @@ int main(int argc, char** argv) {
switch (game_version) {
case GameVersion::Jak1:
jak1::run_build_level(input_json, output_file, "jak1/");
jak1::run_build_level(input_json, output_file, "jak1/", gen_fr3);
break;
case GameVersion::Jak2:
jak2::run_build_level(input_json, output_file, "jak2/");
jak2::run_build_level(input_json, output_file, "jak2/", gen_fr3);
break;
case GameVersion::Jak3:
jak3::run_build_level(input_json, output_file, "jak3/");
jak3::run_build_level(input_json, output_file, "jak3/", gen_fr3);
break;
default:
ASSERT_NOT_REACHED_MSG("unsupported game version");
+2
View File
@@ -112,6 +112,8 @@ MakeSystem::MakeSystem(const std::optional<REPL::Config> repl_config, const std:
add_tool<BuildLevel2Tool>();
add_tool<BuildLevel3Tool>();
add_tool<BuildActorTool>();
add_tool<BuildActor2Tool>();
add_tool<BuildActor3Tool>();
}
/*!
+101 -20
View File
@@ -250,71 +250,152 @@ bool SubtitleV2Tool::run(const ToolInput& task, const PathMap& path_map) {
BuildLevelTool::BuildLevelTool() : Tool("build-level") {}
bool BuildLevelTool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto deps = get_build_level_deps(task.input.at(0));
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
auto rerun = task.input.at(1) == "#t";
std::vector in = {task.input.at(0)};
return rerun || Tool::needs_run({in, deps, task.output, task.arg}, path_map);
}
bool BuildLevelTool::run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
return jak1::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix);
auto gen_fr3 = task.input.at(2) == "#t";
return jak1::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix,
gen_fr3);
}
BuildLevel2Tool::BuildLevel2Tool() : Tool("build-level2") {}
bool BuildLevel2Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto deps = get_build_level_deps(task.input.at(0));
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
auto rerun = task.input.at(1) == "#t";
std::vector in = {task.input.at(0)};
return rerun || Tool::needs_run({in, deps, task.output, task.arg}, path_map);
}
bool BuildLevel2Tool::run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
return jak2::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix);
auto gen_fr3 = task.input.at(2) == "#t";
return jak2::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix,
gen_fr3);
}
BuildLevel3Tool::BuildLevel3Tool() : Tool("build-level3") {}
bool BuildLevel3Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto deps = get_build_level_deps(task.input.at(0));
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
auto rerun = task.input.at(1) == "#t";
std::vector in = {task.input.at(0)};
return rerun || Tool::needs_run({in, deps, task.output, task.arg}, path_map);
}
bool BuildLevel3Tool::run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
if (task.input.size() > 3) {
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);
auto gen_fr3 = task.input.at(2) == "#t";
return jak3::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix,
gen_fr3);
}
BuildActorTool::BuildActorTool() : Tool("build-actor") {}
bool BuildActorTool::needs_run(const ToolInput& task, const PathMap& path_map) {
(void)path_map;
if (task.input.size() > 2) {
if (task.input.size() > 4) {
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;
auto rerun = task.input.at(2) == "#t";
std::vector<std::string> deps{task.input.at(0)};
return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map);
}
bool BuildActorTool::run(const ToolInput& task, const PathMap& path_map) {
(void)path_map;
if (task.input.size() > 2) {
if (task.input.size() > 4) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto gen_mesh = task.input.at(1) == "#t";
return jak1::run_build_actor(task.input.at(0), task.output.at(0), gen_mesh);
jak1::BuildActorParams params;
params.gen_collide_mesh = task.input.at(1) == "#t";
if (task.input.at(3) == "#f") {
params.texture_bucket = -1;
} else {
try {
params.texture_bucket = static_cast<u8>(std::stoi(task.input.at(3)));
} catch (std::invalid_argument&) {
throw std::runtime_error("[build-actor] texture-bucket must be #f or a valid integer.");
}
}
return jak1::run_build_actor(task.input.at(0), task.output.at(0), params);
}
BuildActor2Tool::BuildActor2Tool() : Tool("build-actor2") {}
bool BuildActor2Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() > 4) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto rerun = task.input.at(2) == "#t";
std::vector<std::string> deps{task.input.at(0)};
return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map);
}
bool BuildActor2Tool::run(const ToolInput& task, const PathMap& path_map) {
(void)path_map;
if (task.input.size() > 4) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
jak2::BuildActorParams params;
params.gen_collide_mesh = task.input.at(1) == "#t";
if (task.input.at(3) == "#f") {
params.texture_bucket = -1;
} else {
try {
params.texture_bucket = static_cast<u8>(std::stoi(task.input.at(3)));
} catch (std::invalid_argument&) {
throw std::runtime_error("[build-actor2] texture-bucket must be #f or a valid integer.");
}
}
return jak2::run_build_actor(task.input.at(0), task.output.at(0), params);
}
BuildActor3Tool::BuildActor3Tool() : Tool("build-actor3") {}
bool BuildActor3Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() > 4) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto rerun = task.input.at(2) == "#t";
std::vector<std::string> deps{task.input.at(0)};
return rerun || Tool::needs_run({deps, deps, task.output, task.arg}, path_map);
}
bool BuildActor3Tool::run(const ToolInput& task, const PathMap& path_map) {
(void)path_map;
if (task.input.size() > 4) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
jak3::BuildActorParams params;
params.gen_collide_mesh = task.input.at(1) == "#t";
if (task.input.at(3) == "#f") {
params.texture_bucket = -1;
} else {
try {
params.texture_bucket = static_cast<u8>(std::stoi(task.input.at(3)));
} catch (std::invalid_argument&) {
throw std::runtime_error("[build-actor3] texture-bucket must be #f or a valid integer.");
}
}
return jak3::run_build_actor(task.input.at(0), task.output.at(0), params);
}
+14
View File
@@ -99,3 +99,17 @@ class BuildActorTool : public Tool {
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
class BuildActor2Tool : public Tool {
public:
BuildActor2Tool();
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
class BuildActor3Tool : public Tool {
public:
BuildActor3Tool();
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};