diff --git a/common/math/Vector.h b/common/math/Vector.h index 095b3cb32fe..f9bf8d0abaf 100644 --- a/common/math/Vector.h +++ b/common/math/Vector.h @@ -18,6 +18,12 @@ class Vector { return result; } + static Vector unit(int idx) { + Vector result = Vector::zero(); + result[idx] = T(1); + return result; + } + template constexpr Vector(Args... args) : m_data{T(args)...} { static_assert(sizeof...(args) == Size, "Incorrect number of args"); @@ -226,6 +232,22 @@ class Vector { } } + Vector min(const Vector& other) const { + Vector result; + for (int i = 0; i < Size; i++) { + result[i] = std::min(m_data[i], other[i]); + } + return result; + } + + Vector max(const Vector& other) const { + Vector result; + for (int i = 0; i < Size; i++) { + result[i] = std::max(m_data[i], other[i]); + } + return result; + } + std::string to_string_aligned() const { std::string result = "["; for (auto x : m_data) { diff --git a/custom_levels/test-zone/test-zone.jsonc b/custom_levels/jak1/test-zone/test-zone.jsonc similarity index 98% rename from custom_levels/test-zone/test-zone.jsonc rename to custom_levels/jak1/test-zone/test-zone.jsonc index 1f3420d4c9e..5c3ecaf9439 100644 --- a/custom_levels/test-zone/test-zone.jsonc +++ b/custom_levels/jak1/test-zone/test-zone.jsonc @@ -10,7 +10,7 @@ // Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color, // and bake to vertex colors. For now, only the first vertex color group is used, so make sure you // only have 1. - "gltf_file": "custom_levels/test-zone/test-zone2.glb", + "gltf_file": "custom_levels/jak1/test-zone/test-zone2.glb", // automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself "automatic_wall_detection": true, diff --git a/custom_levels/test-zone/test-zone2.glb b/custom_levels/jak1/test-zone/test-zone2.glb similarity index 100% rename from custom_levels/test-zone/test-zone2.glb rename to custom_levels/jak1/test-zone/test-zone2.glb diff --git a/custom_levels/test-zone/testzone.gd b/custom_levels/jak1/test-zone/testzone.gd similarity index 100% rename from custom_levels/test-zone/testzone.gd rename to custom_levels/jak1/test-zone/testzone.gd diff --git a/custom_levels/jak2/test-zone/test-zone.jsonc b/custom_levels/jak2/test-zone/test-zone.jsonc new file mode 100644 index 00000000000..c2278aa0db9 --- /dev/null +++ b/custom_levels/jak2/test-zone/test-zone.jsonc @@ -0,0 +1,83 @@ +{ + // The "in-game" name of the level. Should be lower case, with dashes (GOAL symbol name) + // the name of this file, and the folder this file is in must have the same name. + "long_name": "test-zone", + // The file name, should be upper case and 8 characters or less. + "iso_name": "TESTZONE", + // The nickname, should be exactly 3 characters + "nickname": "tsz", // 3 char name, all lowercase + // Background mesh file. + // Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color, + // and bake to vertex colors. For now, only the first vertex color group is used, so make sure you + // only have 1. + "gltf_file": "custom_levels/jak2/test-zone/test-zone2.glb", + + // automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself + "automatic_wall_detection": true, + "automatic_wall_angle": 45.0, + + // if your mesh has triangles with incorrect orientation, set this to make all collision mesh triangles double sided + // this makes collision 2x slower and bigger, so only use if really needed + "double_sided_collide": false, + + // available res-lump tag data types: + // int32, float, meters, vector, vector4m (meters) + // + // examples: + // + // adds a float tag 'spring-height' with value of 200 meters (1 meter = 4096.0 units): + // "spring-height": ["meters", 200.0] + // + // adds a vector tag 'movie-pos': + // "movie-pos": ["vector", [4096000.0, -176128.0, 1353973.76, 1.0]] + + // The base actor id for your custom level. If you have multiple levels, this should be unique! + "base_id": 100, + + // All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file. + "art_groups": ["prsn-torture-ag"], + + // 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 + // by setting "save_texture_pngs" to true in the decompiler config. + "textures": [], + + "actors" : [ + { + "trans": [-15.2818, 15.2461, 17.1360], // translation + "etype": "crate", // actor type + "game_task": 0, // associated game task (for powercells, etc) + "kill_mask": 0, + "quat": [0, 0, 0, 1], // quaternion + "bsphere": [-15.2818, 15.2461, 17.1360, 10], // bounding sphere + "lump": { + "name": "test-crate", + "eco-info": ["int32", 18, 2] + } + }, + + { + "trans": [-5.4630, 17.4553, 1.6169], // translation + "etype": "eco-yellow", // actor type + "game_task": 0, // associated game task (for powercells, etc) + "kill_mask": 0, + "quat": [0, 0, 0, 1], // quaternion + "bsphere": [-5.4630, 17.4553, 1.6169, 10], // bounding sphere + "lump": { + "name": "test-eco" + } + }, + + { + "trans": [-7.41, 13.5, 28.42], // translation + "etype": "prsn-torture", // actor type + "game_task": 0, // associated game task (for powercells, etc) + "quat": [0, 0, 0, 1], // quaternion + "bsphere": [-7.41, 13.5, 28.42, 10], // bounding sphere + "lump": { + "name": "test-torture" + } + } + ] +} \ No newline at end of file diff --git a/custom_levels/jak2/test-zone/test-zone2.glb b/custom_levels/jak2/test-zone/test-zone2.glb new file mode 100644 index 00000000000..2df7f4d5953 Binary files /dev/null and b/custom_levels/jak2/test-zone/test-zone2.glb differ diff --git a/custom_levels/jak2/test-zone/testzone.gd b/custom_levels/jak2/test-zone/testzone.gd new file mode 100644 index 00000000000..c94d30e6839 --- /dev/null +++ b/custom_levels/jak2/test-zone/testzone.gd @@ -0,0 +1,9 @@ +;; DGO definition file for Awful Village level +;; We use the convention of having a longer DGO name for levels without precomputed visibility. + +;; the actual file name still needs to be 8.3 +("TSZ.DGO" + ( + "prison-obs.o" + "test-zone.go" + )) \ No newline at end of file diff --git a/goal_src/jak1/game.gp b/goal_src/jak1/game.gp index 0d7f3c540b2..88da8f166f9 100644 --- a/goal_src/jak1/game.gp +++ b/goal_src/jak1/game.gp @@ -154,9 +154,9 @@ ) (defun custom-level-cgo (output-name desc-file-name) - "Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/)" + "Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak1/)" (let ((out-name (string-append "$OUT/iso/" output-name))) - (defstep :in (string-append "custom_levels/" desc-file-name) + (defstep :in (string-append "custom_levels/jak1/" desc-file-name) :tool 'dgo :out `(,out-name) ) @@ -208,7 +208,7 @@ ) (defmacro build-custom-level (name) - (let* ((path (string-append "custom_levels/" name "/" name ".jsonc"))) + (let* ((path (string-append "custom_levels/jak1/" name "/" name ".jsonc"))) `(defstep :in ,path :tool 'build-level :out '(,(string-append "$OUT/obj/" name ".go"))))) @@ -1636,7 +1636,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;; ;; Set up the build system to build the level geometry -;; this path is relative to the custom_levels/ folder +;; this path is relative to the custom_levels/jak1 folder ;; it should point to the .jsonc file that specifies the level. (build-custom-level "test-zone") ;; the DGO file diff --git a/goal_src/jak2/engine/level/level-info.gc b/goal_src/jak2/engine/level/level-info.gc index bfc9737d74b..5d052cfc4fe 100644 --- a/goal_src/jak2/engine/level/level-info.gc +++ b/goal_src/jak2/engine/level/level-info.gc @@ -14690,7 +14690,7 @@ 0 ) -;; added in PC port: test levels from the ps3 version +;; og:preserve-this added in PC port: test levels from the ps3 version (#when USE_PS3_LEVELS (define 4aaron (new 'static 'level-load-info :index 1 @@ -15724,3 +15724,88 @@ (cons! *level-load-list* 'wasall) (cons! *level-load-list* 'stadocc) ) + +;; og:preserve-this added test-zone level +(define test-zone + (new 'static 'level-load-info + :index #x9e + :name 'test-zone + :visname 'test-zone-vis + :nickname 'tsz + :dbname 'test-zone + :taskname 'default + :packages '() + ;; :memory-mode (load-buffer-mode small-center) + :music-bank #f + :ambient-sounds '() + :mood-func 'update-mood-default + :mood-init #f + :ocean #f + :sky #t + :use-camera-other #f + :part-engine-max 16 + :continues '((new 'static 'continue-point + :name "test-zone-start" + :level 'test-zone + :trans (new 'static 'vector :x 0.0 :y (meters 10) :z (meters 10) :w 1.0) + :quat (new 'static 'vector :y 0.061 :w 0.9981) + :camera-trans (new 'static 'vector :x 0.0 :y (meters 1) :z 0.0 :w 1.0) + :camera-rot (new 'static 'inline-array vector3s 3 + (new 'static 'vector3s :data (new 'static 'array float 3 1.0 0.0 0.0)) + (new 'static 'vector3s :data (new 'static 'array float 3 0.0 1.0 0.0)) + (new 'static 'vector3s :data (new 'static 'array float 3 0.0 0.0 1.0)) + ) + :on-goto #f + :vis-nick #f + :want (new 'static 'inline-array level-buffer-state 6 + (new 'static 'level-buffer-state :name 'test-zone :display? 'display :force-vis? #f :force-inside? #f) + (new 'static 'level-buffer-state :name 'ctywide :display? 'display :force-vis? #f :force-inside? #f) + (new 'static 'level-buffer-state :name #f :display? #f :force-vis? #f :force-inside? #f) + (new 'static 'level-buffer-state :name #f :display? #f :force-vis? #f :force-inside? #f) + (new 'static 'level-buffer-state :name #f :display? #f :force-vis? #f :force-inside? #f) + (new 'static 'level-buffer-state :name #f :display? #f :force-vis? #f :force-inside? #f) + ) + :want-sound (new 'static 'array symbol 3 #f #f #f) + ) + ) + :tasks '() + :priority 100 + :load-commands '() + :alt-load-commands '() + :bsp-mask #xffffffffffffffff + :buttom-height (meters -10000000) + :run-packages '() + :wait-for-load #t + :login-func #f + :activate-func #f + :deactivate-func #f + :kill-func #f + :borrow-level (new 'static 'array symbol 2 #f #f) + :borrow-display? (new 'static 'array symbol 2 #f #f) + :base-task-mask (task-mask task0) + :texture-anim-tfrag #f + :texture-anim-pris #f + :texture-anim-shrub #f + :texture-anim-alpha #f + :texture-anim-water #f + :texture-anim-twarp #f + :texture-anim-pris2 #f + :texture-anim-sprite #f + :texture-anim-map #f + :texture-anim-sky #f + :draw-priority 10.0 + :fog-height 327680.0 + :bigmap-id (bigmap-id bigmap-id-20) + :ocean-near-translucent? #t + :ocean-far? #t + :mood-range (new 'static 'mood-range :data (new 'static 'array float 4 0.0 1.0 0.0 1.0)) + :max-rain 1.0 + :fog-mult 1.0 + :ocean-alpha 1.0 + :extra-sound-bank #f + ) + ) + +(#when PC_PORT + (cons! *level-load-list* 'test-zone) + ) diff --git a/goal_src/jak2/engine/target/target-death.gc b/goal_src/jak2/engine/target/target-death.gc index f7a97034485..95ec8b0ebf2 100644 --- a/goal_src/jak2/engine/target/target-death.gc +++ b/goal_src/jak2/engine/target/target-death.gc @@ -262,7 +262,8 @@ ) ) (let ((s5-4 (level-get *level* (-> arg0 level)))) - (when s5-4 + ;; og:preserve-this don't wait for vis if level doesn't have it + (when (and s5-4 (-> s5-4 vis-info 0)) (while (and (-> *level* vis?) (-> s5-4 vis-info 0) (= (-> s5-4 all-visible?) 'loading)) (suspend) ) diff --git a/goal_src/jak2/game.gp b/goal_src/jak2/game.gp index fef36ee8373..29f3973ffcc 100644 --- a/goal_src/jak2/game.gp +++ b/goal_src/jak2/game.gp @@ -291,6 +291,17 @@ (cgo-file "wasall.gd" common-dep) ) +;;;;;;;;;;;;;;;;;;;;;;;;; +;; Example Custom Level +;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Set up the build system to build the level geometry +;; this path is relative to the custom_levels/jak2 folder +;; it should point to the .jsonc file that specifies the level. +(build-custom-level "test-zone") +;; the DGO file +(custom-level-cgo "TSZ.DGO" "test-zone/testzone.gd") + ;;;;;;;;;;;;;;;;;;;;; ;; ANIMATIONS ;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak2/lib/project-lib.gp b/goal_src/jak2/lib/project-lib.gp index 7596a613181..1ace9a754ed 100644 --- a/goal_src/jak2/lib/project-lib.gp +++ b/goal_src/jak2/lib/project-lib.gp @@ -83,6 +83,17 @@ ) ) +(defun custom-level-cgo (output-name desc-file-name) + "Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak2/)" + (let ((out-name (string-append "$OUT/iso/" output-name))) + (defstep :in (string-append "custom_levels/jak2/" desc-file-name) + :tool 'dgo + :out `(,out-name) + ) + (set! *all-cgos* (cons out-name *all-cgos*)) + ) + ) + (defun cgo (output-name desc-file-name) "Add a CGO with the given output name (in $OUT/iso) and input name (in goal_src/jak2/dgos)" (let ((out-name (string-append "$OUT/iso/" output-name))) @@ -124,6 +135,12 @@ ) ) +(defmacro build-custom-level (name) + (let* ((path (string-append "custom_levels/jak2/" name "/" name ".jsonc"))) + `(defstep :in ,path + :tool 'build-level2 + :out '(,(string-append "$OUT/obj/" name ".go"))))) + (defmacro group (name &rest stuff) `(defstep :in "" :tool 'group diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 48e09db03dd..f2ae00b0959 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -5,18 +5,26 @@ add_library(compiler emitter/ObjectGenerator.cpp emitter/Register.cpp debugger/disassemble.cpp - build_level/build_level.cpp - build_level/collide_bvh.cpp - build_level/collide_drawable.cpp - build_level/collide_pack.cpp - build_level/color_quantization.cpp - build_level/Entity.cpp - build_level/FileInfo.cpp - build_level/gltf_mesh_extract.cpp - build_level/LevelFile.cpp - build_level/ResLump.cpp - build_level/Tfrag.cpp - build_level/ambient.cpp + build_level/common/build_level.cpp + build_level/jak1/build_level.cpp + build_level/jak2/build_level.cpp + build_level/collide/jak1/collide_bvh.cpp + build_level/collide/jak1/collide_drawable.cpp + build_level/collide/jak1/collide_pack.cpp + build_level/collide/jak2/collide.cpp + build_level/common/color_quantization.cpp + build_level/common/Entity.cpp + build_level/jak1/Entity.cpp + build_level/jak2/Entity.cpp + build_level/common/FileInfo.cpp + build_level/jak1/FileInfo.cpp + build_level/jak2/FileInfo.cpp + build_level/common/gltf_mesh_extract.cpp + build_level/jak1/LevelFile.cpp + build_level/jak2/LevelFile.cpp + build_level/common/ResLump.cpp + build_level/common/Tfrag.cpp + build_level/jak1/ambient.cpp compiler/Compiler.cpp compiler/Env.cpp compiler/Val.cpp @@ -65,6 +73,9 @@ endif () add_executable(goalc main.cpp) add_executable(goalc-simple simple_main.cpp) +add_executable(build_level build_level/main.cpp) + target_link_libraries(goalc common Zydis compiler) target_link_libraries(goalc-simple common Zydis compiler) +target_link_libraries(build_level common Zydis compiler) diff --git a/goalc/build_level/collide/common/collide_common.h b/goalc/build_level/collide/common/collide_common.h new file mode 100644 index 00000000000..2ca484f1748 --- /dev/null +++ b/goalc/build_level/collide/common/collide_common.h @@ -0,0 +1,314 @@ +#pragma once +#include "common/common_types.h" +#include "common/math/Vector.h" + +struct CollideVertex { + float x, y, z; +}; + +namespace jak1 { +struct PatSurface { + enum class Mode { GROUND = 0, WALL = 1, OBSTACLE = 2, MAX_MODE = 3 }; + enum class Material { + STONE = 0, + ICE = 1, + QUICKSAND = 2, + WATERBOTTOM = 3, + TAR = 4, + SAND = 5, + WOOD = 6, + GRASS = 7, + PCMETAL = 8, + SNOW = 9, + DEEPSNOW = 10, + HOTCOALS = 11, + LAVA = 12, + CRWOOD = 13, + GRAVEL = 14, + DIRT = 15, + METAL = 16, + STRAW = 17, + TUBE = 18, + SWAMP = 19, + STOPPROJ = 20, + ROTATE = 21, + NEUTRAL = 22, + MAX_MATERIAL = 23 + }; + + enum class Event { + NONE = 0, + DEADLY = 1, + ENDLESSFALL = 2, + BURN = 3, + DEADLYUP = 4, + BURNUP = 5, + MELT = 6, + MAX_EVENT = 7, + }; + + void set_noentity(bool x) { + if (x) { + val |= (1 << 0); + } else { + val &= ~(1 << 0); + } + } + bool get_noentity() const { return val & (1 << 0); } + + void set_nocamera(bool x) { + if (x) { + val |= (1 << 1); + } else { + val &= ~(1 << 1); + } + } + bool get_nocamera() const { return val & (1 << 1); } + + void set_noedge(bool x) { + if (x) { + val |= (1 << 2); + } else { + val &= ~(1 << 2); + } + } + bool get_noedge() const { return val & (1 << 2); } + + void set_mode(Mode mode) { + val &= ~(0b111 << 3); + val |= ((u32)mode << 3); + } + Mode get_mode() const { return (Mode)(0b111 & (val >> 3)); } + + void set_material(Material mat) { + val &= ~(0b111111 << 6); + val |= ((u32)mat << 6); + } + Material get_material() const { return (Material)(0b111111 & (val >> 6)); } + + void set_nolineofsight(bool x) { + if (x) { + val |= (1 << 12); + } else { + val &= ~(1 << 12); + } + } + bool get_nolineofsight() const { return val & (1 << 12); } + + void set_event(Event ev) { + val &= ~(0b111111 << 14); + val |= ((u32)ev << 14); + } + Event get_event() const { return (Event)(0b111111 & (val >> 14)); } + + bool operator==(const PatSurface& other) const { return val == other.val; } + // bits 13, [15-31] are unused, or have unknown purpose. + u32 val = 0; +}; + +struct CollideFace { + math::Vector4f bsphere; + math::Vector3f v[3]; + PatSurface pat; +}; +} // namespace jak1 + +namespace jak2 { +struct PatSurface { + enum class Mode { GROUND = 0, WALL = 1, OBSTACLE = 2, HALFPIPE = 3, MAX_MODE = 4 }; + enum class Material { + NONE = 0, + ICE = 1, + QUICKSAND = 2, + WATERBOTTOM = 3, + TAR = 4, + SAND = 5, + WOOD = 6, + GRASS = 7, + PCMETAL = 8, + SNOW = 9, + DEEPSNOW = 10, + HOTCOALS = 11, + LAVA = 12, + CRWOOD = 13, + GRAVEL = 14, + DIRT = 15, + METAL = 16, + STRAW = 17, + TUBE = 18, + SWAMP = 19, + STOPPROJ = 20, + ROTATE = 21, + NEUTRAL = 22, + STONE = 23, + CRMETAL = 24, + CARPET = 25, + GRMETAL = 26, + SHMETAL = 27, + HDWOOD = 28, + MAX_MATERIAL = 29 + }; + + enum class Event { + NONE = 0, + DEADLY = 1, + ENDLESSFALL = 2, + BURN = 3, + DEADLYUP = 4, + BURNUP = 5, + MELT = 6, + SLIDE = 7, + LIP = 8, + LIPRAMP = 9, + SHOCK = 10, + SHOCKUP = 11, + HIDE = 12, + RAIL = 13, + SLIPPERY = 14, + MAX_EVENT = 15, + }; + + void set_noentity(bool x) { + if (x) { + val |= (1 << 0); + } else { + val &= ~(1 << 0); + } + } + bool get_noentity() const { return val & (1 << 0); } + + void set_nocamera(bool x) { + if (x) { + val |= (1 << 1); + } else { + val &= ~(1 << 1); + } + } + bool get_nocamera() const { return val & (1 << 1); } + + void set_noedge(bool x) { + if (x) { + val |= (1 << 2); + } else { + val &= ~(1 << 2); + } + } + bool get_noedge() const { return val & (1 << 2); } + + void set_nogrind(bool x) { + if (x) { + val |= (1 << 3); + } else { + val &= ~(1 << 3); + } + } + bool get_nogrind() const { return val & (1 << 3); } + + void set_nojak(bool x) { + if (x) { + val |= (1 << 4); + } else { + val &= ~(1 << 4); + } + } + bool get_nojak() const { return val & (1 << 4); } + + void set_noboard(bool x) { + if (x) { + val |= (1 << 5); + } else { + val &= ~(1 << 5); + } + } + bool get_noboard() const { return val & (1 << 5); } + + void set_nopilot(bool x) { + if (x) { + val |= (1 << 6); + } else { + val &= ~(1 << 6); + } + } + bool get_nopilot() const { return val & (1 << 6); } + + void set_mode(Mode mode) { + val &= ~(0b111 << 7); + val |= ((u32)mode << 7); + } + Mode get_mode() const { return (Mode)(0b111 & (val >> 7)); } + + void set_material(Material mat) { + val &= ~(0b111111 << 10); + val |= ((u32)mat << 10); + } + Material get_material() const { return (Material)(0b111111 & (val >> 10)); } + + void set_nolineofsight(bool x) { + if (x) { + val |= (1 << 16); + } else { + val &= ~(1 << 16); + } + } + bool get_nolineofsight() const { return val & (1 << 16); } + + void set_event(Event ev) { + val &= ~(0b111111 << 18); + val |= ((u32)ev << 18); + } + Event get_event() const { return (Event)(0b111111 & (val >> 18)); } + + void set_probe(bool x) { + if (x) { + val |= (1 << 24); + } else { + val &= ~(1 << 24); + } + } + bool get_probe() const { return val & (1 << 24); } + + void set_nomech(bool x) { + if (x) { + val |= (1 << 25); + } else { + val &= ~(1 << 25); + } + } + bool get_nomech() const { return val & (1 << 25); } + + void set_noproj(bool x) { + if (x) { + val |= (1 << 26); + } else { + val &= ~(1 << 26); + } + } + bool get_noproj() const { return val & (1 << 26); } + + void set_noendlessfall(bool x) { + if (x) { + val |= (1 << 27); + } else { + val &= ~(1 << 27); + } + } + bool get_noendlessfall() const { return val & (1 << 27); } + + void set_noprobe(bool x) { + if (x) { + val |= (1 << 28); + } else { + val &= ~(1 << 28); + } + } + bool get_noprobe() const { return val & (1 << 28); } + + bool operator==(const PatSurface& other) const { return val == other.val; } + u32 val = 0; +}; + +struct CollideFace { + math::Vector3f v[3]; + PatSurface pat; +}; +} // namespace jak2 diff --git a/goalc/build_level/collide_bvh.cpp b/goalc/build_level/collide/jak1/collide_bvh.cpp similarity index 94% rename from goalc/build_level/collide_bvh.cpp rename to goalc/build_level/collide/jak1/collide_bvh.cpp index abbab4531e2..8afaee51f3b 100644 --- a/goalc/build_level/collide_bvh.cpp +++ b/goalc/build_level/collide/jak1/collide_bvh.cpp @@ -27,7 +27,7 @@ constexpr int MAX_UNIQUE_VERTS_IN_FRAG = 128; */ struct CNode { std::vector child_nodes; - std::vector faces; + std::vector faces; math::Vector4f bsphere; }; @@ -101,13 +101,14 @@ void compute_my_bsphere_ritters(CNode& node) { * Split faces in two along a coordinate plane. * Will clear the input faces */ -void split_along_dim(std::vector& faces, +void split_along_dim(std::vector& faces, int dim, - std::vector* out0, - std::vector* out1) { - std::sort(faces.begin(), faces.end(), [=](const CollideFace& a, const CollideFace& b) { - return a.bsphere[dim] < b.bsphere[dim]; - }); + std::vector* out0, + std::vector* out1) { + std::sort(faces.begin(), faces.end(), + [=](const jak1::CollideFace& a, const jak1::CollideFace& b) { + return a.bsphere[dim] < b.bsphere[dim]; + }); lg::print("splitting with size: {}\n", faces.size()); size_t split_idx = faces.size() / 2; out0->insert(out0->end(), faces.begin(), faces.begin() + split_idx); @@ -278,7 +279,7 @@ void debug_stats(const CollideTree& tree) { } // namespace -CollideTree construct_collide_bvh(const std::vector& tris) { +CollideTree construct_collide_bvh(const std::vector& tris) { // part 1: build the tree Timer bvh_timer; lg::info("Building collide bvh from {} triangles", tris.size()); diff --git a/goalc/build_level/collide_bvh.h b/goalc/build_level/collide/jak1/collide_bvh.h similarity index 80% rename from goalc/build_level/collide_bvh.h rename to goalc/build_level/collide/jak1/collide_bvh.h index dca6b47e0d2..15261b5a874 100644 --- a/goalc/build_level/collide_bvh.h +++ b/goalc/build_level/collide/jak1/collide_bvh.h @@ -2,7 +2,7 @@ #include -#include "goalc/build_level/collide_common.h" +#include "goalc/build_level/collide/common/collide_common.h" // requirements: // max depth of 3 (maybe?) @@ -19,7 +19,7 @@ struct DrawNode { struct CollideFrag { math::Vector4f bsphere; - std::vector faces; + std::vector faces; }; struct DrawableInlineArrayNode { @@ -36,5 +36,5 @@ struct CollideTree { DrawableInlineArrayCollideFrag frags; }; -CollideTree construct_collide_bvh(const std::vector& tris); +CollideTree construct_collide_bvh(const std::vector& tris); } // namespace collide diff --git a/goalc/build_level/collide_drawable.cpp b/goalc/build_level/collide/jak1/collide_drawable.cpp similarity index 99% rename from goalc/build_level/collide_drawable.cpp rename to goalc/build_level/collide/jak1/collide_drawable.cpp index 2ef770a3948..d32f1e6ca17 100644 --- a/goalc/build_level/collide_drawable.cpp +++ b/goalc/build_level/collide/jak1/collide_drawable.cpp @@ -68,7 +68,7 @@ :size-assert #x44 */ -size_t generate_pat_array(DataObjectGenerator& gen, const std::vector& pats) { +size_t generate_pat_array(DataObjectGenerator& gen, const std::vector& pats) { gen.align_to_basic(); size_t result = gen.current_offset_bytes(); for (auto& pat : pats) { diff --git a/goalc/build_level/collide_drawable.h b/goalc/build_level/collide/jak1/collide_drawable.h similarity index 71% rename from goalc/build_level/collide_drawable.h rename to goalc/build_level/collide/jak1/collide_drawable.h index 5804dbb5c18..a9a637a6757 100644 --- a/goalc/build_level/collide_drawable.h +++ b/goalc/build_level/collide/jak1/collide_drawable.h @@ -1,7 +1,7 @@ #pragma once -#include "goalc/build_level/collide_bvh.h" -#include "goalc/build_level/collide_pack.h" +#include "collide_bvh.h" +#include "collide_pack.h" class DataObjectGenerator; diff --git a/goalc/build_level/collide_pack.cpp b/goalc/build_level/collide/jak1/collide_pack.cpp similarity index 97% rename from goalc/build_level/collide_pack.cpp rename to goalc/build_level/collide/jak1/collide_pack.cpp index 04e1b115fca..9e864896a73 100644 --- a/goalc/build_level/collide_pack.cpp +++ b/goalc/build_level/collide/jak1/collide_pack.cpp @@ -91,7 +91,7 @@ PackedU16Verts pack_verts_to_u16(const std::vector& input) { } struct PatSurfaceHash { - size_t operator()(const PatSurface& in) const { return std::hash()(in.val); } + size_t operator()(const jak1::PatSurface& in) const { return std::hash()(in.val); } }; /*! @@ -99,10 +99,10 @@ struct PatSurfaceHash { * There's a pat "palette" with up 255 unique pats. */ struct PatMap { - std::unordered_map map; - std::vector pats; + std::unordered_map map; + std::vector pats; - u32 add_pat(PatSurface pat) { + u32 add_pat(jak1::PatSurface pat) { const auto& lookup = map.find(pat); if (lookup == map.end()) { u32 new_idx = pats.size(); diff --git a/goalc/build_level/collide_pack.h b/goalc/build_level/collide/jak1/collide_pack.h similarity index 87% rename from goalc/build_level/collide_pack.h rename to goalc/build_level/collide/jak1/collide_pack.h index 316b56e9f45..b34068ad1c0 100644 --- a/goalc/build_level/collide_pack.h +++ b/goalc/build_level/collide/jak1/collide_pack.h @@ -1,6 +1,6 @@ #pragma once -#include "goalc/build_level/collide_bvh.h" +#include "collide_bvh.h" struct CollideFragMeshData { math::Vector4f bsphere; // not part of the collide frag, but is part of the drawable wrapping it @@ -15,7 +15,7 @@ struct CollideFragMeshData { struct CollideFragMeshDataArray { std::vector packed_frag_data; - std::vector pats; + std::vector pats; }; CollideFragMeshDataArray pack_collide_frags(const std::vector& frag_data); \ No newline at end of file diff --git a/goalc/build_level/collide/jak2/collide.cpp b/goalc/build_level/collide/jak2/collide.cpp new file mode 100644 index 00000000000..00010814a28 --- /dev/null +++ b/goalc/build_level/collide/jak2/collide.cpp @@ -0,0 +1,1158 @@ +#include "collide.h" + +#include +#include +#include +#include + +#include "common/log/log.h" +#include "common/util/Assert.h" + +#include "goalc/data_compiler/DataObjectGenerator.h" + +/*! + * An axis-aligned bounding box + */ +struct BoundingBox { + math::Vector3f min = math::Vector3f::zero(); + math::Vector3f max = math::Vector3f::zero(); +}; + +/*! + * See if "axis" is a separating axis for a bounding-box to triangle intersection test. + * The bounding box is centered at the origin. + * Return true if the axis is a separating axis. + */ +bool separating_axis_test(const math::Vector3f& bbox_half_side_length, + const math::Vector3f& axis, + const math::Vector3f& a, + const math::Vector3f& b, + const math::Vector3f& c) { + // project triangle to axis + const float pa = axis.dot(a); + const float pb = axis.dot(b); + const float pc = axis.dot(c); + + // project box to axis. + const float pbox_plus = std::abs(axis[0] * bbox_half_side_length[0]) + + std::abs(axis[1] * bbox_half_side_length[1]) + + std::abs(axis[2] * bbox_half_side_length[2]); + const float pbox_minus = -pbox_plus; + + const float ptri_max = std::max(std::max(pa, pb), pc); + const float ptri_min = std::min(std::min(pa, pb), pc); + + if (ptri_max < pbox_minus) { + return true; + } + + if (ptri_min > pbox_plus) { + return true; + } + + // there must be overlap. + return false; +} + +/*! + * Check to see if a triangle intersects an axis-aligned box. + */ +bool triangle_bounding_box(const BoundingBox& bbox_w, + const math::Vector3f& a_w, + const math::Vector3f& b_w, + const math::Vector3f& c_w) { + // first, translate everything so the center of the bounding box is at the origin + const math::Vector3f box_center = (bbox_w.max + bbox_w.min) / 2.f; + + const math::Vector3f half_side_length = bbox_w.max - box_center; + const math::Vector3f a = a_w - box_center; + const math::Vector3f b = b_w - box_center; + const math::Vector3f c = c_w - box_center; + + // the separating axis says that if two convex shapes don't intersect, you can project them onto a + // separating axis (line) and their projections don't overlap. This axis is either a face normal, + // or a cross-product of edges from each shape. + + // To check intersection, we'll check each possible separating axis - if any are valid, then the + // shapes don't intersect. + + // First, check the face normals of the box. This check is special-cased for speed - most + // calls to this function will not have intersection, one of these will be a valid separating + // axis. + + // find the elementwise min/max of triangle vertices + const math::Vector3f tri_min = a.min(b.min(c)); + const math::Vector3f tri_max = a.max(b.max(c)); + + // check face normals of the box + for (int axis = 0; axis < 3; axis++) { + if (tri_max[axis] < -half_side_length[axis]) { + return false; + } + if (tri_min[axis] > half_side_length[axis]) { + return false; + } + } + + // check the face normal of the tri + const math::Vector3f tri_normal = (b - a).cross(c - a); + if (separating_axis_test(half_side_length, tri_normal, a, b, c)) { + return false; + } + + // all three edges of the triangle + const math::Vector3f tri_edges[3] = { + a - b, + a - c, + c - b, + }; + + // check each triangle edge + for (auto tri_edge : tri_edges) { + // against each box edge + for (int box_axis = 0; box_axis < 3; box_axis++) { + const math::Vector3f axis = math::Vector3f::unit(box_axis).cross(tri_edge); + if (separating_axis_test(half_side_length, axis, a, b, c)) { + return false; + } + } + } + + // all possible separating axes failed, there is intersection. + return true; +} + +bool bounding_box_bounding_box(const BoundingBox& a, const BoundingBox& b) { + for (int i = 0; i < 3; i++) { + if (a.min[i] > b.max[i]) { + return false; + } + if (a.max[i] < b.min[i]) { + return false; + } + } + 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. + */ +CollideHash construct_collide_hash(const std::vector& tris) { + std::vector jak2_tris; + jak2_tris.reserve(tris.size()); + + for (const auto& tri : tris) { + auto& new_tri = jak2_tris.emplace_back(); + for (int i = 0; i < 3; i++) { + new_tri.v[i] = tri.v[i]; + new_tri.pat = jak2_pat(tri.pat); + } + } + + return construct_collide_hash(jak2_tris); +} + +/*! + * Utility to build a bounding box. + * If no points are added, the box is set to 0. + */ +struct BBoxBuilder { + bool added_one = false; + BoundingBox box; + + // modify box to include this point. + void add_pt(const math::Vector3f& pt) { + if (added_one) { + box.min.min_in_place(pt); + box.max.max_in_place(pt); + } else { + box.min = pt; + box.max = pt; + } + added_one = true; + } + + // modify box to include this tri. + void add_tri(const jak2::CollideFace& tri) { + for (const auto& v : tri.v) { + add_pt(v); + } + } + + void add_box(const BoundingBox& box) { + add_pt(box.min); + add_pt(box.max); + } +}; + +/*! + * Given two bounding boxes, compute the volume of their intersection. + */ +float overlap_volume(const BoundingBox& a, const BoundingBox& b) { + BoundingBox intersection; + for (int i = 0; i < 3; i++) { + intersection.min[i] = std::max(a.min[i], b.min[i]); + intersection.max[i] = std::min(a.max[i], b.max[i]); + } + const math::Vector3f size = intersection.max - intersection.min; + float ret = 1.f; + for (int i = 0; i < 3; i++) { + if (size[i] <= 0) { + return 0; + } + ret *= size[i]; + } + return ret; +} + +/*! + * A portion of a mesh, used in the fragment_mesh function. + */ +struct Frag { + std::vector tri_indices; +}; + +/*! + * Statistics about a Frag, used for a few steps below. + */ +struct FragStats { + BoundingBox bbox; + math::Vector3f average_vertex_position; + math::Vector3f median_vertex_position; +}; + +/*! + * Find bounding box and average position for the triangles selected by indices. + */ +FragStats compute_frag_stats(const std::vector& tris, + const std::vector& indices) { + ASSERT(!tris.empty()); + ASSERT(!indices.empty()); + + const float inv_vert_count = 1.f / (indices.size() * 3); + + FragStats ret; + BBoxBuilder bbox; + ret.average_vertex_position.set_zero(); + + for (auto idx : indices) { + for (const auto& vtx : tris[idx].v) { + bbox.add_pt(vtx); + ret.average_vertex_position += vtx * inv_vert_count; + } + } + + for (int i = 0; i < 3; i++) { + std::vector vx; + vx.reserve(tris.size() * 3); + for (auto idx : indices) { + for (const auto& vtx : tris[idx].v) { + vx.push_back(vtx[i]); + } + } + std::sort(vx.begin(), vx.end()); + ret.median_vertex_position[i] = vx[vx.size() / 2]; + } + + ret.bbox = bbox.box; + return ret; +} + +struct VectorHash { + size_t operator()(const math::Vector3f& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +struct CVertexHash { + size_t operator()(const math::Vector& in) const { + return std::hash()(in.x()) ^ std::hash()(in.y()) ^ std::hash()(in.z()); + } +}; + +/*! + * How many unique vertices are there in this frag? + * (currently using float equality, however, a smarter version could look at quantized vertices) + */ +int unique_vertex_count(const Frag& frag, const std::vector& tris) { + std::unordered_set vmap; + for (auto i : frag.tri_indices) { + for (const auto& v : tris[i].v) { + vmap.insert(v); + } + } + return (int)vmap.size(); +} + +/*! + * Is this a frag that we can use in the game? + */ +bool frag_is_valid_for_packing(const Frag& frag, + const FragStats& stats, + const std::vector& tris) { + if (frag.tri_indices.size() >= UINT8_MAX) { + // the fragment has too many triangles. I think this can actually be UINT8_MAX and we + // just put 0 as the size. However, this is confusing so let's just make the max 1 less + // for now. + return false; + } + + // there is a limit to the size of a fragment: + // the -4096 removes 1 meter from the end, just to make sure that order-of-operations rounding + // differences doesn't move a vertex outside the grid + const float kMaxFragSize = UINT16_MAX * 16 - 4096; + for (int i = 0; i < 3; i++) { + if (stats.bbox.max[i] - stats.bbox.min[i] >= kMaxFragSize) { + return false; + } + } + + // there is a limit to the number of unique vertices + if (unique_vertex_count(frag, tris) >= UINT8_MAX) { + return false; + } + + return true; +} + +/*! + * A way to split the fragment along a plane + */ +struct FragSplit { + // a plane that intersects the specified axis at the value. (and is normal to this axis) + int axis = 0; + float value = 0; +}; + +/*! + * Info about a split + */ +struct SplitStats { + // how many tris on each side + int tri_count[2] = {0, 0}; + + // the bounding box of those tris. only valid if nonzero tris. + BoundingBox bboxes[2]; + + float overlap_volume = 0; + float imbalance = 0; + bool had_zero = false; +}; + +SplitStats compute_split_stats(const Frag& frag, + const std::vector& tris, + const FragSplit& split) { + SplitStats stats; + BBoxBuilder bbox[2]; + + for (auto i : frag.tri_indices) { + const auto& tri = tris[i]; + const math::Vector3f average_pt = (tri.v[0] + tri.v[1] + tri.v[2]) / 3.f; + const int out_bin = (average_pt[split.axis] > split.value) ? 1 : 0; + bbox[out_bin].add_tri(tri); + stats.tri_count[out_bin]++; + } + stats.bboxes[0] = bbox[0].box; + stats.bboxes[1] = bbox[1].box; + + if (stats.tri_count[0] && stats.tri_count[1]) { + stats.overlap_volume = overlap_volume(stats.bboxes[0], stats.bboxes[1]); + float max_count = std::max(stats.tri_count[1], stats.tri_count[0]); + float min_count = std::min(stats.tri_count[1], stats.tri_count[0]); + stats.imbalance = max_count / min_count; + stats.had_zero = false; + } else { + stats.overlap_volume = 0; + stats.imbalance = 0; + stats.had_zero = true; + } + return stats; +} + +int idx_of_max(float a, float b, float c) { + if (a > b) { + if (a > c) { + return 0; + } else { + // a > b, c > a. + return 2; + } + } else { + if (b > c) { + return 1; + } else { + return 2; + } + } +} + +FragSplit pick_best_frag_split(const Frag& frag, + const FragStats& stats, + const std::vector& tris) { + // this is the tricky part. + + // I think the most important thing about splitting is that we should try to minimize overlapping + // fragments in the final mesh. Overlapping fragments means that we'll need more space for + // buckets, and the engine will need to check more fragments. + + // Based on what I learned with Jak 1, we also want to avoid: + // - fragments with bad (large) aspect ratio. Although the Jak 2 code is likely _much_ better at + // this case because it uses a box instead of a sphere, I think that we'll struggle to split + // up these fragments at the later levels. + + math::Vector3f box_size = stats.bbox.max - stats.bbox.min; + float min_box_size = box_size[0]; + float max_box_size = box_size[0]; + int max_idx = 0; + for (int i = 0; i < 3; i++) { + if (box_size[i] > max_box_size) { + max_idx = i; + max_box_size = box_size[i]; + } + min_box_size = std::min(box_size[i], min_box_size); + } + + const float aspect = max_box_size / min_box_size; + + FragSplit splits[3]; + SplitStats split_stats[3]; + + for (int i = 0; i < 3; i++) { + splits[i].axis = i; + splits[i].value = stats.average_vertex_position[i]; + split_stats[i] = compute_split_stats(frag, tris, splits[i]); + } + + if (aspect > 25) { + if (split_stats[max_idx].imbalance < 4) { + printf( + "pick best frag split splitting a frag of size %d due to bad aspect (%f), with imbalance " + "%f\n", + (int)frag.tri_indices.size(), aspect, split_stats[max_idx].imbalance); + return splits[max_idx]; + } else { + printf( + "weird: there's a bad aspect frag (%f, %f), but splitting along the worst axis causes " + "imbalance %f.\n", + max_box_size / 4096.f, min_box_size / 4096.f, split_stats[max_idx].imbalance); + } + } + + float scores[3]; + for (int i = 0; i < 3; i++) { + if (split_stats[i].had_zero) { + scores[i] = -std::numeric_limits::max(); + } else { + scores[i] = -split_stats[i].overlap_volume; + } + } + + return splits[idx_of_max(scores[0], scores[1], scores[2])]; +} + +Frag add_all_to_frag(const std::vector& tris) { + ASSERT(!tris.empty()); + + Frag ret; + ret.tri_indices.reserve(tris.size()); + for (size_t i = 0; i < tris.size(); i++) { + ret.tri_indices.push_back(i); + } + return ret; +} + +void split_frag(const Frag& in, + const FragSplit& split, + const std::vector& tris, + Frag* out_a, + Frag* out_b) { + for (auto i : in.tri_indices) { + const auto& tri = tris[i]; + const math::Vector3f average_pt = (tri.v[0] + tri.v[1] + tri.v[2]) / 3.f; + if (average_pt[split.axis] > split.value) { + out_a->tri_indices.push_back(i); + } else { + out_b->tri_indices.push_back(i); + } + } +} + +std::vector fragment_mesh(const std::vector& tris) { + struct FragAndStats { + Frag f; + FragStats s; + }; + + auto initial_frag = add_all_to_frag(tris); + auto initial_stats = compute_frag_stats(tris, initial_frag.tri_indices); + if (frag_is_valid_for_packing(initial_frag, initial_stats, tris)) { + printf("initial is good!\n"); + printf("%s\n%s\n\n", initial_stats.bbox.min.to_string_aligned().c_str(), + initial_stats.bbox.max.to_string_aligned().c_str()); + return {initial_frag}; + } + + // split up all "too big" frags until they are good. + std::vector too_big_frags = {{initial_frag, initial_stats}}; + std::vector good_frags; + + while (!too_big_frags.empty()) { + printf("sizes %zu %zu\n", too_big_frags.size(), good_frags.size()); + auto& back = too_big_frags.back(); + + // split it! + FragAndStats ab[2]; + auto split = pick_best_frag_split(back.f, back.s, tris); + split_frag(back.f, split, tris, &ab[0].f, &ab[1].f); + + too_big_frags.pop_back(); // invalidate back. + + // check if split frags are good or not. + for (auto& fs : ab) { + fs.s = compute_frag_stats(tris, fs.f.tri_indices); + if (frag_is_valid_for_packing(fs.f, fs.s, tris)) { + good_frags.push_back(std::move(fs.f)); + } else { + too_big_frags.push_back(std::move(fs)); + } + } + } + return good_frags; +} + +struct VectorIntHash { + size_t operator()(const std::vector& in) const { + size_t ret = 0; + for (size_t i = 0; i < in.size(); i++) { + ret ^= std::hash()(i ^ in[i]); + } + return ret; + } +}; + +CollideHash build_grid_for_main_hash(std::vector&& frags) { + lg::info("Creating main hash"); + CollideHash result; + BBoxBuilder bbox; + for (const auto& frag : frags) { + bbox.add_pt(frag.bbox_min_corner); + bbox.add_pt(frag.bbox_max_corner); + } + + const math::Vector3f box_size = bbox.box.max - bbox.box.min; + + // grid the box. It _looks_ like the village1 level just picks dims that get you closest to 10000 + // for the cell size. + constexpr float kTargetCellSize = 30000; + + int grid_dimension[3] = {(int)(box_size[0] / kTargetCellSize), + (int)(box_size[1] / kTargetCellSize), + (int)(box_size[2] / kTargetCellSize)}; + for (auto& x : grid_dimension) { + if (x >= UINT8_MAX) { + x = UINT8_MAX; + } + } + lg::info("Size is {}x{}x{} (total {})\n", grid_dimension[0], grid_dimension[1], grid_dimension[2], + grid_dimension[0] * grid_dimension[1] * grid_dimension[2]); + const math::Vector3f grid_cell_size(box_size[0] / grid_dimension[0], + box_size[1] / grid_dimension[1], + box_size[2] / grid_dimension[2]); + + std::vector> frags_in_cells; + + // debug + std::vector debug_found_flags(frags.size(), false); + int debug_intersect_count = 0; + + // yzx order to match game + for (int yi = 0; yi < grid_dimension[1]; yi++) { + for (int zi = 0; zi < grid_dimension[2]; zi++) { + for (int xi = 0; xi < grid_dimension[0]; xi++) { + auto& cell_list = frags_in_cells.emplace_back(); + + BoundingBox cell; + cell.min = + math::Vector3f(xi * grid_cell_size[0], yi * grid_cell_size[1], zi * grid_cell_size[2]) + + bbox.box.min; + cell.max = cell.min + grid_cell_size; + + for (size_t fi = 0; fi < frags.size(); fi++) { + const auto& frag = frags[fi]; + if (bounding_box_bounding_box(cell, {frag.bbox_min_corner, frag.bbox_max_corner})) { + debug_found_flags[fi] = true; + debug_intersect_count++; + cell_list.push_back(fi); + } + } + + std::sort(cell_list.begin(), cell_list.end()); + }; + } + } + + lg::info("Index data size is {}, deduplicating", debug_intersect_count); + + std::unordered_map, size_t, VectorIntHash> index_map; + for (const auto& cell_list : frags_in_cells) { + auto& bucket = result.buckets.emplace_back(); + bucket.count = cell_list.size(); + + const auto& it = index_map.find(cell_list); + if (it == index_map.end()) { + bucket.index = result.index_array.size(); + index_map[cell_list] = bucket.index; + for (auto x : cell_list) { + result.index_array.push_back(x); + } + } else { + bucket.index = it->second; + } + } + + lg::info("Index array size is {} in the end", result.index_array.size()); + if (result.index_array.size() > UINT16_MAX) { + printf("index array is too big: %d\n", (int)result.index_array.size()); + ASSERT_NOT_REACHED(); + } + + int unique_found = 0; + for (auto x : debug_found_flags) { + if (x) { + unique_found++; + } + } + + printf("frag find counts: %d %d %d\n", unique_found, (int)debug_found_flags.size(), + debug_intersect_count); + if (unique_found != (int)debug_found_flags.size()) { + printf(" --- !!! %d frags disappeared\n", (int)debug_found_flags.size() - unique_found); + } + + // for (auto& list : frags_in_cells) { + // auto& bucket = result.buckets.emplace_back(); + // bucket.index = result.index_array.size(); + // bucket.count = list.size(); + // for (auto x : list) { + // result.index_array.push_back(x); + // } + // } + + result.grid_step = grid_cell_size; + result.axis_scale = + math::Vector3f(1.f / grid_cell_size[0], 1.f / grid_cell_size[1], 1.f / grid_cell_size[2]); + result.bbox_min_corner = bbox.box.min; + result.bbox_min_corner_i = bbox.box.min.cast(); + result.bbox_max_corner_i = bbox.box.max.cast(); + result.qwc_id_bits = (frags.size() + 127) / 128; + result.fragments = std::move(frags); + + for (int i = 0; i < 3; i++) { + result.dimension_array[i] = grid_dimension[i]; + } + return result; +} + +/*! + * Build a CollideFragment by "hashing" a list of triangles + */ +CollideFragment build_grid_for_frag(const std::vector& tris, const Frag& frag) { + CollideFragment result; + + // find the bounding box + BBoxBuilder bbox; + for (auto i : frag.tri_indices) { + bbox.add_tri(tris[i]); + } + + // build vertex, poly, pat tables: + std::vector> vertices; + std::vector polys; + std::vector pats; + + std::unordered_map, size_t, CVertexHash> vertex_to_vertex_array_index; + std::unordered_map pat_to_pat_array_index; + + for (auto ti : frag.tri_indices) { + const auto& input_tri = tris[ti]; + auto& poly = polys.emplace_back(); + + // add pat: + auto pat_it = pat_to_pat_array_index.find(input_tri.pat.val); + if (pat_it == pat_to_pat_array_index.end()) { + pat_to_pat_array_index[input_tri.pat.val] = pats.size(); + ASSERT(pats.size() < UINT8_MAX); + poly.pat_index = pats.size(); + pats.push_back(input_tri.pat); + } else { + poly.pat_index = pat_it->second; + } + + // add vertices + for (int i = 0; i < 3; i++) { + const math::Vector3f vert_f = (input_tri.v[i] - bbox.box.min) / 16.f; + for (int j = 0; j < 3; j++) { + ASSERT(vert_f[j] >= 0 && vert_f[j] < UINT16_MAX); + } + const auto vert_i = vert_f.cast(); + const auto& it = vertex_to_vertex_array_index.find(vert_i); + if (it == vertex_to_vertex_array_index.end()) { + vertex_to_vertex_array_index[vert_i] = vertex_to_vertex_array_index.size(); + ASSERT(vertex_to_vertex_array_index.size() < UINT8_MAX); + poly.vertex_index[i] = vertices.size(); + vertices.push_back(vert_i); + } else { + poly.vertex_index[i] = it->second; + } + } + } + + // grid the box. We can have only 256 cells, so we take a 1x1 grid and split it in half 8 times. + // TODO: there are probably smarter ways to do this. + math::Vector3f grid_cell_size = bbox.box.max - bbox.box.min; + int grid_dimension[3] = {1, 1, 1}; + for (int i = 0; i < 8; i++) { + int split_axis = idx_of_max(grid_cell_size[0], grid_cell_size[1], grid_cell_size[2]); + grid_dimension[split_axis] *= 2; + grid_cell_size[split_axis] /= 2; + } + ASSERT(grid_dimension[0] * grid_dimension[1] * grid_dimension[2] == 256); + + // per-cell, a list of polys that intersect it. + std::vector> polys_in_cells; + + // debug + std::vector debug_found_flags(frag.tri_indices.size(), false); + int debug_intersect_count = 0; + + // yzx order to match game + for (int yi = 0; yi < grid_dimension[1]; yi++) { + for (int zi = 0; zi < grid_dimension[2]; zi++) { + for (int xi = 0; xi < grid_dimension[0]; xi++) { + auto& cell_list = polys_in_cells.emplace_back(); + + BoundingBox cell; + cell.min = + math::Vector3f(xi * grid_cell_size[0], yi * grid_cell_size[1], zi * grid_cell_size[2]) + + bbox.box.min; + cell.max = cell.min + grid_cell_size; + + for (size_t ti = 0; ti < frag.tri_indices.size(); ti++) { + const auto& tri = tris[frag.tri_indices[ti]]; + if (triangle_bounding_box(cell, tri.v[0], tri.v[1], tri.v[2])) { + debug_found_flags[ti] = true; + debug_intersect_count++; + cell_list.push_back(ti); + } + } + + std::sort(cell_list.begin(), cell_list.end()); + }; + } + } + + // TODO: could dedup buckets here. + int unique_found = 0; + for (auto x : debug_found_flags) { + if (x) { + unique_found++; + } + } + + // printf("find counts: %d %d %d\n", unique_found, (int)debug_found_flags.size(), + // debug_intersect_count); + ASSERT(debug_intersect_count < INT16_MAX); // not really sure what to do if this happens... + if (unique_found != (int)debug_found_flags.size()) { + printf(" --- !!! %d triangles disappeared\n", (int)debug_found_flags.size() - unique_found); + } + + result.pat_array = std::move(pats); + for (auto& list : polys_in_cells) { + auto& bucket = result.buckets.emplace_back(); + bucket.index = result.index_array.size(); + bucket.count = list.size(); + for (auto x : list) { + result.index_array.push_back(x); + } + } + result.poly_array = std::move(polys); + for (auto x : vertices) { + auto& v = result.vert_array.emplace_back(); + v.position[0] = x.x(); + v.position[1] = x.y(); + v.position[2] = x.z(); + } + + result.grid_step = grid_cell_size; + result.axis_scale = + math::Vector3f(1.f / grid_cell_size[0], 1.f / grid_cell_size[1], 1.f / grid_cell_size[2]); + result.bbox_min_corner = bbox.box.min; + result.bbox_max_corner = bbox.box.max; + result.bbox_min_corner_i = bbox.box.min.cast(); + result.bbox_max_corner_i = bbox.box.max.cast(); + + // bsphere: + math::Vector3f mid = (result.bbox_max_corner + result.bbox_min_corner) / 2.f; + math::Vector3f size = (result.bbox_max_corner - result.bbox_min_corner) / 2.f; + const float radius = size.length(); + result.bsphere = math::Vector4f(mid.x(), mid.y(), mid.z(), radius); + + for (int i = 0; i < 3; i++) { + result.dimension_array[i] = grid_dimension[i]; + } + return result; +} + +CollideHash construct_collide_hash(const std::vector& tris) { + CollideHash collide_hash; + + std::vector frags = fragment_mesh(tris); + std::vector hashed_frags; + for (auto& frag : frags) { + hashed_frags.push_back(build_grid_for_frag(tris, frag)); + } + + // hash tris in frags + // hash frags + // ?? + return build_grid_for_main_hash(std::move(hashed_frags)); +} + +size_t add_pod_to_object_file(DataObjectGenerator& gen, + const u8* in, + size_t size_bytes, + size_t align_bytes) { + const size_t align_words = (align_bytes + 3) / 4; + gen.align(align_words); + const size_t ret = gen.current_offset_bytes(); + + const size_t full_words = size_bytes / 4; + size_t bytes = 0; + for (size_t word = 0; word < full_words; word++) { + u32 data = 0; + memcpy(&data, in + 4 * word, 4); + gen.add_word(data); + bytes += 4; + } + + u8 remainder[4] = {0, 0, 0, 0}; + int i = 0; + while (bytes < size_bytes) { + remainder[i++] = in[bytes++]; + } + u32 last; + memcpy(&last, remainder, 4); + gen.add_word(last); + return ret; +} + +template +size_t add_pod_vector_to_object_file(DataObjectGenerator& gen, const std::vector& data) { + return add_pod_to_object_file(gen, (const u8*)data.data(), data.size() * sizeof(T), sizeof(T)); +} + +size_t add_to_object_file(const CollideFragment& frag, DataObjectGenerator& gen) { + // PAT ARRAY + static_assert(sizeof(jak2::PatSurface) == sizeof(u32)); + auto pat_array = add_pod_vector_to_object_file(gen, frag.pat_array); + + // Bucket ARRAY + static_assert(sizeof(CollideBucket) == sizeof(u32)); + auto bucket_array = add_pod_vector_to_object_file(gen, frag.buckets); + + // Poly ARRAY + static_assert(sizeof(CollideFragmentPoly) == sizeof(u32)); + auto poly_array = add_pod_vector_to_object_file(gen, frag.poly_array); + + // Vert ARRAY + static_assert(sizeof(CollideFragmentVertex) == 6); + gen.align(4); + auto vert_array = add_pod_vector_to_object_file(gen, frag.vert_array); + + // Index ARRAY + auto index_array = add_pod_vector_to_object_file(gen, frag.index_array); + + // collide-hash-fragment + gen.align_to_basic(); + gen.add_type_tag("collide-hash-fragment"); // 0 + size_t result = gen.current_offset_bytes(); + + // ((num-buckets uint16 :offset 4) + // (num-indices uint16 :offset 6) + const u32 bucket_index_word = (((u32)frag.index_array.size()) << 16) | ((u32)frag.buckets.size()); + gen.add_word(bucket_index_word); // 4 + + // (pat-array uint32 :offset 8) + gen.link_word_to_byte(gen.add_word(0), pat_array); // 8 + + // (bucket-array uint32 :offset 12) + gen.link_word_to_byte(gen.add_word(0), bucket_array); // 12 + + // bsphere of drawable + gen.add_word_float(frag.bsphere.x()); // 16 + gen.add_word_float(frag.bsphere.y()); // 20 + gen.add_word_float(frag.bsphere.z()); // 24 + gen.add_word_float(frag.bsphere.w()); // 28 + + // (grid-step vector :inline :offset-assert 32) + gen.add_word_float(frag.grid_step.x()); // 32 + gen.add_word_float(frag.grid_step.y()); // 36 + gen.add_word_float(frag.grid_step.z()); // 40 + + // (dimension-array uint32 4 :offset 44) + u32 dim_array = 0; + dim_array |= (frag.dimension_array[0]); + dim_array |= ((frag.dimension_array[1]) << 8); + dim_array |= ((frag.dimension_array[2]) << 16); + gen.add_word(dim_array); // 44 + + // (bbox bounding-box :inline :offset-assert 48) + gen.add_word_float(frag.bbox_min_corner.x()); // 48 + gen.add_word_float(frag.bbox_min_corner.y()); // 52 + gen.add_word_float(frag.bbox_min_corner.z()); // 56 + + // (num-verts uint16 :offset 60) + // (num-polys uint8 :offset 62) + // (poly-count uint8 :offset 63) + u32 counts_word = 0; + ASSERT(frag.vert_array.size() < UINT16_MAX); + counts_word |= (frag.vert_array.size()); + ASSERT(frag.poly_array.size() < UINT8_MAX); + counts_word |= (frag.poly_array.size() << 16); + counts_word |= (frag.poly_array.size() << 24); + gen.add_word(counts_word); // 60 + + // (axis-scale vector :inline :offset 64) + gen.add_word_float(frag.axis_scale.x()); // 64 + gen.add_word_float(frag.axis_scale.y()); // 68 + gen.add_word_float(frag.axis_scale.z()); // 72 + + // (poly-array uint32 :offset 76) + gen.link_word_to_byte(gen.add_word(0), poly_array); // 76 + + // (bbox4w bounding-box4w :inline :offset-assert 80) + gen.add_word(frag.bbox_min_corner_i.x()); // 80 + gen.add_word(frag.bbox_min_corner_i.y()); // 84 + gen.add_word(frag.bbox_min_corner_i.z()); // 88 + + // (vert-array uint32 :offset 92) + gen.link_word_to_byte(gen.add_word(0), vert_array); // 92 + + gen.add_word(frag.bbox_max_corner_i.x()); // 96 + gen.add_word(frag.bbox_max_corner_i.y()); // 100 + gen.add_word(frag.bbox_max_corner_i.z()); // 104 + + // (index-array uint32 :offset 108) + gen.link_word_to_byte(gen.add_word(0), index_array); + + // (avg-extents vector :inline :offset 80) + // (stats collide-hash-fragment-stats :inline :offset 60) + return result; +} + +size_t add_to_object_file(const CollideHash& hash, DataObjectGenerator& gen) { + std::vector frags; + for (auto& frag : hash.fragments) { + frags.push_back(add_to_object_file(frag, gen)); + } + + auto buckets = add_pod_vector_to_object_file(gen, hash.buckets); + + // create the item array. + auto item_array = gen.current_offset_bytes(); + for (auto& x : hash.index_array) { + gen.add_word(x); + gen.link_word_to_byte(gen.add_word(0), frags.at(x)); + } + + gen.align_to_basic(); + gen.add_type_tag("collide-hash"); // 0 + size_t result = gen.current_offset_bytes(); + + //((num-ids uint16 :offset 4) + // (id-count uint16 :offset 6) + u32 ids_word = 0; + ids_word |= hash.fragments.size(); + ids_word |= (hash.fragments.size() << 16); + gen.add_word(ids_word); // 4 + + // (num-buckets uint32 :offset 8) + gen.add_word(hash.buckets.size()); // 8 + + // (qwc-id-bits uint32 :offset 12) + gen.add_word(hash.qwc_id_bits); // 12 + + // (grid-step vector :inline :offset 16) + gen.add_word_float(hash.grid_step.x()); // 16 + gen.add_word_float(hash.grid_step.y()); // 20 + gen.add_word_float(hash.grid_step.z()); // 24 + gen.add_word_float(1.f); // 28 + + // (bbox bounding-box :inline :offset-assert 32) + gen.add_word_float(hash.bbox_min_corner.x()); // 32 + gen.add_word_float(hash.bbox_min_corner.y()); // 36 + gen.add_word_float(hash.bbox_min_corner.z()); // 40 + + // (bucket-array uint32 :offset 44) + gen.link_word_to_byte(gen.add_word(0), buckets); // 44 + + // (axis-scale vector :inline :offset 48) + gen.add_word_float(hash.axis_scale.x()); // 48 + gen.add_word_float(hash.axis_scale.y()); // 52 + gen.add_word_float(hash.axis_scale.z()); // 56 + + // (item-array (inline-array collide-hash-item) :offset 60 :score 1) + gen.link_word_to_byte(gen.add_word(0), item_array); // 60 + + // (bbox4w bounding-box4w :inline :offset-assert 64) + gen.add_word(hash.bbox_min_corner_i.x()); // 64 + gen.add_word(hash.bbox_min_corner_i.y()); // 68 + gen.add_word(hash.bbox_min_corner_i.z()); // 72 + + // (dimension-array uint32 3 :offset 76) ;; ? + u32 dim_array = 0; + dim_array |= (hash.dimension_array[0]); + dim_array |= ((hash.dimension_array[1]) << 8); + dim_array |= ((hash.dimension_array[2]) << 16); + gen.add_word(dim_array); // 76 + + gen.add_word(hash.bbox_max_corner_i.x()); // 80 + gen.add_word(hash.bbox_max_corner_i.y()); // 84 + gen.add_word(hash.bbox_max_corner_i.z()); // 88 + + // (num-items uint32 :offset 92) + gen.add_word(hash.index_array.size()); + + // (avg-extents vector :inline :offset 64) + + return result; +} diff --git a/goalc/build_level/collide/jak2/collide.h b/goalc/build_level/collide/jak2/collide.h new file mode 100644 index 00000000000..85a69c25913 --- /dev/null +++ b/goalc/build_level/collide/jak2/collide.h @@ -0,0 +1,122 @@ +#pragma once + +#include + +#include "common/common_types.h" +#include "common/math/Vector.h" + +#include "goalc/build_level/collide/common/collide_common.h" + +// High-level collision system idea: +// Each level has a single collide-hash object storing all collision data. +// The mesh is divided into "fragments". Each fragment is made up of triangles. +// There's a two-level lookup: if you want to find all triangles in a box, you must first find all +// the fragments that intersect the box, then find all the triangles in those fragments that +// intersect the box. +// Each fragment has a bounding box. All triangles inside that fragment fit inside the bounding box. + +/*! + * Vertex in the collide mesh. This is stored as an offset from the bottom corner of the bounding + * box. This is scaled by 16. (a "1" stored here means a distance of 16.f, or 16/4096 of in-game + * meter.) + */ +struct CollideFragmentVertex { + u16 position[3]; +}; + +/*! + * Polygon in the collide mesh. This is a reference to three vertices in the vertex array, and a + * "pat" (polygon attributes?) in the pat array. + */ +struct CollideFragmentPoly { + u8 vertex_index[3]; + u8 pat_index; +}; + +/*! + * The Collide Fragment is divided into a 3D grid. Each cell in the grid has a "bucket" which + * collects a list of all polygons that intersect the cell. The bucket stores a reference to values + * in the index list, which are polygon indices. + */ +struct CollideBucket { + s16 index; + s16 count; +}; + +struct CollideFragment { + std::vector pat_array; + + // per-cell references to the index list + std::vector buckets; + + // references to polygons + std::vector index_array; + + // references to vertices/pats + std::vector poly_array; + + std::vector vert_array; + + // others + + // the x/y/z sizes of a grid cell + math::Vector3f grid_step; + + // inverse of grid step + math::Vector3f axis_scale; + + // the corners of our bounding box + math::Vector3f bbox_min_corner; + math::Vector3f bbox_max_corner; + math::Vector4f bsphere; + math::Vector bbox_min_corner_i; + math::Vector bbox_max_corner_i; + + // the number of cells in the grid along the x/y/z axis + u32 dimension_array[3] = {0, 0, 0}; +}; + +/* + ((num-ids uint16 :offset 4) +(id-count uint16 :offset 6) +(num-buckets uint32 :offset 8) +(qwc-id-bits uint32 :offset 12) +(grid-step vector :inline :offset 16) +(bbox bounding-box :inline :offset-assert 32) +(bbox4w bounding-box4w :inline :offset-assert 64) +(axis-scale vector :inline :offset 48) +(avg-extents vector :inline :offset 64) +(bucket-array uint32 :offset 44) +(item-array (inline-array collide-hash-item) :offset 60 :score 1) +(dimension-array uint32 3 :offset 76) ;; ? +(num-items uint32 :offset 92) + */ + +struct CollideHash { + // if you have a bit for each ID in the item list, how many quadwords (128-byte word) is it? + u32 qwc_id_bits = 0; + + // this is similar to the use in CollideHashFragment, but this points to entries in the . + std::vector buckets; + + // buckets point to this array, which points to the fragments below + std::vector index_array; + + // the actual fragments + std::vector fragments; + + // all these have the same meaning as in CollideFragment and define the grid. + math::Vector3f grid_step; + math::Vector3f axis_scale; + math::Vector3f bbox_min_corner; + math::Vector bbox_min_corner_i; + math::Vector bbox_max_corner_i; + u32 dimension_array[3] = {0, 0, 0}; +}; + +CollideHash construct_collide_hash(const std::vector& tris); +CollideHash construct_collide_hash(const std::vector& tris); + +class DataObjectGenerator; + +size_t add_to_object_file(const CollideHash& hash, DataObjectGenerator& gen); diff --git a/goalc/build_level/collide_common.h b/goalc/build_level/collide_common.h deleted file mode 100644 index a2da8246008..00000000000 --- a/goalc/build_level/collide_common.h +++ /dev/null @@ -1,112 +0,0 @@ -#pragma once -#include "common/common_types.h" -#include "common/math/Vector.h" - -struct PatSurface { - enum class Mode { GROUND = 0, WALL = 1, OBSTACLE = 2, MAX_MODE = 3 }; - enum class Material { - STONE = 0, - ICE = 1, - QUICKSAND = 2, - WATERBOTTOM = 3, - TAR = 4, - SAND = 5, - WOOD = 6, - GRASS = 7, - PCMETAL = 8, - SNOW = 9, - DEEPSNOW = 10, - HOTCOALS = 11, - LAVA = 12, - CRWOOD = 13, - GRAVEL = 14, - DIRT = 15, - METAL = 16, - STRAW = 17, - TUBE = 18, - SWAMP = 19, - STOPPROJ = 20, - ROTATE = 21, - NEUTRAL = 22, - MAX_MATERIAL = 23 - }; - - enum class Event { - NONE = 0, - DEADLY = 1, - ENDLESSFALL = 2, - BURN = 3, - DEADLYUP = 4, - BURNUP = 5, - MELT = 6, - MAX_EVENT = 7, - }; - - void set_noentity(bool x) { - if (x) { - val |= (1 << 0); - } else { - val &= ~(1 << 0); - } - } - bool get_noentity() const { return val & (1 << 0); } - - void set_nocamera(bool x) { - if (x) { - val |= (1 << 1); - } else { - val &= ~(1 << 1); - } - } - bool get_nocamera() const { return val & (1 << 1); } - - void set_noedge(bool x) { - if (x) { - val |= (1 << 2); - } else { - val &= ~(1 << 2); - } - } - bool get_noedge() const { return val & (1 << 2); } - - void set_mode(Mode mode) { - val &= ~(0b111 << 3); - val |= ((u32)mode << 3); - } - Mode get_mode() const { return (Mode)(0b111 & (val >> 3)); } - - void set_material(Material mat) { - val &= ~(0b111111 << 6); - val |= ((u32)mat << 6); - } - Material get_material() const { return (Material)(0b111111 & (val >> 6)); } - - void set_nolineofsight(bool x) { - if (x) { - val |= (1 << 12); - } else { - val &= ~(1 << 12); - } - } - bool get_nolineofsight() const { return val & (1 << 12); } - - void set_event(Event ev) { - val &= ~(0b111111 << 14); - val |= ((u32)ev << 14); - } - Event get_event() const { return (Event)(0b111111 & (val >> 14)); } - - bool operator==(const PatSurface& other) const { return val == other.val; } - // bits 13, [15-31] are unused, or have unknown purpose. - u32 val = 0; -}; - -struct CollideVertex { - float x, y, z; -}; - -struct CollideFace { - math::Vector4f bsphere; - math::Vector3f v[3]; - PatSurface pat; -}; diff --git a/goalc/build_level/common/Entity.cpp b/goalc/build_level/common/Entity.cpp new file mode 100644 index 00000000000..4f3ed66c772 --- /dev/null +++ b/goalc/build_level/common/Entity.cpp @@ -0,0 +1,96 @@ +#include "Entity.h" + +math::Vector4f vectorm3_from_json(const nlohmann::json& json) { + ASSERT(json.size() == 3); + math::Vector4f result; + for (int i = 0; i < 3; i++) { + result[i] = json[i].get() * METER_LENGTH; + } + result[3] = 1.f; + return result; +} + +math::Vector4f vectorm4_from_json(const nlohmann::json& json) { + ASSERT(json.size() == 4); + math::Vector4f result; + for (int i = 0; i < 4; i++) { + result[i] = json[i].get() * METER_LENGTH; + } + return result; +} + +math::Vector4f movie_pos_from_json(const nlohmann::json& json) { + ASSERT(json.size() == 4); + math::Vector4f result; + for (int i = 0; i < 3; i++) { + result[i] = json[i].get() * METER_LENGTH; + } + result[3] = json[3].get() * DEGREES_LENGTH; + return result; +} + +math::Vector4f vector_from_json(const nlohmann::json& json) { + ASSERT(json.size() == 4); + math::Vector4f result; + for (int i = 0; i < 4; i++) { + result[i] = json[i].get(); + } + return result; +} + +std::unique_ptr res_from_json_array(const std::string& name, + const nlohmann::json& json_array) { + ASSERT(json_array.size() > 0); + std::string array_type = json_array[0].get(); + if (array_type == "int32") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(json_array[i].get()); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "uint32") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(json_array[i].get()); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "vector") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(vector_from_json(json_array[i])); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "vector4m") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(vectorm4_from_json(json_array[i])); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "movie-pos") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(movie_pos_from_json(json_array[i])); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "float") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(json_array[i].get()); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "meters") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(json_array[i].get() * METER_LENGTH); + } + return std::make_unique(name, data, -1000000000.0000); + } else if (array_type == "degrees") { + std::vector data; + for (size_t i = 1; i < json_array.size(); i++) { + data.push_back(json_array[i].get() * DEGREES_LENGTH); + } + return std::make_unique(name, data, -1000000000.0000); + } else { + ASSERT_MSG(false, fmt::format("unsupported array type: {}\n", array_type)); + } +} \ No newline at end of file diff --git a/goalc/build_level/common/Entity.h b/goalc/build_level/common/Entity.h new file mode 100644 index 00000000000..3b22503375a --- /dev/null +++ b/goalc/build_level/common/Entity.h @@ -0,0 +1,14 @@ +#pragma once + +#include "common/goal_constants.h" +#include "common/util/Assert.h" + +#include "goalc/build_level/common/ResLump.h" +#include "goalc/data_compiler/DataObjectGenerator.h" + +#include "third-party/json.hpp" + +math::Vector4f vectorm3_from_json(const nlohmann::json& json); +math::Vector4f vectorm4_from_json(const nlohmann::json& json); +math::Vector4f vector_from_json(const nlohmann::json& json); +std::unique_ptr res_from_json_array(const std::string& name, const nlohmann::json& json_array); \ No newline at end of file diff --git a/goalc/build_level/FileInfo.cpp b/goalc/build_level/common/FileInfo.cpp similarity index 61% rename from goalc/build_level/FileInfo.cpp rename to goalc/build_level/common/FileInfo.cpp index e93781dd017..34fb3c49728 100644 --- a/goalc/build_level/FileInfo.cpp +++ b/goalc/build_level/common/FileInfo.cpp @@ -18,15 +18,3 @@ size_t FileInfo::add_to_object_file(DataObjectGenerator& gen) const { return offset; } - -FileInfo make_file_info_for_level(const std::string& file_name) { - FileInfo info; - info.file_type = "bsp-header"; - info.file_name = file_name; - info.major_version = versions::jak1::LEVEL_FILE_VERSION; - info.minor_version = 0; - info.maya_file_name = "Unknown"; - info.tool_debug = "Created by OpenGOAL buildlevel"; - info.mdb_file_name = "Unknown"; - return info; -} diff --git a/goalc/build_level/FileInfo.h b/goalc/build_level/common/FileInfo.h similarity index 91% rename from goalc/build_level/FileInfo.h rename to goalc/build_level/common/FileInfo.h index 7932018ca11..8dd0f5f4aa4 100644 --- a/goalc/build_level/FileInfo.h +++ b/goalc/build_level/common/FileInfo.h @@ -23,6 +23,4 @@ struct FileInfo { std::string mdb_file_name; size_t add_to_object_file(DataObjectGenerator& gen) const; -}; - -FileInfo make_file_info_for_level(const std::string& file_name); \ No newline at end of file +}; \ No newline at end of file diff --git a/goalc/build_level/ResLump.cpp b/goalc/build_level/common/ResLump.cpp similarity index 94% rename from goalc/build_level/ResLump.cpp rename to goalc/build_level/common/ResLump.cpp index 83bcebd9d35..ec8cc8b699c 100644 --- a/goalc/build_level/ResLump.cpp +++ b/goalc/build_level/common/ResLump.cpp @@ -117,6 +117,28 @@ int ResUint8::get_alignment() const { return 16; } +ResUint32::ResUint32(const std::string& name, const std::vector& values, float key_frame) + : Res(name, key_frame), m_values(values) {} + +TagInfo ResUint32::get_tag_info() const { + TagInfo result; + result.elt_type = "uint32"; + result.elt_count = m_values.size(); + result.inlined = true; + result.data_size = m_values.size() * sizeof(u32); + return result; +} + +void ResUint32::write_data(DataObjectGenerator& gen) const { + for (auto& val : m_values) { + gen.add_word(val); + } +} + +int ResUint32::get_alignment() const { + return 16; +} + ResVector::ResVector(const std::string& name, const std::vector& values, float key_frame) diff --git a/goalc/build_level/ResLump.h b/goalc/build_level/common/ResLump.h similarity index 92% rename from goalc/build_level/ResLump.h rename to goalc/build_level/common/ResLump.h index 991926f2a8c..fc51dd846db 100644 --- a/goalc/build_level/ResLump.h +++ b/goalc/build_level/common/ResLump.h @@ -77,6 +77,17 @@ class ResUint8 : public Res { std::vector m_values; }; +class ResUint32 : public Res { + public: + ResUint32(const std::string& name, const std::vector& values, float key_frame); + TagInfo get_tag_info() const override; + void write_data(DataObjectGenerator& gen) const override; + int get_alignment() const override; + + private: + std::vector m_values; +}; + class ResVector : public Res { public: ResVector(const std::string& name, const std::vector& values, float key_frame); diff --git a/goalc/build_level/TexturePool.h b/goalc/build_level/common/TexturePool.h similarity index 100% rename from goalc/build_level/TexturePool.h rename to goalc/build_level/common/TexturePool.h diff --git a/goalc/build_level/Tfrag.cpp b/goalc/build_level/common/Tfrag.cpp similarity index 98% rename from goalc/build_level/Tfrag.cpp rename to goalc/build_level/common/Tfrag.cpp index 1b0e2779418..a7bd8e957fe 100644 --- a/goalc/build_level/Tfrag.cpp +++ b/goalc/build_level/common/Tfrag.cpp @@ -2,9 +2,10 @@ #include +#include "gltf_mesh_extract.h" + #include "common/custom_data/pack_helpers.h" -#include "goalc/build_level/gltf_mesh_extract.h" #include "goalc/data_compiler/DataObjectGenerator.h" void tfrag_from_gltf(const gltf_mesh_extract::TfragOutput& mesh_extract_out, diff --git a/goalc/build_level/Tfrag.h b/goalc/build_level/common/Tfrag.h similarity index 80% rename from goalc/build_level/Tfrag.h rename to goalc/build_level/common/Tfrag.h index 5fe9198a0cb..5ceade14a6c 100644 --- a/goalc/build_level/Tfrag.h +++ b/goalc/build_level/common/Tfrag.h @@ -2,10 +2,11 @@ #include +#include "gltf_mesh_extract.h" + #include "common/custom_data/Tfrag3Data.h" -#include "goalc/build_level/TexturePool.h" -#include "goalc/build_level/gltf_mesh_extract.h" +#include "goalc/build_level/common/TexturePool.h" class DataObjectGenerator; diff --git a/goalc/build_level/common/build_level.cpp b/goalc/build_level/common/build_level.cpp new file mode 100644 index 00000000000..258fd81b908 --- /dev/null +++ b/goalc/build_level/common/build_level.cpp @@ -0,0 +1,43 @@ +#include "build_level.h" + +void save_pc_data(const std::string& nickname, + tfrag3::Level& data, + const fs::path& fr3_output_dir) { + Serializer ser; + data.serialize(ser); + auto compressed = + compression::compress_zstd(ser.get_save_result().first, ser.get_save_result().second); + lg::print("stats for {}\n", data.level_name); + print_memory_usage(data, ser.get_save_result().second); + lg::print("compressed: {} -> {} ({:.2f}%)\n", ser.get_save_result().second, compressed.size(), + 100.f * compressed.size() / ser.get_save_result().second); + file_util::write_binary_file(fr3_output_dir / fmt::format("{}.fr3", str_util::to_upper(nickname)), + compressed.data(), compressed.size()); +} + +std::vector get_build_level_deps(const std::string& input_file) { + auto level_json = parse_commented_json( + file_util::read_text_file(file_util::get_file_path({input_file})), input_file); + return {level_json.at("gltf_file").get()}; +} + +// Find all art groups the custom level needs in a list of object files, +// skipping any that we already found in other dgos before +std::vector find_art_groups( + std::vector& processed_ags, + const std::vector& custom_level_ag, + const std::vector& dgo_files) { + std::vector art_groups; + for (const auto& file : dgo_files) { + // skip any art groups we already added from other dgos + if (std::find(processed_ags.begin(), processed_ags.end(), file.name) != processed_ags.end()) { + continue; + } + if (std::find(custom_level_ag.begin(), custom_level_ag.end(), file.name) != + custom_level_ag.end()) { + art_groups.push_back(file); + processed_ags.push_back(file.name); + } + } + return art_groups; +} diff --git a/goalc/build_level/common/build_level.h b/goalc/build_level/common/build_level.h new file mode 100644 index 00000000000..d71a8bba50c --- /dev/null +++ b/goalc/build_level/common/build_level.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include "common/log/log.h" +#include "common/util/compress.h" +#include "common/util/json_util.h" +#include "common/util/string_util.h" + +#include "decompiler/level_extractor/extract_level.h" + +void save_pc_data(const std::string& nickname, tfrag3::Level& data, const fs::path& fr3_output_dir); +std::vector get_build_level_deps(const std::string& input_file); +std::vector find_art_groups( + std::vector& processed_ags, + const std::vector& custom_level_ag, + const std::vector& dgo_files); diff --git a/goalc/build_level/color_quantization.cpp b/goalc/build_level/common/color_quantization.cpp similarity index 100% rename from goalc/build_level/color_quantization.cpp rename to goalc/build_level/common/color_quantization.cpp diff --git a/goalc/build_level/color_quantization.h b/goalc/build_level/common/color_quantization.h similarity index 100% rename from goalc/build_level/color_quantization.h rename to goalc/build_level/common/color_quantization.h diff --git a/goalc/build_level/gltf_mesh_extract.cpp b/goalc/build_level/common/gltf_mesh_extract.cpp similarity index 97% rename from goalc/build_level/gltf_mesh_extract.cpp rename to goalc/build_level/common/gltf_mesh_extract.cpp index bdf2a1e1153..3935b679486 100644 --- a/goalc/build_level/gltf_mesh_extract.cpp +++ b/goalc/build_level/common/gltf_mesh_extract.cpp @@ -6,12 +6,12 @@ #include +#include "color_quantization.h" + #include "common/log/log.h" #include "common/math/geometry.h" #include "common/util/Timer.h" -#include "goalc/build_level/color_quantization.h" - #include "third-party/tiny_gltf/tiny_gltf.h" namespace gltf_mesh_extract { @@ -609,7 +609,7 @@ void extract(const Input& in, dedup_vertices(out); } -std::optional> subdivide_face_if_needed(CollideFace face_in) { +std::optional> subdivide_face_if_needed(jak1::CollideFace face_in) { math::Vector3f v_min = face_in.v[0]; v_min.min_in_place(face_in.v[1]); v_min.min_in_place(face_in.v[2]); @@ -629,7 +629,7 @@ std::optional> subdivide_face_if_needed(CollideFace fac math::Vector3f v0 = face_in.v[0]; math::Vector3f v1 = face_in.v[1]; math::Vector3f v2 = face_in.v[2]; - CollideFace fs[4]; + jak1::CollideFace fs[4]; fs[0].v[0] = v0; fs[0].v[1] = a; fs[0].v[2] = c; @@ -653,7 +653,7 @@ std::optional> subdivide_face_if_needed(CollideFace fac fs[3].bsphere = math::bsphere_of_triangle(fs[3].v); fs[3].pat = face_in.pat; - std::vector result; + std::vector result; for (auto f : fs) { auto next_faces = subdivide_face_if_needed(f); if (next_faces) { @@ -671,7 +671,7 @@ std::optional> subdivide_face_if_needed(CollideFace fac struct PatResult { bool set = false; bool ignore = false; - PatSurface pat; + jak1::PatSurface pat; }; PatResult custom_props_to_pat(const tinygltf::Value& val, const std::string& /*debug_name*/) { @@ -691,12 +691,12 @@ PatResult custom_props_to_pat(const tinygltf::Value& val, const std::string& /*d result.ignore = false; int mat = val.Get("collide_material").Get(); - ASSERT(mat < (int)PatSurface::Material::MAX_MATERIAL); - result.pat.set_material(PatSurface::Material(mat)); + ASSERT(mat < (int)jak1::PatSurface::Material::MAX_MATERIAL); + result.pat.set_material(jak1::PatSurface::Material(mat)); int evt = val.Get("collide_event").Get(); - ASSERT(evt < (int)PatSurface::Event::MAX_EVENT); - result.pat.set_event(PatSurface::Event(evt)); + ASSERT(evt < (int)jak1::PatSurface::Event::MAX_EVENT); + result.pat.set_event(jak1::PatSurface::Event(evt)); if (val.Get("nolineofsight").Get()) { result.pat.set_nolineofsight(true); @@ -708,8 +708,8 @@ PatResult custom_props_to_pat(const tinygltf::Value& val, const std::string& /*d if (val.Has("collide_mode")) { int mode = val.Get("collide_mode").Get(); - ASSERT(mode < (int)PatSurface::Mode::MAX_MODE); - result.pat.set_mode(PatSurface::Mode(mode)); + ASSERT(mode < (int)jak1::PatSurface::Mode::MAX_MODE); + result.pat.set_mode(jak1::PatSurface::Mode(mode)); } if (val.Get("nocamera").Get()) { @@ -760,7 +760,7 @@ void extract(const Input& in, auto verts = gltf_vertices(model, prim.attributes, n.w_T_node, false, true, mesh.name); for (size_t iidx = 0; iidx < prim_indices.size(); iidx += 3) { - CollideFace face; + jak1::CollideFace face; // get the positions for (int j = 0; j < 3; j++) { @@ -804,7 +804,7 @@ void extract(const Input& in, } } - std::vector fixed_faces; + std::vector fixed_faces; int fix_count = 0; for (auto& face : out.faces) { auto try_fix = subdivide_face_if_needed(face); @@ -835,7 +835,7 @@ void extract(const Input& in, math::Vector3f face_normal = (face.v[1] - face.v[0]).cross(face.v[2] - face.v[0]).normalized(); if (face_normal[1] < wall_cos) { - face.pat.set_mode(PatSurface::Mode::WALL); + face.pat.set_mode(jak1::PatSurface::Mode::WALL); wall_count++; } } diff --git a/goalc/build_level/gltf_mesh_extract.h b/goalc/build_level/common/gltf_mesh_extract.h similarity index 81% rename from goalc/build_level/gltf_mesh_extract.h rename to goalc/build_level/common/gltf_mesh_extract.h index 1c5c62a8943..94dfc09d6bd 100644 --- a/goalc/build_level/gltf_mesh_extract.h +++ b/goalc/build_level/common/gltf_mesh_extract.h @@ -4,8 +4,8 @@ #include "common/custom_data/Tfrag3Data.h" -#include "goalc/build_level/TexturePool.h" -#include "goalc/build_level/collide_common.h" +#include "goalc/build_level/collide/common/collide_common.h" +#include "goalc/build_level/common/TexturePool.h" namespace gltf_mesh_extract { @@ -25,7 +25,7 @@ struct TfragOutput { }; struct CollideOutput { - std::vector faces; + std::vector faces; }; struct Output { diff --git a/goalc/build_level/Entity.cpp b/goalc/build_level/jak1/Entity.cpp similarity index 72% rename from goalc/build_level/Entity.cpp rename to goalc/build_level/jak1/Entity.cpp index 3521597afe7..857ae9c4f37 100644 --- a/goalc/build_level/Entity.cpp +++ b/goalc/build_level/jak1/Entity.cpp @@ -1,13 +1,6 @@ #include "Entity.h" -#include "common/goal_constants.h" -#include "common/util/Assert.h" - -#include "goalc/build_level/ResLump.h" -#include "goalc/data_compiler/DataObjectGenerator.h" - -#include "third-party/json.hpp" - +namespace jak1 { size_t EntityActor::generate(DataObjectGenerator& gen) const { size_t result = res_lump.generate_header(gen, "entity-actor"); for (int i = 0; i < 4; i++) { @@ -30,18 +23,6 @@ size_t EntityActor::generate(DataObjectGenerator& gen) const { return result; } -size_t EntityAmbient::generate(DataObjectGenerator& gen) const { - size_t result = res_lump.generate_header(gen, "entity-ambient"); - for (size_t i = 0; i < 4; i++) { - gen.add_word_float(trans[i]); - } - ASSERT(vis_id < UINT16_MAX); - gen.add_word(aid); - gen.add_word(vis_id); - res_lump.generate_tag_list_and_data(gen, result); - return result; -} - size_t generate_drawable_actor(DataObjectGenerator& gen, const EntityActor& actor, size_t actor_loc) { @@ -58,21 +39,6 @@ size_t generate_drawable_actor(DataObjectGenerator& gen, return result; } -size_t generate_drawable_ambient(DataObjectGenerator& gen, - const EntityAmbient& ambient, - size_t ambient_loc) { - gen.align_to_basic(); - gen.add_type_tag("drawable-ambient"); // 0 - size_t result = gen.current_offset_bytes(); - gen.add_word(ambient.vis_id); // 4 - gen.link_word_to_byte(gen.add_word(0), ambient_loc); // 8 - gen.add_word(0); // 12 - for (int i = 0; i < 4; i++) { - gen.add_word_float(ambient.bsphere[i]); // 16, 20, 24, 28 - } - return result; -} - size_t generate_inline_array_actors(DataObjectGenerator& gen, const std::vector& actors) { std::vector actor_locs; @@ -101,6 +67,72 @@ size_t generate_inline_array_actors(DataObjectGenerator& gen, return result; } +void add_actors_from_json(const nlohmann::json& json, + std::vector& actor_list, + u32 base_aid) { + for (const auto& actor_json : json) { + auto& actor = actor_list.emplace_back(); + actor.aid = actor_json.value("aid", base_aid + actor_list.size()); + actor.trans = vectorm3_from_json(actor_json.at("trans")); + actor.etype = actor_json.at("etype").get(); + actor.game_task = actor_json.value("game_task", 0); + actor.vis_id = actor_json.value("vis_id", 0); + actor.quat = math::Vector4f(0, 0, 0, 1); + if (actor_json.find("quat") != actor_json.end()) { + actor.quat = vector_from_json(actor_json.at("quat")); + } + actor.bsphere = vectorm4_from_json(actor_json.at("bsphere")); + + if (actor_json.find("lump") != actor_json.end()) { + for (auto [key, value] : actor_json.at("lump").items()) { + if (value.is_string()) { + std::string value_string = value.get(); + if (value_string.size() > 0 && value_string[0] == '\'') { + actor.res_lump.add_res( + std::make_unique(key, value_string.substr(1), -1000000000.0000)); + } else { + actor.res_lump.add_res( + std::make_unique(key, value_string, -1000000000.0000)); + } + continue; + } + + if (value.is_array()) { + actor.res_lump.add_res(res_from_json_array(key, value)); + } + } + } + actor.res_lump.sort_res(); + } +} + +size_t EntityAmbient::generate(DataObjectGenerator& gen) const { + size_t result = res_lump.generate_header(gen, "entity-ambient"); + for (size_t i = 0; i < 4; i++) { + gen.add_word_float(trans[i]); + } + ASSERT(vis_id < UINT16_MAX); + gen.add_word(aid); + gen.add_word(vis_id); + res_lump.generate_tag_list_and_data(gen, result); + return result; +} + +size_t generate_drawable_ambient(DataObjectGenerator& gen, + const EntityAmbient& ambient, + size_t ambient_loc) { + gen.align_to_basic(); + gen.add_type_tag("drawable-ambient"); // 0 + size_t result = gen.current_offset_bytes(); + gen.add_word(ambient.vis_id); // 4 + gen.link_word_to_byte(gen.add_word(0), ambient_loc); // 8 + gen.add_word(0); // 12 + for (int i = 0; i < 4; i++) { + gen.add_word_float(ambient.bsphere[i]); // 16, 20, 24, 28 + } + return result; +} + size_t generate_inline_array_ambients(DataObjectGenerator& gen, const std::vector& ambients) { std::vector ambient_locs; @@ -125,75 +157,6 @@ size_t generate_inline_array_ambients(DataObjectGenerator& gen, return result; } -namespace { -math::Vector4f vectorm3_from_json(const nlohmann::json& json) { - ASSERT(json.size() == 3); - math::Vector4f result; - for (int i = 0; i < 3; i++) { - result[i] = json[i].get() * METER_LENGTH; - } - result[3] = 1.f; - return result; -} - -math::Vector4f vectorm4_from_json(const nlohmann::json& json) { - ASSERT(json.size() == 4); - math::Vector4f result; - for (int i = 0; i < 4; i++) { - result[i] = json[i].get() * METER_LENGTH; - } - return result; -} - -math::Vector4f vector_from_json(const nlohmann::json& json) { - ASSERT(json.size() == 4); - math::Vector4f result; - for (int i = 0; i < 4; i++) { - result[i] = json[i].get(); - } - return result; -} - -std::unique_ptr res_from_json_array(const std::string& name, - const nlohmann::json& json_array) { - ASSERT(json_array.size() > 0); - std::string array_type = json_array[0].get(); - if (array_type == "int32") { - std::vector data; - for (size_t i = 1; i < json_array.size(); i++) { - data.push_back(json_array[i].get()); - } - return std::make_unique(name, data, -1000000000.0000); - } else if (array_type == "vector") { - std::vector data; - for (size_t i = 1; i < json_array.size(); i++) { - data.push_back(vector_from_json(json_array[i])); - } - return std::make_unique(name, data, -1000000000.0000); - } else if (array_type == "vector4m") { - std::vector data; - for (size_t i = 1; i < json_array.size(); i++) { - data.push_back(vectorm4_from_json(json_array[i])); - } - return std::make_unique(name, data, -1000000000.0000); - } else if (array_type == "float") { - std::vector data; - for (size_t i = 1; i < json_array.size(); i++) { - data.push_back(json_array[i].get()); - } - return std::make_unique(name, data, -1000000000.0000); - } else if (array_type == "meters") { - std::vector data; - for (size_t i = 1; i < json_array.size(); i++) { - data.push_back(json_array[i].get() * METER_LENGTH); - } - return std::make_unique(name, data, -1000000000.0000); - } else { - ASSERT_MSG(false, fmt::format("unsupported array type: {}\n", array_type)); - } -} -} // namespace - void add_ambients_from_json(const nlohmann::json& json, std::vector& ambient_list, u32 base_aid) { @@ -224,42 +187,4 @@ void add_ambients_from_json(const nlohmann::json& json, ambient.res_lump.sort_res(); } } - -void add_actors_from_json(const nlohmann::json& json, - std::vector& actor_list, - u32 base_aid) { - for (const auto& actor_json : json) { - auto& actor = actor_list.emplace_back(); - actor.aid = actor_json.value("aid", base_aid + actor_list.size()); - actor.trans = vectorm3_from_json(actor_json.at("trans")); - actor.etype = actor_json.at("etype").get(); - actor.game_task = actor_json.value("game_task", 0); - actor.vis_id = actor_json.value("vis_id", 0); - actor.quat = math::Vector4f(0, 0, 0, 1); - if (actor_json.find("quat") != actor_json.end()) { - actor.quat = vector_from_json(actor_json.at("quat")); - } - actor.bsphere = vectorm4_from_json(actor_json.at("bsphere")); - - if (actor_json.find("lump") != actor_json.end()) { - for (auto [key, value] : actor_json.at("lump").items()) { - if (value.is_string()) { - std::string value_string = value.get(); - if (value_string.size() > 0 && value_string[0] == '\'') { - actor.res_lump.add_res( - std::make_unique(key, value_string.substr(1), -1000000000.0000)); - } else { - actor.res_lump.add_res( - std::make_unique(key, value_string, -1000000000.0000)); - } - continue; - } - - if (value.is_array()) { - actor.res_lump.add_res(res_from_json_array(key, value)); - } - } - } - actor.res_lump.sort_res(); - } -} \ No newline at end of file +} // namespace jak1 diff --git a/goalc/build_level/Entity.h b/goalc/build_level/jak1/Entity.h similarity index 68% rename from goalc/build_level/Entity.h rename to goalc/build_level/jak1/Entity.h index 15df5a2631e..1cb2aafd6f2 100644 --- a/goalc/build_level/Entity.h +++ b/goalc/build_level/jak1/Entity.h @@ -1,18 +1,17 @@ #pragma once -#include "goalc/build_level/ResLump.h" - -#include "third-party/json.hpp" +#include "goalc/build_level/common/Entity.h" +namespace jak1 { /* - * (trans vector :inline :offset-assert 32) - (aid uint32 :offset-assert 48) - * (nav-mesh nav-mesh :offset-assert 52) - (etype type :offset-assert 56) ;; probably type - (task game-task :offset-assert 60) - (vis-id uint16 :offset-assert 62) - (vis-id-signed int16 :offset 62) ;; added - (quat quaternion :inline :offset-assert 64) + * (trans vector :inline :offset-assert 32) + * (aid uint32 :offset-assert 48) + * (nav-mesh nav-mesh :offset-assert 52) + * (etype type :offset-assert 56) ;; probably type + * (task game-task :offset-assert 60) + * (vis-id uint16 :offset-assert 62) + * (vis-id-signed int16 :offset 62) ;; added + * (quat quaternion :inline :offset-assert 64) */ struct EntityActor { ResLump res_lump; @@ -28,6 +27,13 @@ struct EntityActor { size_t generate(DataObjectGenerator& gen) const; }; +size_t generate_inline_array_actors(DataObjectGenerator& gen, + const std::vector& actors); + +void add_actors_from_json(const nlohmann::json& json, + std::vector& actor_list, + u32 base_aid); + struct EntityAmbient { ResLump res_lump; u32 aid = 0; @@ -37,15 +43,9 @@ struct EntityAmbient { size_t generate(DataObjectGenerator& gen) const; }; -size_t generate_inline_array_actors(DataObjectGenerator& gen, - const std::vector& actors); - size_t generate_inline_array_ambients(DataObjectGenerator& gen, const std::vector& ambients); void add_ambients_from_json(const nlohmann::json& json, std::vector& ambient_list, u32 base_aid); - -void add_actors_from_json(const nlohmann::json& json, - std::vector& actor_list, - u32 base_aid); \ No newline at end of file +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/jak1/FileInfo.cpp b/goalc/build_level/jak1/FileInfo.cpp new file mode 100644 index 00000000000..245b2b69cc4 --- /dev/null +++ b/goalc/build_level/jak1/FileInfo.cpp @@ -0,0 +1,19 @@ +#include "FileInfo.h" + +#include "common/versions/versions.h" + +#include "goalc/data_compiler/DataObjectGenerator.h" + +namespace jak1 { +FileInfo make_file_info_for_level(const std::string& file_name) { + FileInfo info; + info.file_type = "bsp-header"; + info.file_name = file_name; + info.major_version = versions::jak1::LEVEL_FILE_VERSION; + info.minor_version = 0; + info.maya_file_name = "Unknown"; + info.tool_debug = "Created by OpenGOAL buildlevel"; + info.mdb_file_name = "Unknown"; + return info; +} +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/jak1/FileInfo.h b/goalc/build_level/jak1/FileInfo.h new file mode 100644 index 00000000000..115ed1fa959 --- /dev/null +++ b/goalc/build_level/jak1/FileInfo.h @@ -0,0 +1,7 @@ +#pragma once + +#include "goalc/build_level/common/FileInfo.h" + +namespace jak1 { +FileInfo make_file_info_for_level(const std::string& file_name); +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/LevelFile.cpp b/goalc/build_level/jak1/LevelFile.cpp similarity index 97% rename from goalc/build_level/LevelFile.cpp rename to goalc/build_level/jak1/LevelFile.cpp index e93040321cc..7393ca3f711 100644 --- a/goalc/build_level/LevelFile.cpp +++ b/goalc/build_level/jak1/LevelFile.cpp @@ -1,7 +1,6 @@ #include "LevelFile.h" -#include "goalc/data_compiler/DataObjectGenerator.h" - +namespace jak1 { static size_t ambient_arr_slot; size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const { @@ -85,7 +84,7 @@ std::vector LevelFile::save_object_file() const { auto file_info_slot = info.add_to_object_file(gen); gen.link_word_to_byte(1, file_info_slot); - ambient_arr_slot = generate_inline_array_ambients(gen, ambients); + ambient_arr_slot = jak1::generate_inline_array_ambients(gen, ambients); //(bsphere vector :inline :offset-assert 16) //(all-visible-list (pointer uint16) :offset-assert 32) @@ -124,4 +123,5 @@ std::vector LevelFile::save_object_file() const { //(unk-data-8 uint32 55 :offset-assert 180) return gen.generate_v2(); -} \ No newline at end of file +} +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/LevelFile.h b/goalc/build_level/jak1/LevelFile.h similarity index 91% rename from goalc/build_level/LevelFile.h rename to goalc/build_level/jak1/LevelFile.h index c3a6c2419a3..17844e9e206 100644 --- a/goalc/build_level/LevelFile.h +++ b/goalc/build_level/jak1/LevelFile.h @@ -4,17 +4,19 @@ #include #include +#include "Entity.h" +#include "FileInfo.h" +#include "ambient.h" + #include "common/common_types.h" -#include "goalc/build_level/Entity.h" -#include "goalc/build_level/FileInfo.h" -#include "goalc/build_level/Tfrag.h" -#include "goalc/build_level/ambient.h" -#include "goalc/build_level/collide_bvh.h" -#include "goalc/build_level/collide_common.h" -#include "goalc/build_level/collide_drawable.h" -#include "goalc/build_level/collide_pack.h" +#include "goalc/build_level/collide/common/collide_common.h" +#include "goalc/build_level/collide/jak1/collide_bvh.h" +#include "goalc/build_level/collide/jak1/collide_drawable.h" +#include "goalc/build_level/collide/jak1/collide_pack.h" +#include "goalc/build_level/common/Tfrag.h" +namespace jak1 { struct VisibilityString { std::vector bytes; }; @@ -134,4 +136,5 @@ struct LevelFile { // (unk-data-8 uint32 55 :offset-assert 180) std::vector save_object_file() const; -}; \ No newline at end of file +}; +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/ambient.cpp b/goalc/build_level/jak1/ambient.cpp similarity index 96% rename from goalc/build_level/ambient.cpp rename to goalc/build_level/jak1/ambient.cpp index 85b45a06dc7..759d3ac887b 100644 --- a/goalc/build_level/ambient.cpp +++ b/goalc/build_level/jak1/ambient.cpp @@ -24,6 +24,7 @@ ) */ +namespace jak1 { size_t DrawableTreeAmbient::add_to_object_file(DataObjectGenerator& gen, size_t ambient_array) { gen.align_to_basic(); gen.add_type_tag("drawable-tree-ambient"); @@ -37,4 +38,5 @@ size_t DrawableTreeAmbient::add_to_object_file(DataObjectGenerator& gen, size_t gen.add_word(0); // 28 gen.link_word_to_byte(gen.add_word(0), ambient_array); return result; -} \ No newline at end of file +} +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/ambient.h b/goalc/build_level/jak1/ambient.h similarity index 81% rename from goalc/build_level/ambient.h rename to goalc/build_level/jak1/ambient.h index 8128f906f21..a5f6e62ff0e 100644 --- a/goalc/build_level/ambient.h +++ b/goalc/build_level/jak1/ambient.h @@ -4,6 +4,8 @@ class DataObjectGenerator; +namespace jak1 { struct DrawableTreeAmbient { static size_t add_to_object_file(DataObjectGenerator& gen, size_t ambient_array); -}; \ No newline at end of file +}; +} // namespace jak1 diff --git a/goalc/build_level/build_level.cpp b/goalc/build_level/jak1/build_level.cpp similarity index 76% rename from goalc/build_level/build_level.cpp rename to goalc/build_level/jak1/build_level.cpp index 417be470d8e..70683b4df5b 100644 --- a/goalc/build_level/build_level.cpp +++ b/goalc/build_level/jak1/build_level.cpp @@ -1,66 +1,15 @@ #include "build_level.h" -#include "common/custom_data/Tfrag3Data.h" -#include "common/log/log.h" -#include "common/util/FileUtil.h" -#include "common/util/compress.h" -#include "common/util/json_util.h" -#include "common/util/string_util.h" - #include "decompiler/extractor/extractor_util.h" #include "decompiler/level_extractor/extract_merc.h" -#include "goalc/build_level/Entity.h" -#include "goalc/build_level/FileInfo.h" -#include "goalc/build_level/LevelFile.h" -#include "goalc/build_level/Tfrag.h" -#include "goalc/build_level/collide_bvh.h" -#include "goalc/build_level/collide_pack.h" -#include "goalc/build_level/gltf_mesh_extract.h" - -#include "third-party/fmt/core.h" - -void save_pc_data(const std::string& nickname, - tfrag3::Level& data, - const fs::path& fr3_output_dir) { - Serializer ser; - data.serialize(ser); - auto compressed = - compression::compress_zstd(ser.get_save_result().first, ser.get_save_result().second); - lg::print("stats for {}\n", data.level_name); - print_memory_usage(data, ser.get_save_result().second); - lg::print("compressed: {} -> {} ({:.2f}%)\n", ser.get_save_result().second, compressed.size(), - 100.f * compressed.size() / ser.get_save_result().second); - file_util::write_binary_file(fr3_output_dir / fmt::format("{}.fr3", str_util::to_upper(nickname)), - compressed.data(), compressed.size()); -} - -std::vector get_build_level_deps(const std::string& input_file) { - auto level_json = parse_commented_json( - file_util::read_text_file(file_util::get_file_path({input_file})), input_file); - return {level_json.at("gltf_file").get()}; -} - -// Find all art groups the custom level needs in a list of object files, -// skipping any that we already found in other dgos before -std::vector find_art_groups( - std::vector& processed_ags, - const std::vector& custom_level_ag, - const std::vector& dgo_files) { - std::vector art_groups; - for (const auto& file : dgo_files) { - // skip any art groups we already added from other dgos - if (std::find(processed_ags.begin(), processed_ags.end(), file.name) != processed_ags.end()) { - continue; - } - if (std::find(custom_level_ag.begin(), custom_level_ag.end(), file.name) != - custom_level_ag.end()) { - art_groups.push_back(file); - processed_ags.push_back(file.name); - } - } - return art_groups; -} - +#include "goalc/build_level/collide/jak1/collide_bvh.h" +#include "goalc/build_level/collide/jak1/collide_pack.h" +#include "goalc/build_level/common/Tfrag.h" +#include "goalc/build_level/jak1/Entity.h" +#include "goalc/build_level/jak1/FileInfo.h" +#include "goalc/build_level/jak1/LevelFile.h" + +namespace jak1 { bool run_build_level(const std::string& input_file, const std::string& bsp_output_file, const std::string& output_prefix) { @@ -100,7 +49,8 @@ bool run_build_level(const std::string& input_file, file.actors = std::move(actors); // ambients std::vector ambients; - add_ambients_from_json(level_json.at("ambients"), ambients, level_json.value("base_id", 12345)); + jak1::add_ambients_from_json(level_json.at("ambients"), ambients, + level_json.value("base_id", 12345)); file.ambients = std::move(ambients); auto& ambient_drawable_tree = file.drawable_trees.ambients.emplace_back(); (void)ambient_drawable_tree; @@ -259,3 +209,4 @@ bool run_build_level(const std::string& input_file, return true; } +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/build_level.h b/goalc/build_level/jak1/build_level.h similarity index 51% rename from goalc/build_level/build_level.h rename to goalc/build_level/jak1/build_level.h index e5f1c689f6f..239891b5407 100644 --- a/goalc/build_level/build_level.h +++ b/goalc/build_level/jak1/build_level.h @@ -1,11 +1,9 @@ #pragma once -#include -#include - -#include "decompiler/level_extractor/extract_level.h" +#include "goalc/build_level/common/build_level.h" +namespace jak1 { bool run_build_level(const std::string& input_file, const std::string& bsp_output_file, const std::string& output_prefix); -std::vector get_build_level_deps(const std::string& input_file); +} // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/jak2/Entity.cpp b/goalc/build_level/jak2/Entity.cpp new file mode 100644 index 00000000000..c8ab118ba35 --- /dev/null +++ b/goalc/build_level/jak2/Entity.cpp @@ -0,0 +1,109 @@ +#include "Entity.h" + +namespace jak2 { +size_t EntityActor::generate(DataObjectGenerator& gen) const { + size_t result = res_lump.generate_header(gen, "entity-actor"); + for (int i = 0; i < 4; i++) { + gen.add_word_float(trans[i]); + } + gen.add_word(aid); + gen.add_word(kill_mask); + gen.add_type_tag(etype); + + ASSERT(game_task < UINT16_MAX); + ASSERT(vis_id < UINT16_MAX); + u32 packed = (game_task) | (vis_id << 16); + gen.add_word(packed); + + for (int i = 0; i < 4; i++) { + gen.add_word_float(quat[i]); + } + + res_lump.generate_tag_list_and_data(gen, result); + return result; +} + +size_t generate_drawable_actor(DataObjectGenerator& gen, + const EntityActor& actor, + size_t actor_loc) { + gen.align_to_basic(); + gen.add_type_tag("drawable-actor"); // 0 + size_t result = gen.current_offset_bytes(); + gen.add_word(actor.vis_id); // 4 + gen.link_word_to_byte(gen.add_word(0), actor_loc); // 8 + gen.add_word(0); // 12 + for (int i = 0; i < 4; i++) { + gen.add_word_float(actor.bsphere[i]); // 16, 20, 24, 28 + } + + return result; +} + +size_t generate_inline_array_actors(DataObjectGenerator& gen, + const std::vector& actors) { + std::vector actor_locs; + for (auto& actor : actors) { + actor_locs.push_back(actor.generate(gen)); + } + + gen.align_to_basic(); + gen.add_type_tag("drawable-inline-array-actor"); // 0 + size_t result = gen.current_offset_bytes(); + ASSERT(actors.size() < UINT16_MAX); + gen.add_word(actors.size() << 16); // 4 + gen.add_word(0); + gen.add_word(0); + + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + + ASSERT((gen.current_offset_bytes() % 16) == 0); + + for (size_t i = 0; i < actors.size(); i++) { + generate_drawable_actor(gen, actors[i], actor_locs[i]); + } + return result; +} + +void add_actors_from_json(const nlohmann::json& json, + std::vector& actor_list, + u32 base_aid) { + for (const auto& actor_json : json) { + auto& actor = actor_list.emplace_back(); + actor.aid = actor_json.value("aid", base_aid + actor_list.size()); + actor.trans = vectorm3_from_json(actor_json.at("trans")); + actor.etype = actor_json.at("etype").get(); + actor.kill_mask = actor_json.value("kill_mask", 0); + actor.game_task = actor_json.value("game_task", 0); + actor.vis_id = actor_json.value("vis_id", 0); + actor.quat = math::Vector4f(0, 0, 0, 1); + if (actor_json.find("quat") != actor_json.end()) { + actor.quat = vector_from_json(actor_json.at("quat")); + } + actor.bsphere = vectorm4_from_json(actor_json.at("bsphere")); + + if (actor_json.find("lump") != actor_json.end()) { + for (auto [key, value] : actor_json.at("lump").items()) { + if (value.is_string()) { + std::string value_string = value.get(); + if (value_string.size() > 0 && value_string[0] == '\'') { + actor.res_lump.add_res( + std::make_unique(key, value_string.substr(1), -1000000000.0000)); + } else { + actor.res_lump.add_res( + std::make_unique(key, value_string, -1000000000.0000)); + } + continue; + } + + if (value.is_array()) { + actor.res_lump.add_res(res_from_json_array(key, value)); + } + } + } + actor.res_lump.sort_res(); + } +} +} // namespace jak2 diff --git a/goalc/build_level/jak2/Entity.h b/goalc/build_level/jak2/Entity.h new file mode 100644 index 00000000000..72e181d454c --- /dev/null +++ b/goalc/build_level/jak2/Entity.h @@ -0,0 +1,38 @@ +#pragma once + +#include "goalc/build_level/common/Entity.h" + +namespace jak2 { +/* + * (trans vector :inline :offset-assert 32) + * (aid uint32 :offset-assert 48) + * (kill-mask task-mask :offset-assert 52) + * (etype type :offset-assert 56) ;; probably type + * (task game-task :offset-assert 60) + * (vis-id uint16 :offset-assert 62) + * (quat quaternion :inline :offset-assert 64) + */ +struct EntityActor { + ResLump res_lump; + math::Vector4f trans; // w = 1 here + u32 aid = 0; + u32 kill_mask = 0; + std::string etype; + u32 game_task = 0; + u32 vis_id = 0; + math::Vector4f quat; + + math::Vector4f bsphere; + + size_t generate(DataObjectGenerator& gen) const; +}; + +size_t generate_inline_array_actors(DataObjectGenerator& gen, + const std::vector& actors); + +void add_actors_from_json(const nlohmann::json& json, + std::vector& actor_list, + u32 base_aid); + +struct Region {}; +} // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/FileInfo.cpp b/goalc/build_level/jak2/FileInfo.cpp new file mode 100644 index 00000000000..c223ce35c6b --- /dev/null +++ b/goalc/build_level/jak2/FileInfo.cpp @@ -0,0 +1,19 @@ +#include "FileInfo.h" + +#include "common/versions/versions.h" + +#include "goalc/data_compiler/DataObjectGenerator.h" + +namespace jak2 { +FileInfo make_file_info_for_level(const std::string& file_name) { + FileInfo info; + info.file_type = "bsp-header"; + info.file_name = file_name; + info.major_version = versions::jak2::LEVEL_FILE_VERSION; + info.minor_version = 0; + info.maya_file_name = "Unknown"; + info.tool_debug = "Created by OpenGOAL buildlevel"; + info.mdb_file_name = "Unknown"; + return info; +} +} // namespace jak2 diff --git a/goalc/build_level/jak2/FileInfo.h b/goalc/build_level/jak2/FileInfo.h new file mode 100644 index 00000000000..79a0dd1a980 --- /dev/null +++ b/goalc/build_level/jak2/FileInfo.h @@ -0,0 +1,7 @@ +#pragma once + +#include "goalc/build_level/common/FileInfo.h" + +namespace jak2 { +FileInfo make_file_info_for_level(const std::string& file_name); +} // namespace jak2 diff --git a/goalc/build_level/jak2/LevelFile.cpp b/goalc/build_level/jak2/LevelFile.cpp new file mode 100644 index 00000000000..689ede5155b --- /dev/null +++ b/goalc/build_level/jak2/LevelFile.cpp @@ -0,0 +1,140 @@ +#include "LevelFile.h" + +#include "goalc/data_compiler/DataObjectGenerator.h" + +namespace jak2 { +size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const { + /* + (deftype drawable-tree-array (drawable-group) + ((trees drawable-tree 1 :offset 32 :score 100)) + :flag-assert #x1200000024 + ) + (deftype drawable-group (drawable) + ((length int16 :offset 6) + (data drawable 1 :offset-assert 32) + ) + (:methods + (new (symbol type int) _type_) + ) + :flag-assert #x1200000024 + ) + */ + gen.align_to_basic(); + gen.add_type_tag("drawable-tree-array"); + size_t result = gen.current_offset_bytes(); + int num_trees = 0; + num_trees += tfrags.size(); + gen.add_word(num_trees << 16); + gen.add_word(0); + gen.add_word(0); + + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + + // todo add trees... + + if (num_trees == 0) { + gen.add_word(0); // the one at the end. + } else { + int tree_word = (int)gen.current_offset_bytes() / 4; + for (int i = 0; i < num_trees; i++) { + gen.add_word(0); + } + + for (auto& tfrag : tfrags) { + // gen.set_word(tree_word++, tfrag.add_to_object_file(gen)); + gen.link_word_to_byte(tree_word++, tfrag.add_to_object_file(gen)); + } + } + + return result; +} + +size_t generate_u32_array(const std::vector& array, DataObjectGenerator& gen) { + gen.align(4); + size_t result = gen.current_offset_bytes(); + for (auto& entry : array) { + gen.add_word(entry); + } + return result; +} + +std::vector LevelFile::save_object_file() const { + DataObjectGenerator gen; + gen.add_type_tag("bsp-header"); + + // add blank space for the bsp-header + while (gen.words() < 100) { + gen.add_word(0); + } + + //(info file-info :offset 4) + auto file_info_slot = info.add_to_object_file(gen); + gen.link_word_to_byte(1, file_info_slot); + + //(bsphere vector :inline :offset-assert 16) + //(all-visible-list (pointer uint16) :offset-assert 32) + //(visible-list-length int32 :offset-assert 36) + //(drawable-trees drawable-tree-array :offset-assert 40) + gen.link_word_to_byte(40 / 4, drawable_trees.add_to_object_file(gen)); + //(pat pointer :offset-assert 44) + //(pat-length int32 :offset-assert 48) + //(texture-remap-table (pointer uint64) :offset-assert 52) + //(texture-remap-table-len int32 :offset-assert 56) + //(texture-ids (pointer texture-id) :offset-assert 60) + //(texture-page-count int32 :offset-assert 64) + //(unknown-basic basic :offset-assert 68) + //(name symbol :offset-assert 72) + gen.link_word_to_symbol(name, 72 / 4); + //(nickname symbol :offset-assert 76) + gen.link_word_to_symbol(nickname, 76 / 4); + //(vis-info level-vis-info 8 :offset-assert 80) + //(actors drawable-inline-array-actor :offset-assert 112) + gen.link_word_to_byte(112 / 4, generate_inline_array_actors(gen, actors)); + //(cameras (array entity-camera) :offset-assert 116) + //(nodes (inline-array bsp-node) :offset-assert 120) + //(level level :offset-assert 124) + //(current-leaf-idx uint16 :offset-assert 128) + //(cam-outside-bsp uint8 :offset 152) + //(cam-using-back uint8 :offset-assert 153) + //(cam-box-idx uint16 :offset-assert 154) + //(ambients symbol :offset-assert 156) + //(subdivide-close float :offset-assert 160) + //(subdivide-far float :offset-assert 160) + //(race-meshes (array entity-race-mesh) :offset-assert 168) + //(actor-birth-order (pointer uint32) :offset-assert 172) + gen.link_word_to_byte(172 / 4, generate_u32_array(actor_birth_order, gen)); + //(light-hash light-hash :offset-assert 176) + //(nav-meshes (array entity-nav-mesh) :offset-assert 180) + //(actor-groups (array actor-group) :offset-assert 184) + //(region-trees (array drawable-tree-region-prim) :offset-assert 188) + //(region-array region-array :offset-assert 192) + //(collide-hash collide-hash :offset-assert 196) + gen.link_word_to_byte(196 / 4, add_to_object_file(collide_hash, gen)); + //(wind-array uint32 :offset 200) + //(wind-array-length int32 :offset 204) + //(city-level-info city-level-info :offset 208) + //(vis-spheres vector-array :offset 216) + //(vis-spheres-length uint32 :offset 248) + //(region-tree drawable-tree-region-prim :offset 252) + //(tfrag-masks texture-masks-array :offset-assert 256) + //(tfrag-closest (pointer float) :offset-assert 260) + //(tfrag-mask-count uint32 :offset 260) + //(shrub-masks texture-masks-array :offset-assert 264) + //(shrub-closest (pointer float) :offset-assert 268) + //(shrub-mask-count uint32 :offset 268) + //(alpha-masks texture-masks-array :offset-assert 272) + //(alpha-closest (pointer float) :offset-assert 276) + //(alpha-mask-count uint32 :offset 276) + //(water-masks texture-masks-array :offset-assert 280) + //(water-closest (pointer float) :offset-assert 284) + //(water-mask-count uint32 :offset 284) + //(bsp-scale vector :inline :offset-assert 288) + //(bsp-offset vector :inline :offset-assert 304) + //(end uint8 :offset 399) + + return gen.generate_v2(); +} +} // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/LevelFile.h b/goalc/build_level/jak2/LevelFile.h new file mode 100644 index 00000000000..fad9708f084 --- /dev/null +++ b/goalc/build_level/jak2/LevelFile.h @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include + +#include "common/common_types.h" + +#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/jak2/Entity.h" +#include "goalc/build_level/jak2/FileInfo.h" + +namespace jak2 { +struct VisibilityString { + std::vector bytes; +}; + +struct DrawableTreeInstanceTie {}; + +struct DrawableTreeActor {}; + +struct DrawableTreeInstanceShrub {}; + +struct DrawableTreeRegionPrim {}; + +struct DrawableTreeArray { + std::vector tfrags; + std::vector ties; + std::vector actors; // unused? + std::vector regions; + std::vector shrubs; + size_t add_to_object_file(DataObjectGenerator& gen) const; +}; + +struct TextureRemap {}; + +struct TextureId {}; + +struct VisInfo {}; + +struct EntityCamera {}; + +struct BspNode {}; + +struct RaceMesh {}; + +struct LightHash {}; + +struct EntityNavMesh {}; + +struct ActorGroup {}; + +struct RegionTree {}; + +struct RegionArray {}; + +struct CityLevelInfo {}; + +struct TextureMasksArray {}; + +// This is a place to collect all the data that should go into the bsp-header file. +struct LevelFile { + // (info file-info :offset 4) + FileInfo info; + + // (all-visible-list (pointer uint16) :offset-assert 32) + // (visible-list-length int16 :offset-assert 36) + // (extra-vis-list-length int16 :offset-assert 38) + VisibilityString all_visibile_list; + + // (drawable-trees drawable-tree-array :offset-assert 40) + DrawableTreeArray drawable_trees; + + // (pat pointer :offset-assert 44) + // (pat-length int32 :offset-assert 48) + std::vector pat; + + // (texture-remap-table (pointer uint64) :offset-assert 52) + // (texture-remap-table-len int32 :offset-assert 56) + std::vector texture_remap_table; + + // (texture-ids (pointer texture-id) :offset-assert 60) + // (texture-page-count int32 :offset-assert 64) + std::vector texture_ids; + + // (unknown-basic basic :offset-assert 68) + // "misc", seems like it can be zero and is unused. + + // (name symbol :offset-assert 72) + std::string name; // full name + + // (nickname symbol :offset-assert 76) + std::string nickname; // 3 char name + + // (vis-info level-vis-info 8 :offset-assert 80) ;; note: 0 when + std::array vis_infos; + + // (actors drawable-inline-array-actor :offset-assert 112) + std::vector actors; + + // (cameras (array entity-camera) :offset-assert 116) + std::vector cameras; + + // (nodes (inline-array bsp-node) :offset-assert 120) + std::vector nodes; + + // (level level :offset-assert 124) + // zero + + // (current-leaf-idx uint16 :offset-assert 128) + // zero + + // (texture-flags texture-page-flag 10 :offset-assert 130) + std::vector texture_flags; + + // (cam-outside-bsp uint8 :offset 152) + // (cam-using-back uint8 :offset-assert 153) + // (cam-box-idx uint16 :offset-assert 154) + // zero + + // (ambients symbol :offset-assert 156) + // #t + + // (subdivide-close float :offset-assert 160) + float close_subdiv = 0; + + // (subdivide-far float :offset-assert 164) + float far_subdiv = 0; + + // (race-meshes (array entity-race-mesh) :offset-assert 168) + std::vector race_meshes; + + // (actor-birth-order (pointer uint32) :offset-assert 172) + std::vector actor_birth_order; + + // (light-hash light-hash :offset-assert 176) + LightHash light_hash; + // (nav-meshes (array entity-nav-mesh) :offset-assert 180) + std::vector entity_nav_meshes; + // (actor-groups (array actor-group) :offset-assert 184) + std::vector actor_groups; + // (region-trees (array drawable-tree-region-prim) :offset-assert 188) + std::vector region_trees; + // (region-array region-array :offset-assert 192) + RegionArray region_array; + // (collide-hash collide-hash :offset-assert 196) + CollideHash collide_hash; + // (wind-array uint32 :offset 200) + std::vector wind_array; + // (wind-array-length int32 :offset 204) + s32 wind_array_length = 0; + // (city-level-info city-level-info :offset 208) + CityLevelInfo city_level_info; + // (vis-spheres vector-array :offset 216) + // (vis-spheres-length uint32 :offset 248) + + // (region-tree drawable-tree-region-prim :offset 252) + RegionTree region_tree; + // (tfrag-masks texture-masks-array :offset-assert 256) + // (tfrag-closest (pointer float) :offset-assert 260) + // (tfrag-mask-count uint32 :offset 260) + TextureMasksArray tfrag_masks; + // (shrub-masks texture-masks-array :offset-assert 264) + // (shrub-closest (pointer float) :offset-assert 268) + // (shrub-mask-count uint32 :offset 268) + TextureMasksArray shrub_masks; + // (alpha-masks texture-masks-array :offset-assert 272) + // (alpha-closest (pointer float) :offset-assert 276) + // (alpha-mask-count uint32 :offset 276) + TextureMasksArray alpha_masks; + // (water-masks texture-masks-array :offset-assert 280) + // (water-closest (pointer float) :offset-assert 284) + // (water-mask-count uint32 :offset 284) + TextureMasksArray water_masks; + // (bsp-scale vector :inline :offset-assert 288) + // (bsp-offset vector :inline :offset-assert 304) + + std::vector save_object_file() const; +}; +} // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/build_level.cpp b/goalc/build_level/jak2/build_level.cpp new file mode 100644 index 00000000000..9f6210dd96d --- /dev/null +++ b/goalc/build_level/jak2/build_level.cpp @@ -0,0 +1,198 @@ +#include "build_level.h" + +#include "decompiler/extractor/extractor_util.h" +#include "decompiler/level_extractor/extract_merc.h" +#include "goalc/build_level/collide/jak2/collide.h" +#include "goalc/build_level/common/Tfrag.h" +#include "goalc/build_level/jak2/Entity.h" +#include "goalc/build_level/jak2/FileInfo.h" +#include "goalc/build_level/jak2/LevelFile.h" + +namespace jak2 { +bool run_build_level(const std::string& input_file, + const std::string& bsp_output_file, + const std::string& output_prefix) { + auto level_json = parse_commented_json( + file_util::read_text_file(file_util::get_file_path({input_file})), input_file); + LevelFile file; // GOAL level file + tfrag3::Level pc_level; // PC level file + TexturePool tex_pool; // pc level texture pool + + // process input mesh from blender + gltf_mesh_extract::Input mesh_extract_in; + mesh_extract_in.filename = + file_util::get_file_path({level_json.at("gltf_file").get()}); + mesh_extract_in.auto_wall_enable = level_json.value("automatic_wall_detection", true); + mesh_extract_in.double_sided_collide = level_json.at("double_sided_collide").get(); + mesh_extract_in.auto_wall_angle = level_json.value("automatic_wall_angle", 30.0); + mesh_extract_in.tex_pool = &tex_pool; + gltf_mesh_extract::Output mesh_extract_out; + gltf_mesh_extract::extract(mesh_extract_in, mesh_extract_out); + + // add stuff to the GOAL level structure + file.info = make_file_info_for_level(fs::path(input_file).filename().string()); + // all vis + // drawable trees + // pat + // texture remap + // texture ids + // unk zero + // name + file.name = level_json.at("long_name").get(); + // nick + file.nickname = level_json.at("nickname").get(); + // vis infos + // actors + std::vector actors; + add_actors_from_json(level_json.at("actors"), actors, level_json.value("base_id", 1234)); + file.actors = std::move(actors); + // cameras + // nodes + // regions + // subdivs + // actor birth + for (size_t i = 0; i < file.actors.size(); i++) { + file.actor_birth_order.push_back(i); + } + + // add stuff to the PC level structure + pc_level.level_name = file.name; + + // TFRAG + auto& tfrag_drawable_tree = file.drawable_trees.tfrags.emplace_back(); + tfrag_from_gltf(mesh_extract_out.tfrag, tfrag_drawable_tree, + pc_level.tfrag_trees[0].emplace_back()); + pc_level.textures = std::move(tex_pool.textures_by_idx); + + // COLLIDE + if (mesh_extract_out.collide.faces.empty()) { + lg::error("No collision geometry was found"); + } else { + file.collide_hash = construct_collide_hash(mesh_extract_out.collide.faces); + } + + // Save the GOAL level + auto result = file.save_object_file(); + lg::print("Level bsp file size {} bytes\n", result.size()); + auto save_path = file_util::get_jak_project_dir() / bsp_output_file; + file_util::create_dir_if_needed_for_file(save_path); + lg::print("Saving to {}\n", save_path.string()); + file_util::write_binary_file(save_path, result.data(), result.size()); + + // 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())) { + fs::path iso_folder = ""; + lg::info("Looking for ISO path..."); + // TODO - add to file_util + for (const auto& entry : + fs::directory_iterator(file_util::get_jak_project_dir() / "iso_data")) { + // TODO - hard-coded to jak 2 + if (entry.is_directory() && + entry.path().filename().string().find("jak2") != std::string::npos) { + lg::info("Found ISO path: {}", entry.path().string()); + iso_folder = entry.path(); + } + } + + if (iso_folder.empty() || !fs::exists(iso_folder)) { + lg::warn("Could not locate ISO path!"); + return false; + } + + // Look for iso build info if it's available, otherwise default to ntsc_v1 + const auto version_info = get_version_info_or_default(iso_folder); + + decompiler::Config config; + try { + config = decompiler::read_config_file( + file_util::get_jak_project_dir() / "decompiler/config/jak2/jak2_config.jsonc", + version_info.decomp_config_version, + R"({"decompile_code": false, "find_functions": false, "levels_extract": true, "allowed_objects": [], "save_texture_pngs": false})"); + } catch (const std::exception& e) { + lg::error("Failed to parse config: {}", e.what()); + return false; + } + + std::vector dgos, objs; + for (const auto& dgo_name : config.dgo_names) { + dgos.push_back(iso_folder / dgo_name); + } + + for (const auto& obj_name : config.object_file_names) { + objs.push_back(iso_folder / obj_name); + } + + decompiler::ObjectFileDB db(dgos, fs::path(config.obj_file_name_map_file), objs, {}, {}, + config); + + // need to process link data for tpages + db.process_link_data(config); + + decompiler::TextureDB tex_db; + 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); + + // find all art groups used by the custom level in other dgos + if (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 + const auto& dgo_name = dgo.substr(4); + const auto& files = db.obj_files_by_dgo.at(dgo_name); + auto art_groups = + find_art_groups(processed_art_groups, + level_json.at("art_groups").get>(), files); + auto tex_remap = decompiler::extract_tex_remap(db, dgo_name); + for (const auto& ag : art_groups) { + if (ag.name.length() > 3 && !ag.name.compare(ag.name.length() - 3, 3, "-ag")) { + const auto& ag_file = db.lookup_record(ag); + lg::print("custom level: extracting art group {}\n", ag_file.name_in_dgo); + decompiler::extract_merc(ag_file, tex_db, db.dts, tex_remap, pc_level, false, + db.version()); + } + } + } + } + + // add textures + if (level_json.contains("textures") && !level_json.at("textures").empty()) { + std::vector processed_textures; + std::vector wanted_texs = + level_json.at("textures").get>(); + // first check the texture is not already in the level + for (auto& level_tex : pc_level.textures) { + if (std::find(wanted_texs.begin(), wanted_texs.end(), level_tex.debug_name) != + wanted_texs.end()) { + processed_textures.push_back(level_tex.debug_name); + } + } + + // then add + for (auto& [id, tex] : tex_db.textures) { + for (auto& tex0 : wanted_texs) { + if (std::find(processed_textures.begin(), processed_textures.end(), tex.name) != + processed_textures.end()) { + continue; + } + if (tex.name == tex0) { + lg::info("custom level: adding texture {} from tpage {} ({})", tex.name, tex.page, + tex_db.tpage_names.at(tex.page)); + pc_level.textures.push_back( + make_texture(id, tex, tex_db.tpage_names.at(tex.page), true)); + processed_textures.push_back(tex.name); + } + } + } + } + } + + // Save the PC level + save_pc_data(file.nickname, pc_level, + file_util::get_jak_project_dir() / "out" / output_prefix / "fr3"); + + return true; +} +} // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/build_level.h b/goalc/build_level/jak2/build_level.h new file mode 100644 index 00000000000..98ea4c34f72 --- /dev/null +++ b/goalc/build_level/jak2/build_level.h @@ -0,0 +1,9 @@ +#pragma once + +#include "goalc/build_level/common/build_level.h" + +namespace jak2 { +bool run_build_level(const std::string& input_file, + const std::string& bsp_output_file, + const std::string& output_prefix); +} // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/main.cpp b/goalc/build_level/main.cpp new file mode 100644 index 00000000000..d26d648808a --- /dev/null +++ b/goalc/build_level/main.cpp @@ -0,0 +1,69 @@ +#include "common/log/log.h" +#include "common/util/Assert.h" +#include "common/util/FileUtil.h" +#include "common/versions/versions.h" + +#include "goalc/build_level/jak1/build_level.h" +#include "goalc/build_level/jak2/build_level.h" + +#include "third-party/CLI11.hpp" + +// debug tool to run only build_level. +int main(int argc, char** argv) { + // logging + lg::set_stdout_level(lg::level::info); + lg::set_flush_level(lg::level::info); + lg::initialize(); + + // game version + std::string game, input_json, output_file; + fs::path project_path_override; + + // path + if (!file_util::setup_project_path(std::nullopt)) { + return 1; + } + + lg::info("Build Level Tool", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR); + + CLI::App app{"OpenGOAL Compiler / REPL"}; + app.add_option("input-json", input_json, + "Input JSON file (for example, custom_levels/jak2/test-zone/test-zone.jsonc)") + ->required(); + app.add_option("output-file", output_file, + "Output .go file, (for example out/jak2/obj/test-zone.go)") + ->required(); + app.add_option("-g,--game", game, "Game version (jak1 or jak2)")->required(); + app.add_option("--proj-path", project_path_override, + "Specify the location of the 'data/' folder"); + app.validate_positionals(); + CLI11_PARSE(app, argc, argv); + + GameVersion game_version = game_name_to_version(game); + + if (!project_path_override.empty()) { + if (!fs::exists(project_path_override)) { + lg::error("Error: project path override '{}' does not exist", project_path_override.string()); + return 1; + } + if (!file_util::setup_project_path(project_path_override)) { + lg::error("Could not setup project path!"); + return 1; + } + } else if (!file_util::setup_project_path(std::nullopt)) { + return 1; + } + + switch (game_version) { + case GameVersion::Jak1: + jak1::run_build_level(input_json, output_file, "jak1/"); + break; + case GameVersion::Jak2: + jak2::run_build_level(input_json, output_file, "jak2/"); + break; + default: + ASSERT_NOT_REACHED_MSG("unsupported game version"); + } + + return 0; +} diff --git a/goalc/make/MakeSystem.cpp b/goalc/make/MakeSystem.cpp index 4f8429a039f..463adc6a3d6 100644 --- a/goalc/make/MakeSystem.cpp +++ b/goalc/make/MakeSystem.cpp @@ -102,6 +102,7 @@ MakeSystem::MakeSystem(const std::optional repl_config, const std: add_tool(); add_tool(); add_tool(); + add_tool(); } /*! diff --git a/goalc/make/Tools.cpp b/goalc/make/Tools.cpp index e929a21affc..12a0a3c7863 100644 --- a/goalc/make/Tools.cpp +++ b/goalc/make/Tools.cpp @@ -4,7 +4,8 @@ #include "common/util/DgoWriter.h" #include "common/util/FileUtil.h" -#include "goalc/build_level/build_level.h" +#include "goalc/build_level/jak1/build_level.h" +#include "goalc/build_level/jak2/build_level.h" #include "goalc/compiler/Compiler.h" #include "goalc/data_compiler/dir_tpages.h" #include "goalc/data_compiler/game_count.h" @@ -258,5 +259,22 @@ bool BuildLevelTool::run(const ToolInput& task, const PathMap& path_map) { if (task.input.size() != 1) { throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); } - return run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix); + return jak1::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix); +} + +BuildLevel2Tool::BuildLevel2Tool() : Tool("build-level2") {} + +bool BuildLevel2Tool::needs_run(const ToolInput& task, const PathMap& path_map) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + auto deps = get_build_level_deps(task.input.at(0)); + return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map); +} + +bool BuildLevel2Tool::run(const ToolInput& task, const PathMap& path_map) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + return jak2::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix); } diff --git a/goalc/make/Tools.h b/goalc/make/Tools.h index a3c6d5e3f77..45b24035a11 100644 --- a/goalc/make/Tools.h +++ b/goalc/make/Tools.h @@ -78,3 +78,10 @@ class BuildLevelTool : public Tool { bool run(const ToolInput& task, const PathMap& path_map) override; bool needs_run(const ToolInput& task, const PathMap& path_map) override; }; + +class BuildLevel2Tool : public Tool { + public: + BuildLevel2Tool(); + bool run(const ToolInput& task, const PathMap& path_map) override; + bool needs_run(const ToolInput& task, const PathMap& path_map) override; +};