diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f5c272..ac87215 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(PortalGame "src/gfx/shader_sources.cpp" "src/gfx/texture.cpp" "src/gfx/texture.hpp" + "src/gfx/uniform_buffer.hpp" "src/gfx/vertex_array.cpp" "src/gfx/vertex_array.hpp" ) diff --git a/README.md b/README.md index 08d49ca..4bdb566 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ ## TODO Fix known issues: -- [ ] Fix apparently incorrect light radius of lights visible through scaled portals - [ ] Fix weird movement physics when having different rotation +- [x] Fix apparently incorrect light radius of lights visible through scaled portals - [x] Fix GL_LINEAR_MIPMAP_LINEAR error - [x] Use cached textures when loading meshes instead of always loading the textures again for each mesh Add new features: -- [ ] (Skeletal) Models and animations - [ ] Minimap for debugging - [ ] Text and UI rendering - [ ] Audio - [ ] Dynamic lights +- [x] (Skeletal) Models and animations diff --git a/blender/exportsimple.py b/blender/exportsimple.py index 3bca708..fd6942a 100644 --- a/blender/exportsimple.py +++ b/blender/exportsimple.py @@ -10,8 +10,25 @@ os.makedirs(out_path, exist_ok=True) roomnames = [] armatures = {} +armature_anims = {} -def export_mesh(obj, output_path, export_luv = False, export_bones = False): +def get_armature_keep_bones(armature: bpy.types.Armature): + keep_bones = set() + + for bone in armature.data.bones: + bone_name = bone.name + is_tag = re.match(r"Tag.(\w+)", bone_name) + + if bone.use_deform or is_tag: + keep_bones.add(bone) + + while bone.parent: + bone = bone.parent + keep_bones.add(bone) + + return keep_bones + +def export_mesh(obj, output_path, export_luv = False, armature = None): if obj.type != 'MESH': raise TypeError("Selected object is not a mesh") @@ -26,6 +43,11 @@ def export_mesh(obj, output_path, export_luv = False, export_bones = False): lightmap_layer = mesh.uv_layers.get("LightmapUV") lightmap_uv_data = lightmap_layer.data if lightmap_layer else None + export_bones = armature is not None + keep_bone_names = None + if export_bones: + keep_bone_names = set(bone.name for bone in get_armature_keep_bones(armature)) + # Vertex deduplication map and list vertex_map = {} # (position, normal, uv, lightmap_uv) -> vertex_index unique_vertices = [] # list of strings (v ...) @@ -33,6 +55,15 @@ def export_mesh(obj, output_path, export_luv = False, export_bones = False): # faces = [] # list of (v1, v2, v3) indices material_faces = {} # material_index -> list of face indices + bone_map = {} + unique_bones = [] + + def get_bone_index(bone_name): + if bone_name not in bone_map: + bone_map[bone_name] = len(unique_bones) + unique_bones.append(bone_name) + return bone_map[bone_name] + for tri in mesh.loop_triangles: face_indices = [] @@ -63,7 +94,32 @@ def export_mesh(obj, output_path, export_luv = False, export_bones = False): else: luv = (0.0, 0.0) - key = (pos, normal, uv, luv) + if export_bones: + deform_bones = [] + + for vert_group in vertex.groups: + weight = round(vert_group.weight, 3) + + if weight < 0.001: + continue + + group = obj.vertex_groups[vert_group.group] + + if group.name not in keep_bone_names: + continue + + deform_bones.append((group.name, weight)) + + deform_bones.sort(key=lambda x: x[1], reverse=True) + deform_bones = deform_bones[:4] + weight_sum = sum(w for _, w in deform_bones) + + bones_data = tuple((get_bone_index(name), w / weight_sum) for name, w in deform_bones) + + else: + bones_data = tuple() + + key = (pos, normal, uv, luv, bones_data) if key in vertex_map: index = vertex_map[key] @@ -81,6 +137,10 @@ def export_mesh(obj, output_path, export_luv = False, export_bones = False): if export_luv: vertex_line_comps.append(" ".join(f"{c:.3f}" for c in luv)) + if export_bones: + vertex_line_comps.append(str(len(bones_data))) + vertex_line_comps.append(" ".join(f"{b[0]} {b[1]:.3f}" for b in bones_data)) + vertex_line = "v " + " ".join(vertex_line_comps) unique_vertices.append(vertex_line) @@ -94,7 +154,13 @@ def export_mesh(obj, output_path, export_luv = False, export_bones = False): if export_luv: f.write("luv\n") - f.write(f"# x y z nx ny nz u v{' lu lv' if export_luv else ''}\n") + if export_bones: + f.write(f"skeleton {armature.name}\n") + + for bone in unique_bones: + f.write(f"d {bone}\n") + + f.write(f"# v {' ' if export_luv else ''}{' [...]' if export_bones else ''}\n") for v in unique_vertices: f.write(v + "\n") @@ -118,13 +184,13 @@ def rotation_str(rotation): return f"{rad_to_deg(rotation.x):.0f} {rad_to_deg(rotation.y):.0f} {rad_to_deg(rotation.z):.0f}" def rotation_str2(rotation): - return f"{rad_to_deg(rotation.x):.2f} {rad_to_deg(rotation.y):.2f} {rad_to_deg(rotation.z):.2f}" + return f"{rad_to_deg(rotation.x):.3f} {rad_to_deg(rotation.y):.3f} {rad_to_deg(rotation.z):.3f}" def position_str(position): - return f"{position.x:.3f} {position.y:.3f} {position.z:.3f}" + return f"{position.x:.4f} {position.y:.4f} {position.z:.4f}" def scale_str(scale): - return f"{scale.x:.3f}" + return f"{scale.x:.4f}" def matrix_decompose_str(matrix): translation, rotation, scale = matrix.decompose() @@ -133,28 +199,12 @@ def matrix_decompose_str(matrix): def get_path(name, ext): return os.path.join(out_path, f"{name}.{ext}") -def get_armature_keep_bones(armature: bpy.types.Armature): - keep_bones = set() - - for bone in armature.data.bones: - bone_name = bone.name - is_tag = re.match(r"Tag.(\w+)", bone_name) - - if bone.use_deform or is_tag: - keep_bones.add(bone) - - while bone.parent: - bone = bone.parent - keep_bones.add(bone) - - return keep_bones - def export_armature(armature: bpy.types.Armature, output_path: str): keep_bones = get_armature_keep_bones(armature) # Export armature data (bones, etc.) with open(output_path, 'w') as f: - f.write("# name parent x y z yaw pitch roll scale\n") + f.write("# b \n") for bone in armature.data.bones: if not bone in keep_bones: @@ -165,12 +215,16 @@ def export_armature(armature: bpy.types.Armature, output_path: str): bind_matrix = bone.matrix_local f.write(f"b {bone.name} {parent_name} {matrix_decompose_str(bind_matrix)}\n") + anims = armature_anims.get(armature.name, []) + for (anim_name, anim_file_name) in anims: + f.write(f"anim {anim_name} {anim_file_name}\n") + print(f"Exported Armature: {armature.name} to {output_path}") def vectors_similar(v1, v2, threshold): return (v1 - v2).length < threshold -def frames_similar(f1, f2, threshold=0.001): +def frames_similar(f1, f2, threshold=0.00001): _, t1, r1, s1, _ = f1 _, t2, r2, s2, _ = f2 @@ -197,8 +251,10 @@ def export_animation(action: bpy.types.Action, armature: bpy.types.Armature, out bone_frames = {bone.name: [] for bone in keep_bones} - start, end = map(int, action.frame_range) - for frame in range(start, end): + _, end = map(int, action.frame_range) + fps = bpy.context.scene.render.fps + + for frame in range(0, end): bpy.context.scene.frame_set(frame) bpy.context.view_layer.update() @@ -221,10 +277,13 @@ def export_animation(action: bpy.types.Action, armature: bpy.types.Armature, out if frames_similar(last_frame, current_frame): continue - + frame_list.append(current_frame) with open(output_path, 'w') as f: + f.write(f"frames {end}\n") + f.write(f"fps {fps}\n") + for bone_name, frames in bone_frames.items(): f.write(f"ch {bone_name}\n") @@ -274,17 +333,23 @@ for object in bpy.data.objects: if match: mesh_name = match.group(1) print(f"Found Non-Room Mesh: {mesh_name}") - export_mesh(object, get_path(mesh_name, "mesh")) - + + armature = None + parent = object.parent if parent and parent.type == 'ARMATURE': armatures[parent.name] = parent print(f" Is skeletal, Parent Armature: {parent.name}") + armature = parent + + export_mesh(object, get_path(mesh_name, "mesh"), armature=armature) + armature_names = [name for name, _ in armatures.items()] print("Armatures will be exported: ", armature_names) actions = {} +armature_anims = {name: [] for name in armature_names} for action in bpy.data.actions: match = re.search(r"Anim.(\w+).(\w+)", action.name) @@ -293,16 +358,19 @@ for action in bpy.data.actions: action_name = match.group(2) if action_armature in armatures: - actions[action.name] = (action, action_name, armatures[action_armature]) + armature = armatures[action_armature] + action_file_name = f"{armature.name}_{action_name}" + actions[action.name] = (action, action_file_name, armature) + armature_anims[action_armature].append((action_name, action_file_name)) action_names = [f"{data[1]} ({data[2].name})" for name, data in actions.items()] print("Actions will be exported: ", action_names) for armature_name, armature in armatures.items(): - export_armature(armature, get_path(armature_name, "sk")) + export_armature(armature, get_path(armature_name, "skel")) -for action_name, (action, simple_action_name, armature) in actions.items(): - export_animation(action, armature, get_path(f"{armature.name}_{simple_action_name}", "anim")) +for action_name, (action, action_file_name, armature) in actions.items(): + export_animation(action, armature, get_path(action_file_name, "anim")) with open(get_path("rooms", "list"), 'w') as rooms_file: for room_name in roomnames: diff --git a/src/app.cpp b/src/app.cpp index 551de48..d3e2d37 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -34,7 +34,7 @@ App::App() // scaling hallway size_t s3i = world_.AddSector(room003); game::Sector& s3 = world_.GetSector(s3i); - s3.AddLight(glm::vec3(0.0f, 0.0f, 1.8f), glm::vec3(1.0f, 0.0f, 0.0f), 3.0f); + s3.AddLight(glm::vec3(1.0f, 0.0f, 1.8f), glm::vec3(1.0f, 0.0f, 0.0f), 3.0f); // purple size_t s4i = world_.AddSector(room001); @@ -71,6 +71,9 @@ App::App() world_.Bake(); + auto skmesh = assets::Mesh::LoadFromFile("data/hands.mesh", false); + + player_ = world_.Spawn(s1i, glm::vec3(0.0f, 0.0f, 1.0f)); } @@ -94,14 +97,15 @@ void App::Frame() renderer_.Begin(viewport_size_.x, viewport_size_.y); player_->SetInput(input_); - player_->Update(delta_time); + + world_.Update(delta_time); //const auto& position = player_->GetOccurrence().GetPosition(); size_t sector_idx; glm::vec3 position, forward, up; player_->GetPOV(sector_idx, position, forward, up); - renderer_.DrawWorld(world_, sector_idx, position, forward, up, aspect, 60.0f); + renderer_.DrawWorld(world_, sector_idx, player_, position, forward, up, aspect, 60.0f); } void App::MouseMove(const glm::vec2& delta) diff --git a/src/assets/mesh.cpp b/src/assets/mesh.cpp index 546a5be..9687182 100644 --- a/src/assets/mesh.cpp +++ b/src/assets/mesh.cpp @@ -55,19 +55,16 @@ assets::Mesh::Mesh(std::span verts, std::span tris, st if (flags & MF_IS_SKELETAL) { - // Bone indices as 4 ints - int32_t bone_indices[4] = { 0, 0, 0, 0 }; - float bone_weights[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + for (int i = 0; i < 4; ++i) + { + BufferPut(buffer, static_cast(vert.bones[i].bone_index)); + } for (int i = 0; i < 4; ++i) { - bone_indices[i] = static_cast(vert.bones[i].bone_index); - bone_weights[i] = vert.bones[i].weight; + BufferPut(buffer, vert.bones[i].weight); } - - BufferPut(buffer, bone_indices); - BufferPut(buffer, bone_weights); - } + } } va_.SetVBOData(buffer.data(), buffer.size()); diff --git a/src/game/entity.cpp b/src/game/entity.cpp index 5cf0152..5199d2c 100644 --- a/src/game/entity.cpp +++ b/src/game/entity.cpp @@ -1,11 +1,17 @@ #include "entity.hpp" #include "world.hpp" +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include game::Entity::Entity(World* world, size_t sector_idx, const CapsuleShape& capsule_shape, const glm::vec3& position) : world_(world), capsule_(capsule_shape), velocity_(0.0f), - touching_portal_(nullptr) + touching_portal_(nullptr), + light_trace_offset_(0.0f, 0.0f, capsule_shape.height * 0.5f), + lights_valid_(false) { CreateOccurrenceParams occu_params; occu_params.sector = &world->GetSector(sector_idx); @@ -122,6 +128,70 @@ void game::Entity::Move(glm::vec3& velocity, float dt) void game::Entity::Update(float dt) { Move(velocity_, dt); + lights_valid_ = false; +} + +void game::Entity::UpdateLights() +{ + if (lights_valid_) + { + return; // Already updated since last update + } + + const SectorEnvironment& env = occu_->sector_->GetEnvironment(); + glm::vec3 trace_pos = occu_->position_ + occu_->basis_ * light_trace_offset_; + + static std::vector lights; + lights.clear(); + occu_->sector_->GetLightsAt(trace_pos, lights); + + // select only closest SD_MAX_LIGHTS lights + if (lights.size() > SD_MAX_LIGHTS) + { + std::partial_sort( + lights.begin(), + lights.begin() + SD_MAX_LIGHTS, + lights.end(), + [&](const Light& a, const Light& b) + { + float aa = glm::distance2(a.position, trace_pos) / (a.radius * a.radius); + float ab = glm::distance2(b.position, trace_pos) / (b.radius * b.radius); + return aa < ab; + } + ); + + lights.resize(SD_MAX_LIGHTS); + } + + // Primary occurence + occu_->lights_.ambient = env.ambient; + occu_->lights_.num_lights = lights.size(); + for (size_t i = 0; i < lights.size(); ++i) + { + const Light& light = lights[i]; + occu_->lights_.colors_rs[i] = glm::vec4(light.color, light.radius); + occu_->lights_.positions[i] = light.position; + } + + // Other occurence + if (other_occu_ && touching_portal_) + { + other_occu_->lights_.ambient = env.ambient; + other_occu_->lights_.num_lights = lights.size(); + for (size_t i = 0; i < lights.size(); ++i) + { + const Light& light = lights[i]; + + // transform pos through portal + glm::vec3 new_pos = touching_portal_->tr_position * glm::vec4(light.position, 1.0f); + float new_radius = light.radius * touching_portal_->tr_scale; + + other_occu_->lights_.colors_rs[i] = glm::vec4(light.color, new_radius); + other_occu_->lights_.positions[i] = new_pos; + } + } + + lights_valid_ = true; } void game::Entity::CreateOtherOccurence(const Portal& portal) @@ -184,3 +254,9 @@ bool game::EntityOccurrence::Sweep(const glm::vec3& target_position, float& hit_ hit_normal ); } + +const game::LightInfluences& game::EntityOccurrence::GetLights() +{ + entity_->UpdateLights(); + return lights_; +} diff --git a/src/game/entity.hpp b/src/game/entity.hpp index 0a4b345..30f00af 100644 --- a/src/game/entity.hpp +++ b/src/game/entity.hpp @@ -2,6 +2,7 @@ #include #include "sector.hpp" +#include "gfx/shader_defs.hpp" namespace game { @@ -14,6 +15,15 @@ namespace game float height; }; + struct LightInfluences + { + glm::vec3 ambient; + // Point lights + glm::vec4 colors_rs[SD_MAX_LIGHTS]; // rgb = color, a = r + glm::vec3 positions[SD_MAX_LIGHTS]; + size_t num_lights = 0; + }; + struct CreateOccurrenceParams { Sector* sector; @@ -38,6 +48,8 @@ namespace game btCapsuleShapeZ bt_capsule_; + LightInfluences lights_; + friend class Entity; public: @@ -46,6 +58,8 @@ namespace game // pos in sector space bool Sweep(const glm::vec3& target_position, float& hit_fraction, glm::vec3& hit_normal, const Portal** hit_portal); + const LightInfluences& GetLights(); + const Sector& GetSector() const { return *sector_; } const glm::vec3& GetPosition() const { return position_; } @@ -66,6 +80,9 @@ namespace game glm::vec3 velocity_; + glm::vec3 light_trace_offset_; // In entity space + bool lights_valid_; + public: Entity(World* world, size_t sector_idx, const CapsuleShape& capsule_shape, const glm::vec3& position); @@ -76,6 +93,8 @@ namespace game void SetVelocity(const glm::vec3& velocity) { velocity_ = velocity; } + void UpdateLights(); + const glm::vec3& GetVelocity() const { return velocity_; } const CapsuleShape& GetCapsuleShape() const { return capsule_; } EntityOccurrence& GetOccurrence() { return *occu_; } diff --git a/src/game/meshinstance.cpp b/src/game/meshinstance.cpp index 7147d9a..6109e44 100644 --- a/src/game/meshinstance.cpp +++ b/src/game/meshinstance.cpp @@ -1,4 +1,5 @@ #include "meshinstance.hpp" +#include "gfx/shader_defs.hpp" #include @@ -7,9 +8,13 @@ game::MeshInstance::MeshInstance(std::shared_ptr mesh, const root_node_(root_node), time_ptr_(time_ptr) { - if (IsSkeletal()) + skeletal_ = (bool)mesh_->GetSkeleton(); + + if (skeletal_) { SetupBoneNodes(); + bone_ubo_ = std::make_unique>(); + bone_ubo_->SetData(nullptr, SD_MAX_BONES); // allocate max size } } @@ -75,13 +80,43 @@ void game::MeshInstance::PlayAnim(const std::string& name) void game::MeshInstance::Update() { - if (IsSkeletal()) + if (skeletal_) { ApplyAnimFrame(); UpdateBoneMatrices(); + bone_ubo_dirty_ = true; } } +void game::MeshInstance::UpdateBoneUBO() +{ + if (!bone_ubo_ || !bone_ubo_dirty_) + { + return; // No need to update + } + + static glm::mat4 skin_mats[SD_MAX_BONES]; + + const auto& skeleton = mesh_->GetSkeleton(); + size_t num_mats = std::min(skeleton->GetNumBones(), static_cast(SD_MAX_BONES)); + + for (size_t i = 0; i < num_mats; ++i) + { + const auto& bone = skeleton->GetBone(i); + const TransformNode& node = bone_nodes_[i]; + skin_mats[i] = node.matrix * bone.inv_bind_matrix; + } + + bone_ubo_->SetData(skin_mats, num_mats); + bone_ubo_dirty_ = false; +} + +const gfx::UniformBuffer& game::MeshInstance::GetBoneUBO() +{ + UpdateBoneUBO(); + return *bone_ubo_; +} + void game::MeshInstance::ApplyAnimFrame() { ApplySkelAnim(current_anim_, anim_time_, 1.0f); diff --git a/src/game/meshinstance.hpp b/src/game/meshinstance.hpp index bd8c192..33c4fdb 100644 --- a/src/game/meshinstance.hpp +++ b/src/game/meshinstance.hpp @@ -2,30 +2,37 @@ #include "assets/mesh.hpp" #include "transform_node.hpp" +#include "gfx/uniform_buffer.hpp" namespace game { class MeshInstance { std::shared_ptr mesh_; + bool skeletal_; const TransformNode* root_node_ = nullptr; std::vector bone_nodes_; // Only used if the mesh is skeletal const float* time_ptr_ = nullptr; // Pointer to game time variable + std::unique_ptr> bone_ubo_; // Only used if the mesh is skeletal + bool bone_ubo_dirty_ = true; + const assets::Animation* current_anim_ = nullptr; float anim_time_ = 0.0f; public: MeshInstance(std::shared_ptr mesh, const TransformNode* root_node, const float* time_ptr); - bool IsSkeletal() const { return (bool)mesh_->GetSkeleton(); } + bool IsSkeletal() const { return skeletal_; } void PlayAnim(const std::string& name); void Update(); + const gfx::UniformBuffer& GetBoneUBO(); + const std::shared_ptr& GetMesh() const { return mesh_; } const TransformNode* GetRootNode() const { return root_node_; } const TransformNode& GetBoneNode(size_t index) const { return bone_nodes_[index]; } @@ -35,6 +42,7 @@ namespace game void SetupBoneNodes(); void UpdateBoneMatrices(); + void UpdateBoneUBO(); protected: virtual void ApplyAnimFrame(); diff --git a/src/game/player.cpp b/src/game/player.cpp index 2f7456e..103e8d1 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -19,7 +19,9 @@ game::Player::Player(World* world, size_t sector_idx, const glm::vec3& position) world->GetTimePtr() ); - vm_hands_.PlayAnim("knife_attack1"); + vm_hands_.PlayAnim("knife_idle"); + + light_trace_offset_ = glm::vec3(0.0f, 0.0f, 0.6f); } void game::Player::Rotate(float delta_yaw, float delta_pitch) @@ -38,6 +40,7 @@ void game::Player::Update(float dt) { time_ += dt; + vm_node_.UpdateMatrix(); vm_hands_.Update(); if (vm_weapon_) @@ -122,18 +125,25 @@ void game::Player::Update(float dt) } -void game::Player::GetPOV(size_t& sector_idx, glm::vec3& position, glm::vec3& forward, glm::vec3& up) const +void game::Player::GetPOV(size_t& sector_idx, glm::vec3& position, glm::vec3& forward, glm::vec3& up) { sector_idx = occu_->GetSector().GetIndex(); glm::vec2 bobbing_offset = GetBobbingOffset(time_, 0.01f * current_speed_); glm::vec3 offset = cam_up_ * (0.7f + bobbing_offset.y) + cam_right_ * bobbing_offset.x; + glm::vec2 vm_offset = bobbing_offset * 0.2f; + vm_offset.y -= 0.01f * current_speed_; + vm_node_.local_transform.position = glm::vec3(vm_offset.x, 0.0f, vm_offset.y - 0.2f); + vm_node_.UpdateMatrix(); + position = occu_->GetPosition() + offset; forward = cam_forward_; up = cam_up_; + other_pov_ = false; + if (touching_portal_ && touching_portal_->link) { float sd = glm::dot(glm::vec3(touching_portal_->plane), position) + touching_portal_->plane.w; @@ -146,11 +156,12 @@ void game::Player::GetPOV(size_t& sector_idx, glm::vec3& position, glm::vec3& fo forward = touching_portal_->tr_basis * forward; up = touching_portal_->tr_basis * up; + other_pov_ = true; } } } -void game::Player::GetViewMeshes(std::vector& out_meshes) const +void game::Player::GetViewMeshes(std::vector& out_meshes) { out_meshes.push_back(&vm_hands_); @@ -160,6 +171,17 @@ void game::Player::GetViewMeshes(std::vector& out_meshes) c } } +const game::LightInfluences& game::Player::GetViewLights() +{ + // Viewed from other occurrence sector + if (other_pov_ && other_occu_) + { + return other_occu_->GetLights(); + } + + return occu_->GetLights(); +} + glm::vec2 game::Player::GetBobbingOffset(float t, float amplitude) { // Frequency and amplitude can be adjusted to tweak the effect diff --git a/src/game/player.hpp b/src/game/player.hpp index e8259fa..b82e414 100644 --- a/src/game/player.hpp +++ b/src/game/player.hpp @@ -26,11 +26,13 @@ namespace game float time_ = 0.0f; float current_speed_ = 0.0f; + bool other_pov_ = false; // True is viewed from other occurence sector (camera behind portal plane) + // Viewmodel stuff TransformNode vm_node_; // Affected by bobbing MeshInstance vm_hands_; std::unique_ptr vm_weapon_; // Can be null - + public: Player(World* world, size_t sector_idx, const glm::vec3& position); @@ -41,9 +43,10 @@ namespace game virtual void Update(float dt) override; - void GetPOV(size_t& sector_idx, glm::vec3& position, glm::vec3& forward, glm::vec3& up) const; + void GetPOV(size_t& sector_idx, glm::vec3& position, glm::vec3& forward, glm::vec3& up); - void GetViewMeshes(std::vector& out_meshes) const; + void GetViewMeshes(std::vector& out_meshes); + const LightInfluences& GetViewLights(); private: static glm::vec2 GetBobbingOffset(float t, float amplitude); diff --git a/src/game/sector.cpp b/src/game/sector.cpp index 74220bd..379afd0 100644 --- a/src/game/sector.cpp +++ b/src/game/sector.cpp @@ -74,6 +74,8 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptrtr_position * glm::vec4(ext_light.position, 1.0f)); // Transform light position to this sector's space - light.radius = ext_light.radius * portal.tr_scale; // Scale the radius + light.radius = ext_light.radius / portal.tr_scale; // Scale the radius light.through_portal = &portal; all_lights_.push_back(light); @@ -479,7 +481,6 @@ void game::Sector::GenerateAllLights() void game::Sector::BakeLightmap() { const size_t lightmap_size = 128; - const glm::vec3 ambient_light(0.2f); // Ambient light color const float margin = 1.0f; std::span mesh_verts = mesh_->GetVertices(); @@ -583,7 +584,7 @@ void game::Sector::BakeLightmap() glm::vec3 texel_pos_ws = u * vert_pos[0] + v * vert_pos[1] + w * vert_pos[2]; glm::vec3 texel_norm_ws = glm::normalize(u * vert_norm[0] + v * vert_norm[1] + w * vert_norm[2]); - glm::vec3 light_color = ambient_light; + glm::vec3 light_color = env_.ambient; // Start with ambient light lights.clear(); GetLightsAt(texel_pos_ws, lights); diff --git a/src/game/sector.hpp b/src/game/sector.hpp index 654168f..f0cb62d 100644 --- a/src/game/sector.hpp +++ b/src/game/sector.hpp @@ -62,6 +62,14 @@ namespace game const Portal* through_portal; }; + struct SectorEnvironment + { + glm::vec3 ambient; + bool has_sun = false; + glm::vec3 sun_direction; + glm::vec3 sun_color; + }; + class Sector { World* world_; @@ -70,6 +78,7 @@ namespace game std::shared_ptr mesh_; + SectorEnvironment env_; std::vector lights_; // Light in this sector std::vector all_lights_; // Lights in this sector and linked sectors @@ -99,6 +108,7 @@ namespace game const std::shared_ptr& GetMesh() const { return mesh_; } const std::shared_ptr& GetLightmap() const { return lightmap_; } btCollisionWorld& GetBtWorld() { return bt_world_; } + const SectorEnvironment& GetEnvironment() const { return env_; } int GetPortalIndex(const std::string& name) const; const Portal& GetPortal(size_t idx) const { return portals_[idx]; } diff --git a/src/game/world.cpp b/src/game/world.cpp index a01ee91..c84338f 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -35,3 +35,13 @@ void game::World::Bake() sector->Bake(); } } + +void game::World::Update(float dt) +{ + time_ += dt; + + for (auto& entity : entities_) + { + entity->Update(dt); + } +} \ No newline at end of file diff --git a/src/game/world.hpp b/src/game/world.hpp index 8eb66b3..63ed4c7 100644 --- a/src/game/world.hpp +++ b/src/game/world.hpp @@ -32,6 +32,8 @@ namespace game entities_.push_back(std::move(entity)); return entity_ptr; } + + void Update(float dt); Sector& GetSector(size_t idx) { return *sectors_[idx]; } const Sector& GetSector(size_t idx) const { return *sectors_[idx]; } diff --git a/src/gfx/renderer.cpp b/src/gfx/renderer.cpp index ed6569c..ee44d94 100644 --- a/src/gfx/renderer.cpp +++ b/src/gfx/renderer.cpp @@ -12,6 +12,7 @@ gfx::Renderer::Renderer() ShaderSources::MakeShader(sector_shader_.shader, SS_SECTOR_MESH_VERT, SS_SECTOR_MESH_FRAG); ShaderSources::MakeShader(portal_shader_.shader, SS_PORTAL_VERT, SS_PORTAL_FRAG); ShaderSources::MakeShader(mesh_shader_.shader, SS_MESH_VERT, SS_MESH_FRAG); + ShaderSources::MakeShader(skel_mesh_shader_.shader, SS_SKEL_MESH_VERT, SS_SKEL_MESH_FRAG); proj_ = glm::mat4(1.0f); // Initialize projection matrix to identity SetupPortalVAO(); @@ -40,6 +41,7 @@ void gfx::Renderer::Begin(size_t width, size_t height) void gfx::Renderer::DrawWorld( const game::World& world, size_t sector_idx, + game::Player* player, const glm::vec3& eye, const glm::vec3& dir, const glm::vec3& up, @@ -93,16 +95,51 @@ void gfx::Renderer::DrawWorld( DrawSector(params); - //glDisable(GL_STENCIL_TEST); + glDisable(GL_STENCIL_TEST); //glDisable(GL_CULL_FACE); //glDisable(GL_DEPTH_TEST); + if (player) + { + draw_meshes_.clear(); + player->GetViewMeshes(draw_meshes_); + + if (!draw_meshes_.empty()) + { + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // Clear depth and stencil for viewmodel + + //glm::mat4 transform = glm::inverse(view); + + // model is -Y forward, Z up, X right + glm::mat4 transform = glm::mat4( + -1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); + + // transform to camera + transform = glm::inverse(view) * transform; + + const game::LightInfluences& lights = player->GetViewLights(); + + for (auto mesh : draw_meshes_) + { + DrawMeshInstance(params, *mesh, transform, lights); + } + + } + } + + } void gfx::Renderer::UnreadyShaders() { portal_shader_.setup_sector = 0; sector_shader_.setup_sector = 0; + mesh_shader_.setup_sector = 0; + skel_mesh_shader_.setup_sector = 0; } void gfx::Renderer::SetupSectorShader(const DrawSectorParams& params, SectorShader& sshader) @@ -382,3 +419,46 @@ void gfx::Renderer::DrawPortalPlane(const DrawSectorParams& params, const game:: glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // Draw the portal as a quad } +void gfx::Renderer::DrawMeshInstance(const DrawSectorParams& params, game::MeshInstance& mesh_instance, const glm::mat4& transform, const game::LightInfluences& lights) +{ + SectorShader* sshader = &mesh_shader_; + bool skeletal = mesh_instance.IsSkeletal(); + + if (skeletal) + { + sshader = &skel_mesh_shader_; + glBindBufferBase(GL_UNIFORM_BUFFER, 0, mesh_instance.GetBoneUBO().GetId()); + } + + SetupSectorShader(params, *sshader); + + const game::TransformNode* mesh_root = mesh_instance.GetRootNode(); + if (mesh_root && !skeletal) // Skeletal meshes have root transform applied in bone matrices + { + glm::mat4 combined = transform * mesh_root->matrix; + glUniformMatrix4fv(sshader->shader->U(gfx::SU_MODEL), 1, GL_FALSE, &combined[0][0]); + } + else + { + glUniformMatrix4fv(sshader->shader->U(gfx::SU_MODEL), 1, GL_FALSE, &transform[0][0]); + } + + // setup lighting + glUniform3fv(sshader->shader->U(gfx::SU_AMBIENT_LIGHT), 1, &lights.ambient[0]); + glUniform1i(sshader->shader->U(gfx::SU_NUM_LIGHTS), (GLint)lights.num_lights); + if (lights.num_lights > 0) + { + glUniform3fv(sshader->shader->U(gfx::SU_LIGHT_POSITIONS), (GLint)lights.num_lights, &lights.positions[0].x); + glUniform4fv(sshader->shader->U(gfx::SU_LIGHT_COLORS_RS), (GLint)lights.num_lights, &lights.colors_rs[0].x); + } + + const assets::Mesh& mesh = *mesh_instance.GetMesh(); + + glBindVertexArray(mesh.GetVA().GetVAOId()); + glActiveTexture(GL_TEXTURE0); + for (auto mesh_materials = mesh.GetMaterials(); const auto& mat : mesh_materials) { + BindTexture(mat.texture.get()); // diffuse texture + glDrawElements(GL_TRIANGLES, mat.num_tris * 3, GL_UNSIGNED_INT, (void*)(mat.first_tri * 3 * sizeof(uint32_t))); + } +} + diff --git a/src/gfx/renderer.hpp b/src/gfx/renderer.hpp index 27919f4..906f377 100644 --- a/src/gfx/renderer.hpp +++ b/src/gfx/renderer.hpp @@ -5,6 +5,7 @@ #include "game/world.hpp" #include "game/sector.hpp" #include "game/meshinstance.hpp" +#include "game/player.hpp" namespace gfx { @@ -37,6 +38,7 @@ namespace gfx void DrawWorld( const game::World& world, size_t sector_idx, + game::Player* player, const glm::vec3& eye, const glm::vec3& dir, const glm::vec3& up, @@ -47,6 +49,7 @@ namespace gfx SectorShader sector_shader_; SectorShader portal_shader_; SectorShader mesh_shader_; + SectorShader skel_mesh_shader_; std::shared_ptr portal_vao_; void SetupPortalVAO(); @@ -60,6 +63,7 @@ namespace gfx glm::mat4 proj_; size_t last_sector_id; const Shader* current_shader_; + std::vector draw_meshes_; void UnreadyShaders(); void SetupSectorShader(const DrawSectorParams& params, SectorShader& sshader); @@ -70,7 +74,7 @@ namespace gfx static bool ComputeQuadScreenAABB(const glm::vec3* verts, const glm::mat4 view_proj, collision::AABB2& aabb); void DrawPortalPlane(const DrawSectorParams& params, const game::Portal& portal, const glm::mat4& trans, bool clear_depth); - void DrawMeshInstance(const DrawSectorParams& params, const game::MeshInstance& mesh_instance, const glm::mat4& model); + void DrawMeshInstance(const DrawSectorParams& params, game::MeshInstance& mesh_instance, const glm::mat4& transform, const game::LightInfluences& lights); }; diff --git a/src/gfx/shader.cpp b/src/gfx/shader.cpp index 3cbd35a..2433134 100644 --- a/src/gfx/shader.cpp +++ b/src/gfx/shader.cpp @@ -12,6 +12,10 @@ static const char* const s_uni_names[] = { "u_portal_size", // SU_PORTAL_SIZE "u_clear_depth", // SU_CLEAR_DEPTH "u_lightmap_tex", // SU_LIGHTMAP_TEX + "u_num_lights", // SU_NUM_LIGHTS + "u_light_positions", // SU_LIGHT_POSITIONS + "u_light_colors_rs", // SU_LIGHT_COLORS_RS + "u_ambient_light", // SU_AMBIENT_LIGHT }; // Vytvori shader z daneho zdroje @@ -81,7 +85,7 @@ gfx::Shader::Shader(const char* vert_src, const char* frag_src) { for (size_t i = 0; i < SU_COUNT; i++) m_uni[i] = glGetUniformLocation(m_id, s_uni_names[i]); - SetupTextureBindings(); + SetupBindings(); } gfx::Shader::~Shader() { @@ -89,12 +93,19 @@ gfx::Shader::~Shader() { glDeleteProgram(m_id); } -void gfx::Shader::SetupTextureBindings() +void gfx::Shader::SetupBindings() { glUseProgram(m_id); glUniform1i(m_uni[SU_TEX], 0); glUniform1i(m_uni[SU_LIGHTMAP_TEX], 1); + // Bones UBO + int ubo_index = glGetUniformBlockIndex(m_id, "Bones"); + if (ubo_index != GL_INVALID_INDEX) + { + glUniformBlockBinding(m_id, ubo_index, 0); // bind to binding point 0 + } + glUseProgram(0); } diff --git a/src/gfx/shader.hpp b/src/gfx/shader.hpp index 5a5d38b..3e5d4c8 100644 --- a/src/gfx/shader.hpp +++ b/src/gfx/shader.hpp @@ -17,6 +17,10 @@ namespace gfx SU_PORTAL_SIZE, SU_CLEAR_DEPTH, SU_LIGHTMAP_TEX, + SU_NUM_LIGHTS, + SU_LIGHT_POSITIONS, + SU_LIGHT_COLORS_RS, + SU_AMBIENT_LIGHT, SU_COUNT }; @@ -49,7 +53,7 @@ namespace gfx GLuint GetId() const { return m_id; } private: - void SetupTextureBindings(); + void SetupBindings(); }; } diff --git a/src/gfx/shader_defs.hpp b/src/gfx/shader_defs.hpp new file mode 100644 index 0000000..ea48079 --- /dev/null +++ b/src/gfx/shader_defs.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define SD_MAX_LIGHTS 4 +#define SD_MAX_BONES 256 diff --git a/src/gfx/shader_sources.cpp b/src/gfx/shader_sources.cpp index 95703c1..da617bc 100644 --- a/src/gfx/shader_sources.cpp +++ b/src/gfx/shader_sources.cpp @@ -1,4 +1,5 @@ #include "shader_sources.hpp" +#include "shader_defs.hpp" #ifndef EMSCRIPTEN #define GLSL_VERSION \ @@ -11,11 +12,58 @@ "\n" #endif +#define STRINGIFY_HELPER(x) #x +#define STRINGIFY(x) STRINGIFY_HELPER(x) + +#define SHADER_DEFS \ + "#define MAX_LIGHTS " STRINGIFY(SD_MAX_LIGHTS) "\n" \ + "#define MAX_BONES " STRINGIFY(SD_MAX_BONES) "\n" \ + "\n" + +#define SHADER_HEADER \ + GLSL_VERSION \ + SHADER_DEFS + +#define MESH_MATRICES_GLSL R"GLSL( + uniform mat4 u_view_proj; + uniform vec4 u_clip_plane; + uniform mat4 u_model; // Transform matrix +)GLSL" + +#define LIGHT_MATRICES_GLSL R"GLSL( + uniform vec3 u_ambient_light; + uniform int u_num_lights; + uniform vec3 u_light_positions[MAX_LIGHTS]; + uniform vec4 u_light_colors_rs[MAX_LIGHTS]; // rgb = color, a = radius +)GLSL" + +#define COMPUTE_LIGHTS_GLSL R"GLSL( + vec3 ComputeLights(in vec3 sector_pos, in vec3 sector_normal) + { + vec3 color = u_ambient_light; + for (int i = 0; i < u_num_lights; ++i) { + vec3 light_pos = u_light_positions[i]; + vec3 light_color = u_light_colors_rs[i].rgb; + float light_radius = u_light_colors_rs[i].a; + + vec3 to_light = light_pos - sector_pos.xyz; + float dist2 = dot(to_light, to_light); + if (dist2 < light_radius * light_radius) { + float dist = sqrt(dist2); + float attenuation = 1.0 - (dist / light_radius); + float dot = max(dot(sector_normal, normalize(to_light)), 0.0); + color += light_color * dot * attenuation; + } + } + return color; + } +)GLSL" + // Zdrojove kody shaderu static const char* const s_srcs[] = { // SS_SECTOR_MESH_VERT -GLSL_VERSION +SHADER_HEADER R"GLSL( layout (location = 0) in vec3 a_pos; layout (location = 1) in vec3 a_normal; @@ -43,7 +91,7 @@ void main() { )GLSL", // SS_SECTOR_MESH_FRAG -GLSL_VERSION +SHADER_HEADER R"GLSL( in vec2 v_uv; in vec2 v_lightmap_uv; @@ -67,7 +115,7 @@ void main() { )GLSL", // SS_PORTAL_VERT -GLSL_VERSION +SHADER_HEADER R"GLSL( layout (location = 0) in vec3 a_pos; @@ -90,7 +138,7 @@ void main() { )GLSL", // SS_PORTAL_FRAG -GLSL_VERSION +SHADER_HEADER R"GLSL( in float v_clip_distance; @@ -113,16 +161,17 @@ void main() { )GLSL", // SS_MESH_VERT -GLSL_VERSION +SHADER_HEADER R"GLSL( layout (location = 0) in vec3 a_pos; layout (location = 1) in vec3 a_normal; layout (location = 3) in vec2 a_uv; -uniform mat4 u_view_proj; -uniform vec4 u_clip_plane; - -uniform mat4 u_model; // Transform matrix +)GLSL" +MESH_MATRICES_GLSL +LIGHT_MATRICES_GLSL +COMPUTE_LIGHTS_GLSL +R"GLSL( out vec2 v_uv; out float v_clip_distance; @@ -131,6 +180,7 @@ out vec3 v_color; void main() { vec4 sector_pos = u_model * vec4(a_pos, 1.0); + vec3 sector_normal = normalize(mat3(u_model) * a_normal); gl_Position = u_view_proj * sector_pos; // Clip against the plane @@ -139,12 +189,82 @@ void main() { //v_normal = mat3(u_model) * a_normal; v_uv = vec2(a_uv.x, 1.0 - a_uv.y); - v_color = vec3(1.0, 1.0, 1.0); + v_color = ComputeLights(sector_pos.xyz, sector_normal); } )GLSL", // SS_MESH_FRAG -GLSL_VERSION +SHADER_HEADER +R"GLSL( +in vec2 v_uv; +in float v_clip_distance; + +in vec3 v_color; + +uniform sampler2D u_tex; + +layout (location = 0) out vec4 o_color; + +void main() { + if (v_clip_distance < 0.0) { + discard; // Discard fragment if it is outside the clip plane + } + + o_color = vec4(texture(u_tex, v_uv)); + o_color.rgb *= v_color; // Apply vertex color + //o_color = vec4(1.0, 0.0, 0.0, 1.0); +} + +)GLSL", + +// SS_SKEL_MESH_VERT +SHADER_HEADER +R"GLSL( +layout (location = 0) in vec3 a_pos; +layout (location = 1) in vec3 a_normal; +layout (location = 3) in vec2 a_uv; +layout (location = 5) in ivec4 a_bone_ids; +layout (location = 6) in vec4 a_bone_weights; + +layout (std140) uniform Bones { + mat4 u_bone_matrices[MAX_BONES]; +}; + +)GLSL" +MESH_MATRICES_GLSL +LIGHT_MATRICES_GLSL +COMPUTE_LIGHTS_GLSL +R"GLSL( + +out vec2 v_uv; +out float v_clip_distance; + +out vec3 v_color; + +void main() { + mat4 bone_transform = mat4(0.0); + for (int i = 0; i < 4; ++i) { + int bone_id = a_bone_ids[i]; + if (bone_id >= 0) { + bone_transform += u_bone_matrices[bone_id] * a_bone_weights[i]; + } + } + + vec4 sector_pos = u_model * bone_transform * vec4(a_pos, 1.0); + vec3 sector_normal = normalize(mat3(u_model) * mat3(bone_transform) * a_normal); + gl_Position = u_view_proj * sector_pos; + + // Clip against the plane + v_clip_distance = dot(sector_pos, u_clip_plane); + + v_uv = vec2(a_uv.x, 1.0 - a_uv.y); + + v_color = ComputeLights(sector_pos.xyz, sector_normal); +} +)GLSL", + +// SS_SKEL_MESH_FRAG +SHADER_HEADER R"GLSL( in vec2 v_uv; in float v_clip_distance; diff --git a/src/gfx/shader_sources.hpp b/src/gfx/shader_sources.hpp index 9992794..b7f44a3 100644 --- a/src/gfx/shader_sources.hpp +++ b/src/gfx/shader_sources.hpp @@ -16,6 +16,9 @@ namespace gfx SS_MESH_VERT, SS_MESH_FRAG, + SS_SKEL_MESH_VERT, + SS_SKEL_MESH_FRAG, + }; class ShaderSources diff --git a/src/gfx/uniform_buffer.hpp b/src/gfx/uniform_buffer.hpp new file mode 100644 index 0000000..865d5d5 --- /dev/null +++ b/src/gfx/uniform_buffer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "buffer_object.hpp" + +namespace gfx +{ + template + class UniformBuffer : public BufferObject + { + public: + UniformBuffer() : BufferObject(GL_UNIFORM_BUFFER, GL_DYNAMIC_DRAW) + { + } + + void SetData(const T* data, size_t count = 1) + { + BufferObject::SetData(data, sizeof(T) * count); + } + + }; +} \ No newline at end of file diff --git a/src/gfx/vertex_array.cpp b/src/gfx/vertex_array.cpp index 538e1f3..a3bea34 100644 --- a/src/gfx/vertex_array.cpp +++ b/src/gfx/vertex_array.cpp @@ -23,7 +23,7 @@ static const VertexAttribInfo s_ATTR_INFO[] = { }; // Pocet typu vertex atributu -static const size_t s_ATTR_COUNT = 5; +static const size_t s_ATTR_COUNT = 7; gfx::VertexArray::VertexArray(int attrs, int flags) : m_usage(GL_STATIC_DRAW), m_num_indices(0) { glGenVertexArrays(1, &m_vao);