Add skeletal meshes, fix lighting a bit

This commit is contained in:
tovjemam 2025-09-05 20:00:19 +02:00
parent e9883b8fb5
commit 251871c776
24 changed files with 580 additions and 77 deletions

View File

@ -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"
)

View File

@ -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

View File

@ -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 <x y z> <nx ny nz> <u v>{' <lu lv>' if export_luv else ''}{' <num_bones> [<idx w>...]' 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 <name> <parent> <x y z> <yaw pitch roll> <scale>\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()
@ -225,6 +281,9 @@ def export_animation(action: bpy.types.Action, armature: bpy.types.Armature, out
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:

View File

@ -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<game::Player>(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)

View File

@ -55,18 +55,15 @@ assets::Mesh::Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> 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<int32_t>(vert.bones[i].bone_index));
}
for (int i = 0; i < 4; ++i)
{
bone_indices[i] = static_cast<int32_t>(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);
}
}

View File

@ -1,11 +1,17 @@
#include "entity.hpp"
#include "world.hpp"
#include <algorithm>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/norm.hpp>
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<Light> 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_;
}

View File

@ -2,6 +2,7 @@
#include <memory>
#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_; }

View File

@ -1,4 +1,5 @@
#include "meshinstance.hpp"
#include "gfx/shader_defs.hpp"
#include <stdexcept>
@ -7,9 +8,13 @@ game::MeshInstance::MeshInstance(std::shared_ptr<const assets::Mesh> mesh, const
root_node_(root_node),
time_ptr_(time_ptr)
{
if (IsSkeletal())
skeletal_ = (bool)mesh_->GetSkeleton();
if (skeletal_)
{
SetupBoneNodes();
bone_ubo_ = std::make_unique<gfx::UniformBuffer<glm::mat4>>();
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<size_t>(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<glm::mat4>& game::MeshInstance::GetBoneUBO()
{
UpdateBoneUBO();
return *bone_ubo_;
}
void game::MeshInstance::ApplyAnimFrame()
{
ApplySkelAnim(current_anim_, anim_time_, 1.0f);

View File

@ -2,30 +2,37 @@
#include "assets/mesh.hpp"
#include "transform_node.hpp"
#include "gfx/uniform_buffer.hpp"
namespace game
{
class MeshInstance
{
std::shared_ptr<const assets::Mesh> mesh_;
bool skeletal_;
const TransformNode* root_node_ = nullptr;
std::vector<TransformNode> bone_nodes_; // Only used if the mesh is skeletal
const float* time_ptr_ = nullptr; // Pointer to game time variable
std::unique_ptr<gfx::UniformBuffer<glm::mat4>> 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<const assets::Mesh> 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<glm::mat4>& GetBoneUBO();
const std::shared_ptr<const assets::Mesh>& 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();

View File

@ -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<const MeshInstance*>& out_meshes) const
void game::Player::GetViewMeshes(std::vector<MeshInstance*>& out_meshes)
{
out_meshes.push_back(&vm_hands_);
@ -160,6 +171,17 @@ void game::Player::GetViewMeshes(std::vector<const MeshInstance*>& 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

View File

@ -26,6 +26,8 @@ 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_;
@ -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<const MeshInstance*>& out_meshes) const;
void GetViewMeshes(std::vector<MeshInstance*>& out_meshes);
const LightInfluences& GetViewLights();
private:
static glm::vec2 GetBobbingOffset(float t, float amplitude);

View File

@ -74,6 +74,8 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
bt_world_.addCollisionObject(portal.bt_col_obj.get());
}
env_.ambient = glm::vec3(0.1f);
}
void game::Sector::ComputePortalVertex(Portal& portal, size_t idx, const glm::vec2& base_vert)
@ -468,7 +470,7 @@ void game::Sector::GenerateAllLights()
{
Light light = ext_light;
light.position = glm::vec3(portal.link->tr_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<const assets::MeshVertex> 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);

View File

@ -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<const assets::Mesh> mesh_;
SectorEnvironment env_;
std::vector<Light> lights_; // Light in this sector
std::vector<Light> all_lights_; // Lights in this sector and linked sectors
@ -99,6 +108,7 @@ namespace game
const std::shared_ptr<const assets::Mesh>& GetMesh() const { return mesh_; }
const std::shared_ptr<gfx::Texture>& 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]; }

View File

@ -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);
}
}

View File

@ -33,6 +33,8 @@ namespace game
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]; }

View File

@ -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)));
}
}

View File

@ -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<VertexArray> portal_vao_;
void SetupPortalVAO();
@ -60,6 +63,7 @@ namespace gfx
glm::mat4 proj_;
size_t last_sector_id;
const Shader* current_shader_;
std::vector<game::MeshInstance*> 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);
};

View File

@ -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);
}

View File

@ -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();
};
}

4
src/gfx/shader_defs.hpp Normal file
View File

@ -0,0 +1,4 @@
#pragma once
#define SD_MAX_LIGHTS 4
#define SD_MAX_BONES 256

View File

@ -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;

View File

@ -16,6 +16,9 @@ namespace gfx
SS_MESH_VERT,
SS_MESH_FRAG,
SS_SKEL_MESH_VERT,
SS_SKEL_MESH_FRAG,
};
class ShaderSources

View File

@ -0,0 +1,21 @@
#pragma once
#include "buffer_object.hpp"
namespace gfx
{
template<class T>
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);
}
};
}

View File

@ -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);