diff --git a/common/util/gltf_util.cpp b/common/util/gltf_util.cpp index 4f7e3bb3a0..d72ee6b099 100644 --- a/common/util/gltf_util.cpp +++ b/common/util/gltf_util.cpp @@ -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 find_single_skin(const tinygltf::Model& model, diff --git a/custom_assets/jak2/levels/test-zone/test-zone.jsonc b/custom_assets/jak2/levels/test-zone/test-zone.jsonc index 1c9319b5a4..3e5f14f46a 100644 --- a/custom_assets/jak2/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak2/levels/test-zone/test-zone.jsonc @@ -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" + } } ] } diff --git a/custom_assets/jak2/levels/test-zone/testzone.gd b/custom_assets/jak2/levels/test-zone/testzone.gd index c94d30e683..9527b8f7b4 100644 --- a/custom_assets/jak2/levels/test-zone/testzone.gd +++ b/custom_assets/jak2/levels/test-zone/testzone.gd @@ -5,5 +5,7 @@ ("TSZ.DGO" ( "prison-obs.o" + "test-zone-obs.o" + "test-actor-ag.go" "test-zone.go" )) \ No newline at end of file diff --git a/custom_assets/jak2/models/custom_levels/test-actor.glb b/custom_assets/jak2/models/custom_levels/test-actor.glb new file mode 100644 index 0000000000..7b69a61534 Binary files /dev/null and b/custom_assets/jak2/models/custom_levels/test-actor.glb differ diff --git a/custom_assets/jak3/levels/test-zone/test-zone.jsonc b/custom_assets/jak3/levels/test-zone/test-zone.jsonc index dc3d86672a..29863094b1 100644 --- a/custom_assets/jak3/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak3/levels/test-zone/test-zone.jsonc @@ -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" + } } ] } diff --git a/custom_assets/jak3/levels/test-zone/testzone.gd b/custom_assets/jak3/levels/test-zone/testzone.gd index 4c8b34e19d..18e15a2c1e 100644 --- a/custom_assets/jak3/levels/test-zone/testzone.gd +++ b/custom_assets/jak3/levels/test-zone/testzone.gd @@ -3,6 +3,8 @@ ;; the actual file name still needs to be 8.3 ("TSZ.DGO" - ("test-zone.go" - ) - ) \ No newline at end of file + ( + "test-zone-obs.o" + "test-actor-ag.go" + "test-zone.go" + )) \ No newline at end of file diff --git a/custom_assets/jak3/models/custom_levels/test-actor.glb b/custom_assets/jak3/models/custom_levels/test-actor.glb new file mode 100644 index 0000000000..7b69a61534 Binary files /dev/null and b/custom_assets/jak3/models/custom_levels/test-actor.glb differ diff --git a/goal_src/jak1/game.gp b/goal_src/jak1/game.gp index da6370fe95..1a8d191da5 100644 --- a/goal_src/jak1/game.gp +++ b/goal_src/jak1/game.gp @@ -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"))))) diff --git a/goal_src/jak2/engine/data/art-h.gc b/goal_src/jak2/engine/data/art-h.gc index f5af744a36..5eaf5a150a 100644 --- a/goal_src/jak2/engine/data/art-h.gc +++ b/goal_src/jak2/engine/data/art-h.gc @@ -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") \ No newline at end of file diff --git a/goal_src/jak2/game.gp b/goal_src/jak2/game.gp index 4c3a75eadd..ea5d48754c 100644 --- a/goal_src/jak2/game.gp +++ b/goal_src/jak2/game.gp @@ -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 ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak2/levels/test-zone/test-zone-obs.gc b/goal_src/jak2/levels/test-zone/test-zone-obs.gc new file mode 100644 index 0000000000..8729fbd32b --- /dev/null +++ b/goal_src/jak2/levels/test-zone/test-zone-obs.gc @@ -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) diff --git a/goal_src/jak2/lib/project-lib.gp b/goal_src/jak2/lib/project-lib.gp index 07ca1a63d9..6fcef076c9 100644 --- a/goal_src/jak2/lib/project-lib.gp +++ b/goal_src/jak2/lib/project-lib.gp @@ -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 diff --git a/goal_src/jak3/engine/data/art-h.gc b/goal_src/jak3/engine/data/art-h.gc index 43dc70a879..f06f4967a7 100644 --- a/goal_src/jak3/engine/data/art-h.gc +++ b/goal_src/jak3/engine/data/art-h.gc @@ -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") diff --git a/goal_src/jak3/game.gp b/goal_src/jak3/game.gp index 419f186bd5..e2535dd8cc 100644 --- a/goal_src/jak3/game.gp +++ b/goal_src/jak3/game.gp @@ -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 ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak3/levels/test-zone/test-zone-obs.gc b/goal_src/jak3/levels/test-zone/test-zone-obs.gc new file mode 100644 index 0000000000..0443f60718 --- /dev/null +++ b/goal_src/jak3/levels/test-zone/test-zone-obs.gc @@ -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) diff --git a/goal_src/jak3/lib/project-lib.gp b/goal_src/jak3/lib/project-lib.gp index 087f583dea..ab53a8d2b0 100644 --- a/goal_src/jak3/lib/project-lib.gp +++ b/goal_src/jak3/lib/project-lib.gp @@ -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"))))) \ No newline at end of file + :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"))))) \ No newline at end of file diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 519ae15666..20c40403e5 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -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 diff --git a/goalc/build_actor/common/MercExtract.cpp b/goalc/build_actor/common/MercExtract.cpp index 84f7d7f7a4..c079c9fb41 100644 --- a/goalc/build_actor/common/MercExtract.cpp +++ b/goalc/build_actor/common/MercExtract.cpp @@ -180,7 +180,7 @@ gltf_util::MercSwapData load_merc_model(u32 current_idx_count, return result; } -std::vector gen_collide_mesh_from_model( +std::vector gen_collide_mesh_from_model_jak1( const tinygltf::Model& model, const std::vector& all_nodes, int joint_idx) { @@ -278,3 +278,201 @@ std::vector gen_collide_mesh_from_model( } return cmeshes; } + +std::vector gen_collide_mesh_from_model_jak2( + const tinygltf::Model& model, + const std::vector& all_nodes, + int joint_idx) { + // data for a single primitive + struct PrimWork { + std::string mesh_name; + std::vector verts; + std::vector indices; + gltf_mesh_extract::PatResult pat; + }; + std::vector> 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 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 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 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 verts; + std::vector 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 gen_collide_mesh_from_model_jak3( + const tinygltf::Model& model, + const std::vector& all_nodes, + int joint_idx) { + // data for a single primitive + struct PrimWork { + std::string mesh_name; + std::vector verts; + std::vector indices; + gltf_mesh_extract::PatResult pat; + }; + std::vector> 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 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 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 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 verts; + std::vector 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; +} \ No newline at end of file diff --git a/goalc/build_actor/common/MercExtract.h b/goalc/build_actor/common/MercExtract.h index 5c6b6808ec..37716d140a 100644 --- a/goalc/build_actor/common/MercExtract.h +++ b/goalc/build_actor/common/MercExtract.h @@ -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 gen_collide_mesh_from_model( + +std::vector gen_collide_mesh_from_model_jak1( + const tinygltf::Model& model, + const std::vector& all_nodes, + int joint_idx); +std::vector gen_collide_mesh_from_model_jak2( + const tinygltf::Model& model, + const std::vector& all_nodes, + int joint_idx); +std::vector gen_collide_mesh_from_model_jak3( const tinygltf::Model& model, const std::vector& all_nodes, int joint_idx); \ No newline at end of file diff --git a/goalc/build_actor/common/build_actor.cpp b/goalc/build_actor/common/build_actor.cpp new file mode 100644 index 0000000000..8aea4cba4b --- /dev/null +++ b/goalc/build_actor/common/build_actor.cpp @@ -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 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 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 node_to_joint; + std::map joint_to_node; + std::vector 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 convert_joints(const std::vector& gjoints) { + std::vector 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 process_anim(const tinygltf::Model& model, + const std::vector& gjoints) { + if (model.animations.empty()) { + lg::warn("no animations detected!"); // TODO: make up a dummy one + return {}; + } + + std::map node_to_joint; + for (size_t i = 0; i < gjoints.size(); i++) { + node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset; + } + + std::vector 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; +} \ No newline at end of file diff --git a/goalc/build_actor/common/build_actor.h b/goalc/build_actor/common/build_actor.h new file mode 100644 index 0000000000..40f993098b --- /dev/null +++ b/goalc/build_actor/common/build_actor.h @@ -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 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 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 children; +}; + +tinygltf::Model load_gltf_model(const fs::path& path); +std::vector extract_skeleton(const tinygltf::Model& model, int skin_idx); +std::vector convert_joints(const std::vector& gjoints); +std::vector process_anim(const tinygltf::Model& model, + const std::vector& gjoints); \ No newline at end of file diff --git a/goalc/build_actor/jak1/build_actor.cpp b/goalc/build_actor/jak1/build_actor.cpp index d7180af5c4..9279bcacbf 100644 --- a/goalc/build_actor/jak1/build_actor.cpp +++ b/goalc/build_actor/jak1/build_actor.cpp @@ -138,27 +138,6 @@ ArtJointAnim::ArtJointAnim(const anim::CompressedAnim& anim, const std::vector 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("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME)); } - // jgeo.lump.add_res( - // std::make_unique("texture-level", std::vector{2}, DEFAULT_RES_TIME)); - // jgeo.lump.add_res(std::make_unique( - // "trans-offset", std::vector{{0.0f, 2048.0f, 0.0f, 1.0f}}, - // DEFAULT_RES_TIME)); - // jgeo.lump.add_res( - // std::make_unique("joint-channel", std::vector{0}, DEFAULT_RES_TIME)); - // jgeo.lump.add_res(std::make_unique( - // "lod-dist", std::vector{5000.0f * METER_LENGTH, 6000.0f * METER_LENGTH}, - // DEFAULT_RES_TIME)); + if (texture_bucket != -1) { + lump.add_res(std::make_unique( + "texture-bucket", std::vector{static_cast(texture_bucket)}, DEFAULT_RES_TIME)); + } + if (texture_level != -1) { + lump.add_res(std::make_unique("texture-level", std::vector{texture_level}, + DEFAULT_RES_TIME)); + } + if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) { + lump.add_res(std::make_unique( + "trans-offset", std::vector{trans_offset}, DEFAULT_RES_TIME)); + } + if (joint_channel != -1) { + lump.add_res(std::make_unique("joint-channel", std::vector{joint_channel}, + DEFAULT_RES_TIME)); + } + if (!lod_dist.empty()) { + lump.add_res(std::make_unique("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 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 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 node_to_joint; - std::map joint_to_node; - std::vector 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 convert_joints(const std::vector& gjoints) { - std::vector 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 process_anim(const tinygltf::Model& model, - const std::vector& gjoints) { - if (model.animations.empty()) { - lg::warn("no animations detected!"); // TODO: make up a dummy one - return {}; - } - - std::map node_to_joint; - for (size_t i = 0; i < gjoints.size(); i++) { - node_to_joint[gjoints[i].gltf_node_index] = i + kGltfToGameJointOffset; - } - - std::vector 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 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 jgeo = std::make_shared(ag.name, mesh, joints); + std::shared_ptr jgeo = std::make_shared(ag.name, mesh, joints, params); ag.elts.emplace_back(jgeo); // dummy merc-ctrl diff --git a/goalc/build_actor/jak1/build_actor.h b/goalc/build_actor/jak1/build_actor.h index ccb347cecc..e7cab0f9a8 100644 --- a/goalc/build_actor/jak1/build_actor.h +++ b/goalc/build_actor/jak1/build_actor.h @@ -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 lod_dist{}; +}; + struct ArtJointGeo : ArtElement { std::vector data; std::vector cmeshes; ResLump lump; size_t mesh_slot; + s8 texture_bucket; + s32 texture_level; + s32 joint_channel; + math::Vector4f trans_offset; + std::vector lod_dist; explicit ArtJointGeo(const std::string& name, std::vector cmeshes, - std::vector& joints) { + std::vector& 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 diff --git a/goalc/build_actor/jak2/build_actor.cpp b/goalc/build_actor/jak2/build_actor.cpp new file mode 100644 index 0000000000..f846dacb60 --- /dev/null +++ b/goalc/build_actor/jak2/build_actor.cpp @@ -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& 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 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("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME)); + } + if (texture_bucket != -1) { + lump.add_res(std::make_unique( + "texture-bucket", std::vector{static_cast(texture_bucket)}, DEFAULT_RES_TIME)); + } + if (texture_level != -1) { + lump.add_res(std::make_unique("texture-level", std::vector{texture_level}, + DEFAULT_RES_TIME)); + } + if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) { + lump.add_res(std::make_unique( + "trans-offset", std::vector{trans_offset}, DEFAULT_RES_TIME)); + } + if (joint_channel != -1) { + lump.add_res(std::make_unique("joint-channel", std::vector{joint_channel}, + DEFAULT_RES_TIME)); + } + if (!lod_dist.empty()) { + lump.add_res(std::make_unique("lod-dist", lod_dist, DEFAULT_RES_TIME)); + } + lump.sort_res(); +} + +size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const { + std::vector data_slots; + std::vector 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 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 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 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 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 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 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 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 user_anims; + + ArtGroup ag(ag_name); + std::vector 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 mesh; + if (params.gen_collide_mesh) { + mesh = gen_collide_mesh_from_model_jak2(model, all_nodes, 3); + } + + std::shared_ptr jgeo = std::make_shared(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(anim, joints)); + } + } else { + ag.elts.emplace_back(std::make_shared(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 \ No newline at end of file diff --git a/goalc/build_actor/jak2/build_actor.h b/goalc/build_actor/jak2/build_actor.h new file mode 100644 index 0000000000..bc705f43f5 --- /dev/null +++ b/goalc/build_actor/jak2/build_actor.h @@ -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 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 vertices; + std::vector 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 lod_dist{}; +}; + +struct ArtJointGeo : ArtElement { + std::vector data; + std::vector cmeshes; + ResLump lump; + size_t mesh_slot; + s8 texture_bucket; + s32 texture_level; + s32 joint_channel; + math::Vector4f trans_offset; + std::vector lod_dist; + + explicit ArtJointGeo(const std::string& name, + std::vector cmeshes, + std::vector& 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& 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& joints); + + size_t generate(DataObjectGenerator& gen) const; +}; + +struct ArtGroup : Art { + FileInfo info; + std::vector> elts; + std::map 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 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 diff --git a/goalc/build_actor/jak3/build_actor.cpp b/goalc/build_actor/jak3/build_actor.cpp new file mode 100644 index 0000000000..4d29057213 --- /dev/null +++ b/goalc/build_actor/jak3/build_actor.cpp @@ -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& 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 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("collide-mesh-group", "array", mesh_slot, DEFAULT_RES_TIME)); + } + if (texture_bucket != -1) { + lump.add_res(std::make_unique( + "texture-bucket", std::vector{static_cast(texture_bucket)}, DEFAULT_RES_TIME)); + } + if (texture_level != -1) { + lump.add_res(std::make_unique("texture-level", std::vector{texture_level}, + DEFAULT_RES_TIME)); + } + if (trans_offset != math::Vector4f{0.f, 0.f, 0.f, 1.f}) { + lump.add_res(std::make_unique( + "trans-offset", std::vector{trans_offset}, DEFAULT_RES_TIME)); + } + if (joint_channel != -1) { + lump.add_res(std::make_unique("joint-channel", std::vector{joint_channel}, + DEFAULT_RES_TIME)); + } + if (!lod_dist.empty()) { + lump.add_res(std::make_unique("lod-dist", lod_dist, DEFAULT_RES_TIME)); + } + lump.sort_res(); +} + +size_t ArtJointGeo::generate_mesh(DataObjectGenerator& gen) const { + std::vector data_slots; + std::vector 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 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 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 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 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 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 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 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 user_anims; + + ArtGroup ag(ag_name); + std::vector 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 mesh; + if (params.gen_collide_mesh) { + mesh = gen_collide_mesh_from_model_jak3(model, all_nodes, 3); + } + + std::shared_ptr jgeo = std::make_shared(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(anim, joints)); + } + } else { + ag.elts.emplace_back(std::make_shared(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 \ No newline at end of file diff --git a/goalc/build_actor/jak3/build_actor.h b/goalc/build_actor/jak3/build_actor.h new file mode 100644 index 0000000000..a436b235f1 --- /dev/null +++ b/goalc/build_actor/jak3/build_actor.h @@ -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 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 vertices; + std::vector 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 lod_dist{}; +}; + +struct ArtJointGeo : ArtElement3 { + std::vector data; + std::vector cmeshes; + ResLump lump; + size_t mesh_slot; + s8 texture_bucket; + s32 texture_level; + s32 joint_channel; + math::Vector4f trans_offset; + std::vector lod_dist; + + explicit ArtJointGeo(const std::string& name, + std::vector cmeshes, + std::vector& 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& 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& joints); + + size_t generate(DataObjectGenerator& gen) const; +}; + +struct ArtGroup : Art { + FileInfo info; + std::vector> elts; + std::map 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 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 diff --git a/goalc/build_actor/main.cpp b/goalc/build_actor/main.cpp index 86ccc2a598..842675268c 100644 --- a/goalc/build_actor/main.cpp +++ b/goalc/build_actor/main.cpp @@ -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"); } diff --git a/goalc/build_level/collide/common/collide_common.h b/goalc/build_level/collide/common/collide_common.h index f23d59bd88..01a50a6f75 100644 --- a/goalc/build_level/collide/common/collide_common.h +++ b/goalc/build_level/collide/common/collide_common.h @@ -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 \ No newline at end of file +} // 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; +} \ No newline at end of file diff --git a/goalc/build_level/collide/jak2/collide.cpp b/goalc/build_level/collide/jak2/collide.cpp index ab81b4dfc6..ef945cf053 100644 --- a/goalc/build_level/collide/jak2/collide.cpp +++ b/goalc/build_level/collide/jak2/collide.cpp @@ -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. */ diff --git a/goalc/build_level/collide/jak3/collide.cpp b/goalc/build_level/collide/jak3/collide.cpp index 1a85ee1948..f983b3dd7d 100644 --- a/goalc/build_level/collide/jak3/collide.cpp +++ b/goalc/build_level/collide/jak3/collide.cpp @@ -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. */ diff --git a/goalc/build_level/common/gltf_mesh_extract.cpp b/goalc/build_level/common/gltf_mesh_extract.cpp index 0223980232..2e384cbd9f 100644 --- a/goalc/build_level/common/gltf_mesh_extract.cpp +++ b/goalc/build_level/common/gltf_mesh_extract.cpp @@ -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; } diff --git a/goalc/build_level/jak1/build_level.cpp b/goalc/build_level/jak1/build_level.cpp index 8395940fbb..e3051f2b36 100644 --- a/goalc/build_level/jak1/build_level.cpp +++ b/goalc/build_level/jak1/build_level.cpp @@ -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 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>(); 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; } diff --git a/goalc/build_level/jak1/build_level.h b/goalc/build_level/jak1/build_level.h index 239891b540..f172aa2a67 100644 --- a/goalc/build_level/jak1/build_level.h +++ b/goalc/build_level/jak1/build_level.h @@ -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 \ No newline at end of file diff --git a/goalc/build_level/jak2/LevelFile.cpp b/goalc/build_level/jak2/LevelFile.cpp index 689ede5155..1741ea34d9 100644 --- a/goalc/build_level/jak2/LevelFile.cpp +++ b/goalc/build_level/jak2/LevelFile.cpp @@ -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; diff --git a/goalc/build_level/jak2/LevelFile.h b/goalc/build_level/jak2/LevelFile.h index fad9708f08..611f2f4fef 100644 --- a/goalc/build_level/jak2/LevelFile.h +++ b/goalc/build_level/jak2/LevelFile.h @@ -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 bytes; }; -struct DrawableTreeInstanceTie {}; - struct DrawableTreeActor {}; struct DrawableTreeInstanceShrub {}; diff --git a/goalc/build_level/jak2/build_level.cpp b/goalc/build_level/jak2/build_level.cpp index ec0f6f9ae3..e226837583 100644 --- a/goalc/build_level/jak2/build_level.cpp +++ b/goalc/build_level/jak2/build_level.cpp @@ -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 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>(); + 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 diff --git a/goalc/build_level/jak2/build_level.h b/goalc/build_level/jak2/build_level.h index 98ea4c34f7..7c50fddc46 100644 --- a/goalc/build_level/jak2/build_level.h +++ b/goalc/build_level/jak2/build_level.h @@ -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 \ No newline at end of file diff --git a/goalc/build_level/jak3/LevelFile.cpp b/goalc/build_level/jak3/LevelFile.cpp index b2fe46395d..6154b36080 100644 --- a/goalc/build_level/jak3/LevelFile.cpp +++ b/goalc/build_level/jak3/LevelFile.cpp @@ -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; diff --git a/goalc/build_level/jak3/LevelFile.h b/goalc/build_level/jak3/LevelFile.h index 48a1223c3e..996592bd75 100644 --- a/goalc/build_level/jak3/LevelFile.h +++ b/goalc/build_level/jak3/LevelFile.h @@ -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 bytes; }; -struct DrawableTreeInstanceTie {}; - struct DrawableTreeActor {}; struct DrawableTreeInstanceShrub {}; diff --git a/goalc/build_level/jak3/build_level.cpp b/goalc/build_level/jak3/build_level.cpp index fd997cfa68..615bfa8d97 100644 --- a/goalc/build_level/jak3/build_level.cpp +++ b/goalc/build_level/jak3/build_level.cpp @@ -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 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>(); + 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 diff --git a/goalc/build_level/jak3/build_level.h b/goalc/build_level/jak3/build_level.h index 4f59275c2e..d3e6c1fcbf 100644 --- a/goalc/build_level/jak3/build_level.h +++ b/goalc/build_level/jak3/build_level.h @@ -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 \ No newline at end of file diff --git a/goalc/build_level/main.cpp b/goalc/build_level/main.cpp index 8c4f3e2c4f..758fa192cb 100644 --- a/goalc/build_level/main.cpp +++ b/goalc/build_level/main.cpp @@ -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"); diff --git a/goalc/make/MakeSystem.cpp b/goalc/make/MakeSystem.cpp index dfa29b7d0e..b999b12734 100644 --- a/goalc/make/MakeSystem.cpp +++ b/goalc/make/MakeSystem.cpp @@ -112,6 +112,8 @@ MakeSystem::MakeSystem(const std::optional repl_config, const std: add_tool(); add_tool(); add_tool(); + add_tool(); + add_tool(); } /*! diff --git a/goalc/make/Tools.cpp b/goalc/make/Tools.cpp index ff6b245f4b..7bcb4e8ad6 100644 --- a/goalc/make/Tools.cpp +++ b/goalc/make/Tools.cpp @@ -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 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 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(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 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(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 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(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); } \ No newline at end of file diff --git a/goalc/make/Tools.h b/goalc/make/Tools.h index 28b88b42cb..152007be8a 100644 --- a/goalc/make/Tools.h +++ b/goalc/make/Tools.h @@ -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; +}; \ No newline at end of file