Add Q3 pickups draw step and hit UI

Introduce a new Quake3 pickups draw workflow step to render in-world pickup icons and register it: add WorkflowQ3PickupsDrawStep (header + implementation), register the step in WorkflowRegistrar, and include the source in CMakeLists. Update the q3_frame workflow JSON to insert a draw_pickups node into the frame graph. Enhance the overlay to show weapon damage in the HUD and draw weapon/hit visuals, and update weapon logic to mark hits and accumulate damage (q3.last_shot_hit, q3.hit_marker_until_frame, q3.damage_done). Also fix trailing newline in CMakeUserPresets.json.
This commit is contained in:
2026-05-02 21:36:40 +01:00
parent 4905d85667
commit a12c575a7e
8 changed files with 310 additions and 3 deletions
+1
View File
@@ -285,6 +285,7 @@ if(BUILD_SDL3_APP)
src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp
src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp
src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp
src/services/impl/workflow/quake3/workflow_q3_pickups_draw_step.cpp
src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_build_collision_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp
+1 -1
View File
@@ -6,4 +6,4 @@
"include": [
"build-ninja/build/generators/CMakePresets.json"
]
}
}
@@ -105,6 +105,12 @@
"metallic": 0.0
}
},
{
"id": "draw_pickups",
"type": "q3.pickups.draw",
"typeVersion": 1,
"position": [1100, 0]
},
{
"id": "end_scene",
"type": "frame.gpu.end_scene",
@@ -167,7 +173,8 @@
"render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } },
"portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } },
"frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } },
"draw_map": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } },
"draw_map": { "main": { "0": [{ "node": "draw_pickups", "type": "main", "index": 0 }] } },
"draw_pickups": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } },
"end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } },
"overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } },
"q3_overlay": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } },
@@ -181,10 +181,45 @@ void WorkflowQ3OverlayDrawStep::DrawSurface(WorkflowContext& context, uint32_t f
const std::string weapon = context.Get<std::string>("q3.current_weapon", "weapon_machinegun");
const int shots = context.Get<int>("q3.shots_fired", 0);
std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots);
const int damage = context.Get<int>("q3.damage_done", 0);
std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots) +
" DAMAGE " + std::to_string(damage);
Text(renderer_, 20, static_cast<float>(kH - 36), hud.c_str(), SDL_Color{255, 216, 64, 255});
Text(renderer_, static_cast<float>(kW / 2 - 4), static_cast<float>(kH / 2 - 4), "+", SDL_Color{255, 255, 255, 220});
const uint32_t frame = static_cast<uint32_t>(context.GetDouble("loop.iteration", 0.0));
const bool flashing = frame < context.Get<uint32_t>("q3.weapon_flash_until_frame", 0u);
const bool hitMarker = frame < context.Get<uint32_t>("q3.hit_marker_until_frame", 0u);
SDL_SetRenderDrawColor(renderer_, 34, 34, 38, 235);
SDL_FRect gunBody{410, 278, 168, 46};
SDL_RenderFillRect(renderer_, &gunBody);
SDL_SetRenderDrawColor(renderer_, 92, 96, 110, 255);
SDL_RenderRect(renderer_, &gunBody);
SDL_SetRenderDrawColor(renderer_, 20, 20, 22, 255);
SDL_FRect grip{452, 318, 36, 30};
SDL_RenderFillRect(renderer_, &grip);
SDL_FRect barrel{568, 291, 54, 18};
SDL_RenderFillRect(renderer_, &barrel);
SDL_SetRenderDrawColor(renderer_, 255, 210, 70, 255);
SDL_RenderLine(renderer_, 424, 290, 550, 290);
Text(renderer_, 430, 300, weapon.substr(7).c_str(), SDL_Color{220, 235, 255, 255});
if (flashing) {
SDL_SetRenderDrawColor(renderer_, 255, 190, 50, 230);
SDL_FRect flash{616, 284, 18, 32};
SDL_RenderFillRect(renderer_, &flash);
SDL_RenderLine(renderer_, 615, 300, 638, 276);
SDL_RenderLine(renderer_, 615, 300, 638, 324);
}
if (hitMarker) {
SDL_SetRenderDrawColor(renderer_, 255, 80, 55, 255);
SDL_RenderLine(renderer_, 308, 172, 320, 160);
SDL_RenderLine(renderer_, 332, 172, 320, 160);
SDL_RenderLine(renderer_, 308, 188, 320, 200);
SDL_RenderLine(renderer_, 332, 188, 320, 200);
Text(renderer_, 300, 204, "HIT", SDL_Color{255, 92, 64, 255});
}
if (context.GetBool("q3.menu_open", false)) {
SDL_FRect panel{120, 42, 400, 250};
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 210);
@@ -0,0 +1,223 @@
#include "services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp"
#include "services/interfaces/workflow/rendering/rendering_types.hpp"
#include <SDL3/SDL_gpu.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <nlohmann/json.hpp>
#include <cstring>
#include <string>
namespace sdl3cpp::services::impl {
namespace {
bool HasPrefix(const std::string& value, const std::string& prefix) {
return value.rfind(prefix, 0) == 0;
}
bool IsPickup(const std::string& classname) {
return HasPrefix(classname, "weapon_") || HasPrefix(classname, "ammo_") ||
HasPrefix(classname, "item_") || HasPrefix(classname, "holdable_");
}
std::string TextureKeyForClass(const std::string& classname) {
if (HasPrefix(classname, "weapon_")) return "q3_pickup_weapon";
if (HasPrefix(classname, "ammo_")) return "q3_pickup_ammo";
if (classname.find("health") != std::string::npos) return "q3_pickup_health";
if (classname.find("armor") != std::string::npos) return "q3_pickup_armor";
return "q3_pickup_powerup";
}
bool ReadVec3(const nlohmann::json& value, glm::vec3& out) {
if (!value.is_array() || value.size() != 3) return false;
out = glm::vec3(value[0].get<float>(), value[1].get<float>(), value[2].get<float>());
return true;
}
} // namespace
WorkflowQ3PickupsDrawStep::WorkflowQ3PickupsDrawStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
WorkflowQ3PickupsDrawStep::~WorkflowQ3PickupsDrawStep() {
if (device_) {
if (quad_vb_) SDL_ReleaseGPUBuffer(device_, quad_vb_);
if (quad_ib_) SDL_ReleaseGPUBuffer(device_, quad_ib_);
if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_);
}
}
std::string WorkflowQ3PickupsDrawStep::GetPluginId() const {
return "q3.pickups.draw";
}
SDL_GPUTexture* WorkflowQ3PickupsDrawStep::CreateColorTexture(SDL_GPUDevice* device, WorkflowContext& context,
const std::string& key,
uint8_t r, uint8_t g, uint8_t b) {
auto* existing = context.Get<SDL_GPUTexture*>(key + "_gpu", nullptr);
if (existing) return existing;
SDL_GPUTextureCreateInfo ti = {};
ti.type = SDL_GPU_TEXTURETYPE_2D;
ti.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
ti.width = 1;
ti.height = 1;
ti.layer_count_or_depth = 1;
ti.num_levels = 1;
ti.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
auto* tex = SDL_CreateGPUTexture(device, &ti);
if (!tex) return nullptr;
SDL_GPUTransferBufferCreateInfo tbi = {};
tbi.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tbi.size = 4;
auto* tb = SDL_CreateGPUTransferBuffer(device, &tbi);
auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(device, tb, false));
mapped[0] = r; mapped[1] = g; mapped[2] = b; mapped[3] = 230;
SDL_UnmapGPUTransferBuffer(device, tb);
auto* cmd = SDL_AcquireGPUCommandBuffer(device);
auto* copy = SDL_BeginGPUCopyPass(cmd);
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = tb;
SDL_GPUTextureRegion dst = {};
dst.texture = tex; dst.w = 1; dst.h = 1; dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
SDL_SubmitGPUCommandBuffer(cmd);
SDL_ReleaseGPUTransferBuffer(device, tb);
SDL_GPUSamplerCreateInfo si = {};
si.min_filter = SDL_GPU_FILTER_NEAREST;
si.mag_filter = SDL_GPU_FILTER_NEAREST;
si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
auto* sampler = SDL_CreateGPUSampler(device, &si);
context.Set<SDL_GPUTexture*>(key + "_gpu", tex);
context.Set<SDL_GPUSampler*>(key + "_sampler", sampler);
return tex;
}
void WorkflowQ3PickupsDrawStep::EnsureBuffers(SDL_GPUDevice* device) {
if (quad_vb_ && quad_ib_) return;
device_ = device;
struct V { float x, y, z, u, v; };
const V verts[4] = {
{-0.5f, -0.5f, 0.0f, 0.0f, 1.0f},
{ 0.5f, -0.5f, 0.0f, 1.0f, 1.0f},
{ 0.5f, 0.5f, 0.0f, 1.0f, 0.0f},
{-0.5f, 0.5f, 0.0f, 0.0f, 0.0f},
};
const uint16_t indices[6] = {0, 1, 2, 0, 2, 3};
const uint32_t total = sizeof(verts) + sizeof(indices);
SDL_GPUBufferCreateInfo vbInfo = {};
vbInfo.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
vbInfo.size = sizeof(verts);
quad_vb_ = SDL_CreateGPUBuffer(device, &vbInfo);
SDL_GPUBufferCreateInfo ibInfo = {};
ibInfo.usage = SDL_GPU_BUFFERUSAGE_INDEX;
ibInfo.size = sizeof(indices);
quad_ib_ = SDL_CreateGPUBuffer(device, &ibInfo);
SDL_GPUTransferBufferCreateInfo tbInfo = {};
tbInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tbInfo.size = total;
transfer_ = SDL_CreateGPUTransferBuffer(device, &tbInfo);
auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(device, transfer_, false));
std::memcpy(mapped, verts, sizeof(verts));
std::memcpy(mapped + sizeof(verts), indices, sizeof(indices));
SDL_UnmapGPUTransferBuffer(device, transfer_);
auto* cmd = SDL_AcquireGPUCommandBuffer(device);
auto* copy = SDL_BeginGPUCopyPass(cmd);
SDL_GPUTransferBufferLocation srcV = {transfer_, 0};
SDL_GPUBufferRegion dstV = {quad_vb_, 0, sizeof(verts)};
SDL_UploadToGPUBuffer(copy, &srcV, &dstV, false);
SDL_GPUTransferBufferLocation srcI = {transfer_, sizeof(verts)};
SDL_GPUBufferRegion dstI = {quad_ib_, 0, sizeof(indices)};
SDL_UploadToGPUBuffer(copy, &srcI, &dstI, false);
SDL_EndGPUCopyPass(copy);
SDL_SubmitGPUCommandBuffer(cmd);
}
void WorkflowQ3PickupsDrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
if (context.GetBool("frame_skip", false)) return;
auto* pass = context.Get<SDL_GPURenderPass*>("gpu_render_pass", nullptr);
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
auto* pipeline = context.Get<SDL_GPUGraphicsPipeline*>("gpu_pipeline_textured", nullptr);
const auto* entities = context.TryGet<nlohmann::json>("bsp.entities");
if (!pass || !cmd || !device || !pipeline || !entities || !entities->is_array()) return;
EnsureBuffers(device);
CreateColorTexture(device, context, "q3_pickup_weapon", 255, 210, 40);
CreateColorTexture(device, context, "q3_pickup_ammo", 255, 120, 45);
CreateColorTexture(device, context, "q3_pickup_health", 40, 235, 85);
CreateColorTexture(device, context, "q3_pickup_armor", 70, 150, 255);
CreateColorTexture(device, context, "q3_pickup_powerup", 190, 90, 255);
auto collected = context.Get<nlohmann::json>("q3.collected", nlohmann::json::object());
auto view = context.Get<glm::mat4>("render.view_matrix", glm::mat4(1.0f));
auto proj = context.Get<glm::mat4>("render.proj_matrix", glm::mat4(1.0f));
auto camPos = context.Get<glm::vec3>("render.camera_pos", glm::vec3(0.0f));
auto shadowVP = context.Get<glm::mat4>("render.shadow_vp", glm::mat4(1.0f));
glm::vec3 camRight(view[0][0], view[1][0], view[2][0]);
glm::vec3 camUp(view[0][1], view[1][1], view[2][1]);
const float time = static_cast<float>(context.GetDouble("frame.elapsed", 0.0));
SDL_BindGPUGraphicsPipeline(pass, pipeline);
SDL_GPUBufferBinding vb = {quad_vb_, 0};
SDL_BindGPUVertexBuffers(pass, 0, &vb, 1);
SDL_GPUBufferBinding ib = {quad_ib_, 0};
SDL_BindGPUIndexBuffer(pass, &ib, SDL_GPU_INDEXELEMENTSIZE_16BIT);
int drawn = 0;
for (const auto& ent : *entities) {
const std::string classname = ent.value("classname", std::string{});
const std::string id = ent.value("id", std::string{});
if (!IsPickup(classname) || collected.value(id, false)) continue;
glm::vec3 pos;
if (!ent.contains("position") || !ReadVec3(ent["position"], pos)) continue;
if (++drawn > 96) break;
const float bob = std::sin(time * 3.0f + static_cast<float>(drawn)) * 0.08f;
const float size = HasPrefix(classname, "weapon_") ? 0.9f : 0.55f;
pos.y += 0.45f + bob;
glm::mat4 model(1.0f);
model[0] = glm::vec4(camRight * size, 0.0f);
model[1] = glm::vec4(camUp * size, 0.0f);
model[2] = glm::vec4(glm::normalize(glm::cross(camRight, camUp)) * size, 0.0f);
model[3] = glm::vec4(pos, 1.0f);
rendering::VertexUniformData vu = {};
glm::mat4 mvp = proj * view * model;
std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16);
std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16);
vu.normal[1] = 1.0f;
vu.uv_scale[0] = 1.0f; vu.uv_scale[1] = 1.0f;
vu.camera_pos[0] = camPos.x; vu.camera_pos[1] = camPos.y; vu.camera_pos[2] = camPos.z;
std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16);
auto fu = context.Get<rendering::FragmentUniformData>("render.frag_uniforms", rendering::FragmentUniformData{});
fu.material[0] = 0.35f;
fu.material[1] = 0.0f;
const std::string texKey = TextureKeyForClass(classname);
auto* tex = context.Get<SDL_GPUTexture*>(texKey + "_gpu", nullptr);
auto* samp = context.Get<SDL_GPUSampler*>(texKey + "_sampler", nullptr);
if (!tex || !samp) continue;
SDL_GPUTextureSamplerBinding bindings[2] = {};
bindings[0].texture = tex; bindings[0].sampler = samp;
bindings[1].texture = tex; bindings[1].sampler = samp;
SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2);
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 0, 0, 0);
}
}
} // namespace sdl3cpp::services::impl
@@ -68,6 +68,7 @@ void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, Wor
context.Set<uint32_t>("q3.weapon_last_fire_frame", lastFire);
context.Set<uint32_t>("q3.weapon_flash_until_frame", lastFire + 4u);
context.Set<int>("q3.shots_fired", context.Get<int>("q3.shots_fired", 0) + 1);
context.Set<bool>("q3.last_shot_hit", false);
auto* world = context.Get<btDiscreteDynamicsWorld*>("physics_world", nullptr);
auto cameraState = context.Get<nlohmann::json>("camera.state", nlohmann::json::object());
@@ -81,6 +82,13 @@ void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, Wor
world->rayTest(from, to, hit);
context.Set<bool>("q3.last_shot_hit", hit.hasHit());
if (hit.hasHit()) {
context.Set<uint32_t>("q3.hit_marker_until_frame", lastFire + 10u);
context.Set<int>("q3.damage_done", context.Get<int>("q3.damage_done", 0) + (
current == "weapon_railgun" ? 100 :
current == "weapon_rocketlauncher" ? 100 :
current == "weapon_shotgun" ? 80 :
current == "weapon_plasmagun" ? 20 :
current == "weapon_machinegun" ? 7 : 15));
context.Set("q3.last_shot_position", nlohmann::json::array({
hit.m_hitPointWorld.x(), hit.m_hitPointWorld.y(), hit.m_hitPointWorld.z()
}));
@@ -117,6 +117,7 @@
#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp"
#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp"
#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp"
#include "services/interfaces/workflow/quake3/workflow_q3_pickups_draw_step.hpp"
// Audio (service-dependent, registered with nullptr)
#include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_pause_step.hpp"
@@ -323,6 +324,7 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
registry->RegisterStep(std::make_shared<WorkflowPostfxBloomBlurStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3MenuUpdateStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3WeaponUpdateStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3PickupsDrawStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3OverlayDrawStep>(logger_));
count += 18;
@@ -0,0 +1,31 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include <SDL3/SDL_gpu.h>
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowQ3PickupsDrawStep final : public IWorkflowStep {
public:
explicit WorkflowQ3PickupsDrawStep(std::shared_ptr<ILogger> logger);
~WorkflowQ3PickupsDrawStep();
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
SDL_GPUTexture* CreateColorTexture(SDL_GPUDevice* device, WorkflowContext& context,
const std::string& key, uint8_t r, uint8_t g, uint8_t b);
void EnsureBuffers(SDL_GPUDevice* device);
std::shared_ptr<ILogger> logger_;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUBuffer* quad_vb_ = nullptr;
SDL_GPUBuffer* quad_ib_ = nullptr;
SDL_GPUTransferBuffer* transfer_ = nullptr;
};
} // namespace sdl3cpp::services::impl