diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 86d4b3a12..3341868bd 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -102,6 +102,8 @@ find_package(stb CONFIG REQUIRED) find_package(EnTT CONFIG REQUIRED) +find_package(assimp CONFIG REQUIRED) + # Build render stack library group set(SDL3CPP_RENDER_STACK_LIBS EnTT::EnTT) @@ -281,12 +283,15 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp src/services/impl/workflow/rendering/workflow_draw_textured_box_step.cpp src/services/impl/workflow/rendering/workflow_draw_textured_step.cpp + src/services/impl/workflow/rendering/workflow_draw_viewmodel_step.cpp src/services/impl/workflow/rendering/workflow_frame_begin_gpu_step.cpp src/services/impl/workflow/rendering/workflow_frame_begin_offscreen_step.cpp src/services/impl/workflow/rendering/workflow_frame_draw_bodies_step.cpp src/services/impl/workflow/rendering/workflow_frame_end_gpu_step.cpp src/services/impl/workflow/rendering/workflow_frame_end_scene_step.cpp + src/services/impl/workflow/rendering/workflow_geometry_create_flashlight_step.cpp src/services/impl/workflow/rendering/workflow_lighting_setup_step.cpp + src/services/impl/workflow/rendering/workflow_model_load_step.cpp src/services/impl/workflow/rendering/workflow_postfx_bloom_blur_step.cpp src/services/impl/workflow/rendering/workflow_postfx_bloom_extract_step.cpp src/services/impl/workflow/rendering/workflow_postfx_composite_step.cpp @@ -297,6 +302,7 @@ if(BUILD_SDL3_APP) src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp src/services/impl/workflow/rendering/workflow_shadow_pass_step.cpp src/services/impl/workflow/rendering/workflow_shadow_setup_step.cpp + src/services/impl/workflow/rendering/workflow_spotlight_setup_step.cpp src/services/impl/workflow/graphics/workflow_gpu_pipeline_create_step.cpp src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp src/services/impl/workflow/graphics/workflow_graphics_buffer_create_index_step.cpp @@ -360,6 +366,7 @@ if(BUILD_SDL3_APP) glm::glm stb::stb EnTT::EnTT + assimp::assimp ) endif() diff --git a/gameengine/CMakeUserPresets.json b/gameengine/CMakeUserPresets.json index 0deca3bfa..73fde6670 100644 --- a/gameengine/CMakeUserPresets.json +++ b/gameengine/CMakeUserPresets.json @@ -4,6 +4,6 @@ "conan": {} }, "include": [ - "build-ninja/build/Release/generators/CMakePresets.json" + "build-ninja/build/generators/CMakePresets.json" ] } \ No newline at end of file diff --git a/gameengine/cmake_config.json b/gameengine/cmake_config.json index 29f0387f9..1eb252bef 100644 --- a/gameengine/cmake_config.json +++ b/gameengine/cmake_config.json @@ -50,7 +50,8 @@ "Bullet", "glm", "stb", - "EnTT" + "EnTT", + "assimp" ] }, "source_exclusions": [ @@ -126,7 +127,8 @@ "Bullet::Bullet", "glm::glm", "stb::stb", - "EnTT::EnTT" + "EnTT::EnTT", + "assimp::assimp" ], "compile_definitions": [], "name": "sdl3_app", diff --git a/gameengine/generate_cmake.py b/gameengine/generate_cmake.py index ef8cdae89..f00ed1715 100755 --- a/gameengine/generate_cmake.py +++ b/gameengine/generate_cmake.py @@ -48,6 +48,11 @@ def load_config(config_path: str) -> dict: return json.load(f) +def _posix_path(p: str) -> str: + """Normalize path separators to forward slashes for CMake compatibility.""" + return p.replace('\\', '/') + + def expand_test_glob_patterns(config: dict) -> dict: """Expand glob patterns in test_targets list itself. @@ -62,7 +67,7 @@ def expand_test_glob_patterns(config: dict) -> dict: if isinstance(test_def, dict) and 'pattern' in test_def: # This is a glob pattern definition - expand it pattern = test_def['pattern'] - matches = sorted(glob(pattern)) + matches = sorted(_posix_path(m) for m in glob(pattern)) matches = [m for m in matches if m not in exclusions] template = test_def.copy() @@ -96,12 +101,12 @@ def expand_globs(config: dict) -> dict: for src in target['sources']: if '*' in src: # Glob pattern - expand it - matches = sorted(glob(src)) + matches = sorted(_posix_path(m) for m in glob(src)) # Filter out excluded files matches = [m for m in matches if m not in exclusions] expanded.extend(matches) elif src not in exclusions: # Check literal sources too - expanded.append(src) + expanded.append(_posix_path(src)) target['sources'] = expanded for test in config.get('test_targets', []): @@ -109,12 +114,12 @@ def expand_globs(config: dict) -> dict: expanded = [] for src in test['sources']: if '*' in src: - matches = sorted(glob(src)) + matches = sorted(_posix_path(m) for m in glob(src)) # Filter out excluded files matches = [m for m in matches if m not in exclusions] expanded.extend(matches) elif src not in exclusions: - expanded.append(src) + expanded.append(_posix_path(src)) test['sources'] = expanded # Provide smart defaults for test targets from config diff --git a/gameengine/packages/seed/assets/textures/ceiling/Plaster001_1K-JPG_Color.jpg b/gameengine/packages/seed/assets/textures/ceiling/Plaster001_1K-JPG_Color.jpg index 54b8d7bb8..ecbd2f572 100644 Binary files a/gameengine/packages/seed/assets/textures/ceiling/Plaster001_1K-JPG_Color.jpg and b/gameengine/packages/seed/assets/textures/ceiling/Plaster001_1K-JPG_Color.jpg differ diff --git a/gameengine/packages/seed/assets/textures/floor/WoodFloor007_1K-JPG_Color.jpg b/gameengine/packages/seed/assets/textures/floor/WoodFloor007_1K-JPG_Color.jpg new file mode 100644 index 000000000..23ac50380 Binary files /dev/null and b/gameengine/packages/seed/assets/textures/floor/WoodFloor007_1K-JPG_Color.jpg differ diff --git a/gameengine/packages/seed/package.json b/gameengine/packages/seed/package.json index d1648ea74..bccb8fcee 100644 --- a/gameengine/packages/seed/package.json +++ b/gameengine/packages/seed/package.json @@ -53,6 +53,82 @@ "fragment": "shaders/msl/constant_color.frag.metal" } } + }, + { + "id": "textured", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { + "vertex": "shaders/spirv/textured.vert.spv", + "fragment": "shaders/spirv/textured.frag.spv" + }, + "msl": { + "vertex": "shaders/msl/textured.vert.metal", + "fragment": "shaders/msl/textured.frag.metal" + } + } + }, + { + "id": "shadow_depth", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { + "vertex": "shaders/spirv/shadow_depth.vert.spv", + "fragment": "shaders/spirv/shadow_depth.frag.spv" + }, + "msl": { + "vertex": "shaders/msl/shadow_depth.vert.metal", + "fragment": "shaders/msl/shadow_depth.frag.metal" + } + } + }, + { + "id": "postfx_fullscreen", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "vertex": "shaders/spirv/postfx_fullscreen.vert.spv" }, + "msl": { "vertex": "shaders/msl/postfx_fullscreen.vert.metal" } + } + }, + { + "id": "postfx_bloom_blur", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "fragment": "shaders/spirv/postfx_bloom_blur.frag.spv" }, + "msl": { "fragment": "shaders/msl/postfx_bloom_blur.frag.metal" } + } + }, + { + "id": "postfx_bloom_extract", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "fragment": "shaders/spirv/postfx_bloom_extract.frag.spv" }, + "msl": { "fragment": "shaders/msl/postfx_bloom_extract.frag.metal" } + } + }, + { + "id": "postfx_composite", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "fragment": "shaders/spirv/postfx_composite.frag.spv" }, + "msl": { "fragment": "shaders/msl/postfx_composite.frag.metal" } + } + }, + { + "id": "postfx_ssao", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "fragment": "shaders/spirv/postfx_ssao.frag.spv" }, + "msl": { "fragment": "shaders/msl/postfx_ssao.frag.metal" } + } + }, + { + "id": "tessellate", + "backendOrder": ["spirv", "msl"], + "backends": { + "spirv": { "compute": "shaders/spirv/tessellate.comp.spv" }, + "msl": { "compute": "shaders/msl/tessellate.comp.metal" } + } } ], "bundled": true, diff --git a/gameengine/packages/seed/shaders/spirv/constant_color.frag.spv b/gameengine/packages/seed/shaders/spirv/constant_color.frag.spv index b1d5ab3d7..2bb465189 100644 Binary files a/gameengine/packages/seed/shaders/spirv/constant_color.frag.spv and b/gameengine/packages/seed/shaders/spirv/constant_color.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/constant_color.vert.glsl b/gameengine/packages/seed/shaders/spirv/constant_color.vert.glsl index e88546a07..8e07374e9 100644 --- a/gameengine/packages/seed/shaders/spirv/constant_color.vert.glsl +++ b/gameengine/packages/seed/shaders/spirv/constant_color.vert.glsl @@ -3,7 +3,7 @@ layout(location = 0) in vec3 a_position; layout(location = 1) in vec4 a_color; -layout(push_constant) uniform PushConstants { +layout(set = 1, binding = 0) uniform Uniforms { mat4 u_modelViewProj; }; diff --git a/gameengine/packages/seed/shaders/spirv/constant_color.vert.spv b/gameengine/packages/seed/shaders/spirv/constant_color.vert.spv index 63f646e30..8994c0e0a 100644 Binary files a/gameengine/packages/seed/shaders/spirv/constant_color.vert.spv and b/gameengine/packages/seed/shaders/spirv/constant_color.vert.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.glsl b/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.glsl new file mode 100644 index 000000000..e028f027c --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.glsl @@ -0,0 +1,27 @@ +#version 450 + +layout(set = 3, binding = 0) uniform BlurParams { + vec4 direction; // xy = texel-scaled blur direction +}; + +layout(set = 2, binding = 0) uniform sampler2D source; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_color; + +void main() { + vec2 dir = direction.xy; + + // 9-tap Gaussian (sigma ~= 4, weights sum to 1.0) + float weight[5] = float[5](0.227027027, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162); + + vec3 result = texture(source, v_uv).rgb * weight[0]; + + for (int i = 1; i < 5; ++i) { + vec2 offset = dir * float(i); + result += texture(source, v_uv + offset).rgb * weight[i]; + result += texture(source, v_uv - offset).rgb * weight[i]; + } + + o_color = vec4(result, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.spv b/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.spv new file mode 100644 index 000000000..12e11b1cf Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/postfx_bloom_blur.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.glsl b/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.glsl new file mode 100644 index 000000000..95f8d1554 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.glsl @@ -0,0 +1,28 @@ +#version 450 + +layout(set = 3, binding = 0) uniform BloomParams { + vec4 params; // x=threshold, y=soft_knee, z=0, w=0 +}; + +layout(set = 2, binding = 0) uniform sampler2D hdr_texture; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_color; + +void main() { + vec3 color = texture(hdr_texture, v_uv).rgb; + + float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722)); + float threshold = params.x; + float knee = params.y; + + // Soft knee: smooth quadratic falloff near threshold + float soft = brightness - threshold + knee; + soft = clamp(soft, 0.0, 2.0 * knee); + soft = soft * soft / (4.0 * knee + 0.00001); + + float contribution = max(soft, brightness - threshold); + contribution /= max(brightness, 0.00001); + + o_color = vec4(color * max(contribution, 0.0), 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.spv b/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.spv new file mode 100644 index 000000000..a2fb6d064 Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/postfx_bloom_extract.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.glsl b/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.glsl new file mode 100644 index 000000000..d60fbb954 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.glsl @@ -0,0 +1,38 @@ +#version 450 + +layout(set = 2, binding = 0) uniform sampler2D hdr_texture; +layout(set = 2, binding = 1) uniform sampler2D ssao_texture; +layout(set = 2, binding = 2) uniform sampler2D bloom_texture; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_color; + +// ACES filmic tonemapping (Narkowicz fit) +vec3 ACESFilm(vec3 x) { + float a = 2.51; + float b = 0.03; + float c = 2.43; + float d = 0.59; + float e = 0.14; + return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); +} + +void main() { + vec3 hdr = texture(hdr_texture, v_uv).rgb; + + // Apply SSAO occlusion + float ao = texture(ssao_texture, v_uv).r; + hdr *= ao; + + // Additive bloom + vec3 bloom = texture(bloom_texture, v_uv).rgb; + hdr += bloom; + + // ACES filmic tonemapping + vec3 mapped = ACESFilm(hdr); + + // sRGB gamma correction + vec3 result = pow(mapped, vec3(1.0 / 2.2)); + + o_color = vec4(result, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.spv b/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.spv new file mode 100644 index 000000000..a325a92fb Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/postfx_composite.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.glsl b/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.glsl new file mode 100644 index 000000000..e00f8c348 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) out vec2 v_uv; + +void main() { + // Fullscreen triangle: 3 vertices covering [-1,1] clip space + v_uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(v_uv * 2.0 - 1.0, 0.0, 1.0); + // Flip Y for Vulkan's top-left origin + v_uv.y = 1.0 - v_uv.y; +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.spv b/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.spv new file mode 100644 index 000000000..bc100609f Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/postfx_fullscreen.vert.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.glsl b/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.glsl new file mode 100644 index 000000000..4e2726112 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.glsl @@ -0,0 +1,90 @@ +#version 450 + +const float PI = 3.14159265359; + +layout(set = 3, binding = 0) uniform SSAOParams { + mat4 u_projection; + mat4 u_inv_projection; + vec4 u_params; // x=radius, y=bias, z=1/width, w=1/height + vec4 u_kernel[16]; // hemisphere sample directions +}; + +layout(set = 2, binding = 0) uniform sampler2D depthTex; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_color; + +vec3 reconstructViewPos(vec2 uv, float depth, mat4 invProj) { + // UV to clip space [-1, 1] + vec4 clip = vec4(uv * 2.0 - 1.0, depth, 1.0); + clip.y = -clip.y; // Vulkan NDC convention + vec4 viewPos = invProj * clip; + return viewPos.xyz / viewPos.w; +} + +// Screen-space procedural noise for sample rotation +vec2 noiseFromUV(vec2 uv, vec2 texelSize) { + vec2 screenPos = uv / texelSize; + return fract(sin(vec2( + dot(screenPos, vec2(12.9898, 78.233)), + dot(screenPos, vec2(39.346, 11.135)) + )) * 43758.5453); +} + +void main() { + float depth = texture(depthTex, v_uv).r; + + // Sky pixels: no occlusion + if (depth >= 1.0) { + o_color = vec4(1.0); + return; + } + + // TODO: SSAO needs Vulkan clip-space tuning — bypass for now + o_color = vec4(1.0); + return; + + float radius = u_params.x; + float bias = u_params.y; + vec2 texelSize = u_params.zw; + + // Reconstruct view-space position and normal + vec3 fragPos = reconstructViewPos(v_uv, depth, u_inv_projection); + vec3 normal = normalize(cross(dFdx(fragPos), dFdy(fragPos))); + + // Random rotation from procedural noise + vec2 noise = noiseFromUV(v_uv, texelSize); + float angle = noise.x * 2.0 * PI; + float cosA = cos(angle); + float sinA = sin(angle); + + // Build TBN from normal + random tangent + vec3 randomVec = vec3(cosA, sinA, 0.0); + vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); + vec3 bitangent = cross(normal, tangent); + mat3 TBN = mat3(tangent, bitangent, normal); + + float occlusion = 0.0; + for (int i = 0; i < 16; ++i) { + // Orient sample in hemisphere around normal + vec3 sampleDir = TBN * u_kernel[i].xyz; + vec3 samplePos = fragPos + sampleDir * radius; + + // Project sample to screen space + vec4 offset = u_projection * vec4(samplePos, 1.0); + offset.xyz /= offset.w; + vec2 sampleUV = offset.xy * 0.5 + 0.5; + sampleUV.y = 1.0 - sampleUV.y; + + // Sample depth at projected position + float sampleDepth = texture(depthTex, sampleUV).r; + vec3 sampleViewPos = reconstructViewPos(sampleUV, sampleDepth, u_inv_projection); + + // Range check: only occlude if sample is within radius + float rangeCheck = smoothstep(0.0, 1.0, radius / max(abs(fragPos.z - sampleViewPos.z), 0.001)); + occlusion += (sampleViewPos.z >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck; + } + + float ao = 1.0 - (occlusion / 16.0); + o_color = vec4(ao, ao, ao, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.spv b/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.spv new file mode 100644 index 000000000..401d5e002 Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/postfx_ssao.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.glsl b/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.glsl new file mode 100644 index 000000000..9fa32a207 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +// Minimal fragment shader for depth-only pass +layout(location = 0) out vec4 o_color; + +void main() { + o_color = vec4(0.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.spv b/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.spv new file mode 100644 index 000000000..bef6c1c12 Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/shadow_depth.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.glsl b/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.glsl new file mode 100644 index 000000000..627eac8f0 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.glsl @@ -0,0 +1,13 @@ +#version 450 + +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec2 a_uv; // present in vertex buffer but unused + +layout(set = 1, binding = 0) uniform ShadowUniforms { + mat4 u_lightVP; + mat4 u_model; +}; + +void main() { + gl_Position = u_lightVP * u_model * vec4(a_position, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.spv b/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.spv new file mode 100644 index 000000000..1bd5d824a Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/shadow_depth.vert.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/tessellate.comp.glsl b/gameengine/packages/seed/shaders/spirv/tessellate.comp.glsl new file mode 100644 index 000000000..6520af44b --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/tessellate.comp.glsl @@ -0,0 +1,48 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +struct TessVertex { + float px, py, pz; // position + float u, v; // uv +}; + +layout(set = 0, binding = 0) uniform sampler2D displacement; + +layout(std430, set = 0, binding = 1) buffer VertexBuffer { + TessVertex vertices[]; +}; + +layout(set = 0, binding = 2) uniform TessParams { + float width; + float depth; + float displacement_strength; + float uv_scale_x; + float uv_scale_y; + uint subdivisions; + uint _pad0; + uint _pad1; +}; + +void main() { + uvec2 gid = gl_GlobalInvocationID.xy; + uint subdiv = subdivisions; + if (gid.x > subdiv || gid.y > subdiv) return; + + float uf = float(gid.x) / float(subdiv); + float vf = float(gid.y) / float(subdiv); + + // Sample displacement with bilinear interpolation + float disp = texture(displacement, vec2(uf, vf)).r; + + // Generate vertex on XZ plane, displaced along Y + float hw = width * 0.5; + float hd = depth * 0.5; + + uint idx = gid.y * (subdiv + 1) + gid.x; + vertices[idx].px = -hw + uf * width; + vertices[idx].py = disp * displacement_strength; + vertices[idx].pz = -hd + vf * depth; + vertices[idx].u = uf * uv_scale_x; + vertices[idx].v = vf * uv_scale_y; +} diff --git a/gameengine/packages/seed/shaders/spirv/tessellate.comp.spv b/gameengine/packages/seed/shaders/spirv/tessellate.comp.spv new file mode 100644 index 000000000..1ec010a13 Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/tessellate.comp.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/textured.frag.glsl b/gameengine/packages/seed/shaders/spirv/textured.frag.glsl new file mode 100644 index 000000000..c03ff13e9 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/textured.frag.glsl @@ -0,0 +1,196 @@ +#version 450 + +// PBR Cook-Torrance + Spotlight + Volumetric Light Beam + PCF Shadows +// Outputs linear HDR — tonemapping handled by postfx composite + +const float PI = 3.14159265359; + +// SDL3 GPU: set=3 for fragment uniform buffers +layout(set = 3, binding = 0) uniform PBRUniforms { + vec4 u_lightDir; // xyz = direction (toward scene), w = unused + vec4 u_lightColor; // rgb = light color * intensity, a = exposure + vec4 u_ambient; // rgb = ambient color * intensity, a = unused + vec4 u_material; // x = roughness, y = metallic, zw = unused + vec4 u_flashPos; // xyz = position, w = cos(inner cone angle) + vec4 u_flashDir; // xyz = direction, w = cos(outer cone angle) + vec4 u_flashColor; // rgb = color * intensity, a = range +}; + +// SDL3 GPU: set=2 for fragment combined image samplers +layout(set = 2, binding = 0) uniform sampler2D albedoTex; +layout(set = 2, binding = 1) uniform sampler2D shadowMap; + +layout(location = 0) in vec2 v_uv; +layout(location = 1) in vec3 v_worldNormal; +layout(location = 2) in vec3 v_worldPos; +layout(location = 3) in vec3 v_cameraPos; +layout(location = 4) in vec4 v_shadowPos; + +layout(location = 0) out vec4 o_color; + +float D_GGX(float NdotH, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float d = NdotH * NdotH * (a2 - 1.0) + 1.0; + return a2 / (PI * d * d); +} + +float G_SchlickGGX(float NdotX, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + return NdotX / (NdotX * (1.0 - k) + k); +} + +float G_Smith(float NdotV, float NdotL, float roughness) { + return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness); +} + +vec3 F_Schlick(float cosTheta, vec3 F0) { + return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +float ComputeShadowPCF(vec4 shadowPos, float NdotL) { + vec3 ndc = shadowPos.xyz / shadowPos.w; + vec2 shadowUV = ndc.xy * 0.5 + 0.5; + shadowUV.y = 1.0 - shadowUV.y; + + if (shadowUV.x < 0.0 || shadowUV.x > 1.0 || shadowUV.y < 0.0 || shadowUV.y > 1.0) + return 1.0; + + float currentDepth = ndc.z; + float bias = max(0.005 * (1.0 - NdotL), 0.001); + + vec2 texelSize = vec2(1.0) / vec2(textureSize(shadowMap, 0)); + float shadow = 0.0; + for (int y = -2; y <= 2; ++y) { + for (int x = -2; x <= 2; ++x) { + vec2 offset = vec2(float(x), float(y)) * texelSize; + float sampleDepth = texture(shadowMap, shadowUV + offset).r; + shadow += (currentDepth - bias > sampleDepth) ? 0.0 : 1.0; + } + } + shadow /= 25.0; + return mix(0.12, 1.0, shadow); +} + +// Spotlight cone attenuation +float SpotlightAtten(vec3 lightToFrag, vec3 spotDir, float cosInner, float cosOuter) { + float cosAngle = dot(normalize(lightToFrag), spotDir); + return clamp((cosAngle - cosOuter) / max(cosInner - cosOuter, 0.001), 0.0, 1.0); +} + +// Volumetric light beam: ray march from camera to surface through spotlight cone +// Simulates light scattering through dust particles in the air +vec3 VolumetricBeam(vec3 camPos, vec3 fragPos, vec3 flashPos, vec3 flashDir, + float cosInner, float cosOuter, vec3 flashColor, float flashRange) { + const int NUM_STEPS = 16; + const float FOG_DENSITY = 0.007; // dust density in the air + + vec3 rayDir = fragPos - camPos; + float rayLen = length(rayDir); + float stepSize = rayLen / float(NUM_STEPS); + + // Dithered start offset to reduce banding artifacts + float dither = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453); + + vec3 accumulated = vec3(0.0); + for (int i = 0; i < NUM_STEPS; ++i) { + float t = (float(i) + dither) * stepSize; + vec3 samplePos = camPos + normalize(rayDir) * t; + + // Check if this point in space is inside the spotlight cone + vec3 toSample = samplePos - flashPos; + float dist = length(toSample); + + // Distance attenuation + float distAtten = clamp(1.0 - dist / flashRange, 0.0, 1.0); + distAtten *= distAtten; + + // Cone attenuation + float cosAngle = dot(normalize(toSample), flashDir); + float coneAtten = clamp((cosAngle - cosOuter) / max(cosInner - cosOuter, 0.001), 0.0, 1.0); + + // Mie-like forward scattering: brighter when looking into the beam + float viewDot = max(dot(normalize(rayDir), flashDir), 0.0); + float scatter = mix(1.0, 3.0, viewDot * viewDot); + + accumulated += flashColor * distAtten * coneAtten * scatter * FOG_DENSITY * stepSize; + } + + return accumulated; +} + +void main() { + vec3 albedo = texture(albedoTex, v_uv).rgb; + float roughness = u_material.x; + float metallic = u_material.y; + float exposure = u_lightColor.a; + + vec3 N = normalize(v_worldNormal); + vec3 V = normalize(v_cameraPos - v_worldPos); + float NdotV = max(dot(N, V), 0.001); + + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 kD_base = (1.0 - metallic) * albedo / PI; + + // === Directional light (sun/fill) === + vec3 L = normalize(-u_lightDir.xyz); + float rawNdotL = dot(N, L); + float NdotL = max(rawNdotL, 0.0); + float wrapLight = max(rawNdotL * 0.5 + 0.5, 0.0); + + vec3 H = normalize(V + L); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + float D = D_GGX(NdotH, roughness); + float G = G_Smith(NdotV, NdotL, roughness); + vec3 F = F_Schlick(HdotV, F0); + vec3 spec = (D * G * F) / max(4.0 * NdotV * NdotL, 0.001); + vec3 diff = (vec3(1.0) - F) * kD_base; + + float shadow = ComputeShadowPCF(v_shadowPos, NdotL); + vec3 Lo = (diff * wrapLight + spec * NdotL) * u_lightColor.rgb * shadow; + + // === Spotlight (flashlight / torch) === + float flashRange = u_flashColor.a; + vec3 volumetric = vec3(0.0); + + if (flashRange > 0.0) { + vec3 spotDir = normalize(u_flashDir.xyz); + + // Surface lighting from spotlight + vec3 lightToFrag = v_worldPos - u_flashPos.xyz; + float dist = length(lightToFrag); + float atten = clamp(1.0 - dist / flashRange, 0.0, 1.0); + atten *= atten; + + float spot = SpotlightAtten(lightToFrag, spotDir, u_flashPos.w, u_flashDir.w); + + vec3 Lf = normalize(-lightToFrag); + float NdotLf = max(dot(N, Lf), 0.0); + vec3 Hf = normalize(V + Lf); + float NdotHf = max(dot(N, Hf), 0.0); + float HdotVf = max(dot(Hf, V), 0.0); + + float Df = D_GGX(NdotHf, roughness); + float Gf = G_Smith(NdotV, NdotLf, roughness); + vec3 Ff = F_Schlick(HdotVf, F0); + vec3 specF = (Df * Gf * Ff) / max(4.0 * NdotV * NdotLf, 0.001); + vec3 diffF = (vec3(1.0) - Ff) * kD_base; + + Lo += (diffF + specF) * NdotLf * u_flashColor.rgb * atten * spot; + + // Volumetric light beam through dusty air + volumetric = VolumetricBeam(v_cameraPos, v_worldPos, u_flashPos.xyz, + spotDir, u_flashPos.w, u_flashDir.w, + u_flashColor.rgb, flashRange); + } + + // === Ambient === + vec3 ambient = u_ambient.rgb * albedo; + + // Combine surface + volumetric fog (fog is additive, not affected by surface) + vec3 color = (Lo + ambient) * exposure + volumetric; + o_color = vec4(color, 1.0); +} diff --git a/gameengine/packages/seed/shaders/spirv/textured.frag.spv b/gameengine/packages/seed/shaders/spirv/textured.frag.spv new file mode 100644 index 000000000..7e0d9cad0 Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/textured.frag.spv differ diff --git a/gameengine/packages/seed/shaders/spirv/textured.vert.glsl b/gameengine/packages/seed/shaders/spirv/textured.vert.glsl new file mode 100644 index 000000000..bb57601f6 --- /dev/null +++ b/gameengine/packages/seed/shaders/spirv/textured.vert.glsl @@ -0,0 +1,29 @@ +#version 450 + +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec2 a_uv; + +layout(set = 1, binding = 0) uniform VertexUniforms { + mat4 u_modelViewProj; + mat4 u_model; + vec4 u_surfaceNormal; + vec4 u_uvScale; + vec4 u_cameraPos; + mat4 u_shadowVP; +}; + +layout(location = 0) out vec2 v_uv; +layout(location = 1) out vec3 v_worldNormal; +layout(location = 2) out vec3 v_worldPos; +layout(location = 3) out vec3 v_cameraPos; +layout(location = 4) out vec4 v_shadowPos; + +void main() { + gl_Position = u_modelViewProj * vec4(a_position, 1.0); + v_uv = a_uv * u_uvScale.xy; + v_worldNormal = u_surfaceNormal.xyz; + vec4 wp = u_model * vec4(a_position, 1.0); + v_worldPos = wp.xyz; + v_cameraPos = u_cameraPos.xyz; + v_shadowPos = u_shadowVP * wp; +} diff --git a/gameengine/packages/seed/shaders/spirv/textured.vert.spv b/gameengine/packages/seed/shaders/spirv/textured.vert.spv new file mode 100644 index 000000000..8e2e661cd Binary files /dev/null and b/gameengine/packages/seed/shaders/spirv/textured.vert.spv differ diff --git a/gameengine/packages/seed/workflows/frame_tick.json b/gameengine/packages/seed/workflows/frame_tick.json index 58aa4ba70..8dd63254f 100644 --- a/gameengine/packages/seed/workflows/frame_tick.json +++ b/gameengine/packages/seed/workflows/frame_tick.json @@ -387,6 +387,23 @@ "body": "cube" } }, + { + "id": "draw_torch", + "name": "Draw Torch Viewmodel", + "type": "draw.viewmodel", + "typeVersion": 1, + "position": [1100, 0], + "parameters": { + "mesh": "torch_model", + "offset_x": 0.35, + "offset_y": -0.35, + "offset_z": -0.35, + "scale": 1.0, + "rot_x": -75, + "roughness": 0.5, + "metallic": 0.6 + } + }, { "id": "end_scene", "name": "End Scene Pass", @@ -565,6 +582,11 @@ } }, "draw_cube": { + "main": { + "0": [{ "node": "draw_torch", "type": "main", "index": 0 }] + } + }, + "draw_torch": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } diff --git a/gameengine/packages/seed/workflows/seed_game.json b/gameengine/packages/seed/workflows/seed_game.json index ee0447c91..cf03e696d 100644 --- a/gameengine/packages/seed/workflows/seed_game.json +++ b/gameengine/packages/seed/workflows/seed_game.json @@ -840,18 +840,52 @@ "typeVersion": 1, "position": [1100, 200], "parameters": { - "light_dir_x": -0.4, - "light_dir_y": -0.7, - "light_dir_z": -0.5, - "light_color_r": 1.0, - "light_color_g": 0.95, - "light_color_b": 0.85, - "light_intensity": 2.5, - "ambient_r": 0.06, - "ambient_g": 0.06, - "ambient_b": 0.08, + "light_dir_x": -0.3, + "light_dir_y": -0.5, + "light_dir_z": -0.6, + "light_color_r": 0.4, + "light_color_g": 0.45, + "light_color_b": 0.6, + "light_intensity": 0.3, + "ambient_r": 0.04, + "ambient_g": 0.04, + "ambient_b": 0.06, "ambient_intensity": 1.0, - "exposure": 0.6 + "exposure": 1.2 + } + }, + { + "id": "flashlight", + "name": "Flashlight (Camera Spotlight)", + "type": "spotlight.setup", + "typeVersion": 1, + "position": [1125, 200], + "parameters": { + "attach": "camera", + "inner_cone": 6, + "outer_cone": 16, + "intensity": 3.0, + "range": 25, + "color_r": 1.0, + "color_g": 0.95, + "color_b": 0.85, + "offset_y": -0.15 + } + }, + { + "id": "load_torch", + "name": "Create Flashlight Geometry", + "type": "geometry.create_flashlight", + "typeVersion": 1, + "position": [1135, 200], + "parameters": { + "name": "torch_model", + "segments": 16, + "body_radius": 0.045, + "body_length": 0.28, + "head_radius": 0.065, + "head_length": 0.09, + "lens_radius": 0.055 } }, { @@ -862,9 +896,11 @@ "position": [1150, 200], "parameters": { "map_size": 2048, - "scene_extent": 15, - "vertex_shader": "packages/seed/shaders/msl/shadow_depth.vert.metal", - "fragment_shader": "packages/seed/shaders/msl/shadow_depth.frag.metal" + "scene_extent": 15 + }, + "inputs": { + "vertex_shader": "shadow_vert_path", + "fragment_shader": "shadow_frag_path" } }, { @@ -1336,6 +1372,16 @@ } }, "lighting_setup": { + "main": { + "0": [{ "node": "flashlight", "type": "main", "index": 0 }] + } + }, + "flashlight": { + "main": { + "0": [{ "node": "load_torch", "type": "main", "index": 0 }] + } + }, + "load_torch": { "main": { "0": [{ "node": "shadow_setup", "type": "main", "index": 0 }] } diff --git a/gameengine/packages/standalone_cubes/workflows/cube_demo_simple.json b/gameengine/packages/standalone_cubes/workflows/cube_demo_simple.json index 52558d51f..c1b6d51a9 100644 --- a/gameengine/packages/standalone_cubes/workflows/cube_demo_simple.json +++ b/gameengine/packages/standalone_cubes/workflows/cube_demo_simple.json @@ -160,13 +160,14 @@ { "id": "set_running", "name": "Set Grid Running", - "type": "variable.set", + "type": "value.literal", "typeVersion": 1, "position": [2200, 0], "parameters": { - "key": "grid.running", "value": true, - "value_type": "bool" + "outputs": { + "value": "grid.running" + } } }, { diff --git a/gameengine/python/dev_commands.py b/gameengine/python/dev_commands.py index ecc765558..9045bbc2e 100755 --- a/gameengine/python/dev_commands.py +++ b/gameengine/python/dev_commands.py @@ -54,10 +54,21 @@ CMAKE_GENERATOR = { DEFAULT_BUILD_DIR = GENERATOR_DEFAULT_DIR[DEFAULT_GENERATOR] TRACE_ENV_VAR = "DEV_COMMANDS_TRACE" -DEFAULT_VCVARSALL = ( - "C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional" - "\\VC\\Auxiliary\\Build\\vcvarsall.bat" -) +def _find_vcvarsall() -> str: + """Auto-detect vcvarsall.bat across VS editions and versions.""" + if not IS_WINDOWS: + return "" + base = "C:\\Program Files\\Microsoft Visual Studio" + # Search newest VS version first, then editions + for version in ["18", "2022", "2019"]: + for edition in ["Community", "Professional", "Enterprise", "BuildTools"]: + bat = f"{base}\\{version}\\{edition}\\VC\\Auxiliary\\Build\\vcvarsall.bat" + if os.path.isfile(bat): + return bat + return "" + + +DEFAULT_VCVARSALL = _find_vcvarsall() def _sh_quote(s: str) -> str: """Minimal POSIX-style quoting for display purposes on non-Windows.""" @@ -184,15 +195,105 @@ def _has_cmake_cache(build_dir: str) -> bool: def dependencies(args: argparse.Namespace) -> None: - """Run Conan profile detection and install dependencies.""" + """Run Conan profile detection and install dependencies with C++20.""" cmd_detect = ["conan", "profile", "detect", "-f"] - cmd_install = ["conan", "install", ".", "-of", "build-ninja", "-b", "missing"] + cmd_install = ["conan", "install", ".", "-of", "build-ninja", "-b", "missing", + "-s", "compiler.cppstd=20"] conan_install_args = _strip_leading_double_dash(args.conan_install_args) if conan_install_args: cmd_install.extend(conan_install_args) run_argvs([cmd_detect, cmd_install], args.dry_run) +def generate(args: argparse.Namespace) -> None: + """Generate CMakeLists.txt from cmake_config.json using Jinja2.""" + env = {"PYTHONIOENCODING": "utf-8"} + cmd = [ + "python", "generate_cmake.py", + "--config", args.config, + "--output", args.output, + ] + if args.template: + cmd.extend(["--template", args.template]) + if args.validate: + cmd.append("--validate") + run_argvs([cmd], args.dry_run, env_overrides=env) + + # Fix CMakeUserPresets.json to only include existing preset files + if not args.dry_run and not args.validate: + _fix_cmake_user_presets() + + +def _fix_cmake_user_presets() -> None: + """Ensure CMakeUserPresets.json only includes existing preset files.""" + import json as json_mod + presets_path = Path("CMakeUserPresets.json") + if not presets_path.exists(): + return + try: + data = json_mod.loads(presets_path.read_text()) + includes = data.get("include", []) + valid = [p for p in includes if Path(p).exists()] + if len(valid) != len(includes): + data["include"] = valid + presets_path.write_text(json_mod.dumps(data, indent=4) + "\n") + print(f" Fixed CMakeUserPresets.json: kept {len(valid)}/{len(includes)} includes") + except (json_mod.JSONDecodeError, OSError): + pass + + +def full_build(args: argparse.Namespace) -> None: + """Run the full build pipeline: dependencies + generate + configure + build.""" + print("=== Step 1/4: Installing dependencies ===") + deps_args = argparse.Namespace( + dry_run=args.dry_run, + conan_install_args=None, + ) + dependencies(deps_args) + + print("\n=== Step 2/4: Generating CMakeLists.txt ===") + gen_args = argparse.Namespace( + dry_run=args.dry_run, + config="cmake_config.json", + template=None, + output="CMakeLists.txt", + validate=False, + ) + generate(gen_args) + + print("\n=== Step 3/4: Configuring CMake ===") + conf_args = argparse.Namespace( + dry_run=args.dry_run, + preset="conan-default", + generator=None, + build_dir=None, + build_type=args.build_type, + cmake_args=["-DBUILD_SDL3_APP=ON", "-DSDL_VERSION=SDL3"], + ) + configure(conf_args) + + print("\n=== Step 4/4: Building ===") + bld_args = argparse.Namespace( + dry_run=args.dry_run, + build_dir="build-ninja/build", + config=args.build_type, + target=args.target, + build_tool_args=None, + ) + build(bld_args) + + if args.run: + print("\n=== Running ===") + run_args = argparse.Namespace( + dry_run=args.dry_run, + build_dir="build-ninja/build/" + args.build_type, + target=None, + no_sync=False, + args=["--bootstrap", args.bootstrap, "--game", args.game], + ) + run_demo(run_args) + + def configure(args: argparse.Namespace) -> None: """Configure a CMake project based on the chosen generator and options.""" if args.preset: @@ -1187,6 +1288,51 @@ def main() -> int: ), ) deps.set_defaults(func=dependencies) + + gen = subparsers.add_parser("generate", help="generate CMakeLists.txt from JSON config") + gen.add_argument( + "--config", default="cmake_config.json", + help="path to cmake_config.json (default: cmake_config.json)", + ) + gen.add_argument( + "--template", default=None, + help="path to Jinja2 template (default: CMakeLists.txt.jinja2)", + ) + gen.add_argument( + "--output", default="CMakeLists.txt", + help="output CMakeLists.txt path (default: CMakeLists.txt)", + ) + gen.add_argument( + "--validate", action="store_true", + help="validate config without generating", + ) + gen.set_defaults(func=generate) + + allp = subparsers.add_parser( + "all", help="full pipeline: dependencies + generate + configure + build [+ run]" + ) + allp.add_argument( + "--build-type", default="Release", + help="build type (default: Release)", + ) + allp.add_argument( + "--target", default="sdl3_app", + help="build target (default: sdl3_app)", + ) + allp.add_argument( + "--run", action="store_true", + help="run the app after building", + ) + allp.add_argument( + "--bootstrap", default="bootstrap_windows" if IS_WINDOWS else "bootstrap_mac", + help="bootstrap package (auto-detected from platform)", + ) + allp.add_argument( + "--game", default="seed", + help="game package to run (default: seed)", + ) + allp.set_defaults(func=full_build) + conf = subparsers.add_parser("configure", help="configure CMake project") conf.add_argument( "--preset", diff --git a/gameengine/src/main.cpp b/gameengine/src/main.cpp index 106efa48e..62ecd752c 100644 --- a/gameengine/src/main.cpp +++ b/gameengine/src/main.cpp @@ -84,6 +84,23 @@ int main(int argc, char** argv) { } } + // Determine shader backend from bootstrap package + std::string shaderDir = "msl"; // default (Mac) + { + std::filesystem::path bootPkgPath = projectRoot / "packages" / bootstrapPackage / "package.json"; + if (std::filesystem::exists(bootPkgPath)) { + std::ifstream bootFile(bootPkgPath); + nlohmann::json bootJson; + bootFile >> bootJson; + if (bootJson.contains("config") && bootJson["config"].contains("renderer")) { + std::string renderer = bootJson["config"]["renderer"].get(); + if (renderer != "metal") shaderDir = "spirv"; + } + } + } + appContext.Set("shader_backend", shaderDir); + logger->Info("Shader backend: " + shaderDir + " (bootstrap: " + bootstrapPackage + ")"); + // Load and execute the default workflow std::filesystem::path mainWorkflowPath = projectRoot / "packages" / gamePackage / defaultWorkflow; if (!std::filesystem::exists(mainWorkflowPath)) { @@ -95,7 +112,11 @@ int main(int argc, char** argv) { sdl3cpp::services::impl::WorkflowDefinitionParser parser(logger); auto mainWorkflow = parser.ParseFile(mainWorkflowPath); - // Load workflow variables into context + // Load workflow variables into context, rewriting shader paths for platform + // Shader paths in workflows default to msl/ (Mac). Bootstrap determines the + // actual backend — if not Metal, rewrite msl/ → spirv/ and .metal → .spv + const bool rewriteShaders = (shaderDir != "msl"); + for (const auto& [name, var] : mainWorkflow.variables) { if (var.defaultValue.empty()) continue; if (var.type == "number") { @@ -103,7 +124,17 @@ int main(int argc, char** argv) { appContext.Set(name, std::stod(var.defaultValue)); } catch (...) {} } else if (var.type == "string") { - appContext.Set(name, var.defaultValue); + std::string val = var.defaultValue; + if (rewriteShaders && val.find("/shaders/msl/") != std::string::npos) { + // Rewrite msl path to spirv: shaders/msl/foo.metal → shaders/spirv/foo.spv + auto pos = val.find("/shaders/msl/"); + val.replace(pos, 13, "/shaders/spirv/"); + // .vert.metal → .vert.spv, .frag.metal → .frag.spv, .comp.metal → .comp.spv + auto ext = val.rfind(".metal"); + if (ext != std::string::npos) val.replace(ext, 6, ".spv"); + logger->Info("Shader rewrite: " + name + " → " + val); + } + appContext.Set(name, val); } else if (var.type == "bool") { appContext.Set(name, var.defaultValue == "true"); } else { diff --git a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp index a2c2db917..2a571fcc2 100644 --- a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp +++ b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp @@ -19,6 +19,9 @@ std::string WorkflowGpuShaderCompileStep::GetPluginId() const { std::string WorkflowGpuShaderCompileStep::ResolvePath(const std::string& path) { if (path.empty() || path[0] != '~') return path; const char* home = std::getenv("HOME"); +#if defined(_WIN32) + if (!home) home = std::getenv("USERPROFILE"); +#endif if (!home) return path; return std::string(home) + path.substr(1); } @@ -82,10 +85,14 @@ void WorkflowGpuShaderCompileStep::Execute(const WorkflowStepDefinition& step, W // Detect shader format from driver SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_SPIRV; const char* driver = SDL_GetGPUDeviceDriver(device); + std::string driver_name = driver ? driver : ""; std::string format_name = "spirv"; - if (driver && std::string(driver) == "metal") { + if (driver_name == "metal") { format = SDL_GPU_SHADERFORMAT_MSL; format_name = "msl"; + } else if (driver_name == "direct3d12") { + format = SDL_GPU_SHADERFORMAT_DXIL; + format_name = "dxil"; } // MSL uses "main0" entrypoint, SPIRV uses "main" diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_draw_viewmodel_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_draw_viewmodel_step.cpp new file mode 100644 index 000000000..fcc7dc570 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_draw_viewmodel_step.cpp @@ -0,0 +1,153 @@ +#include "services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp" +#include "services/interfaces/workflow/rendering/rendering_types.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowDrawViewmodelStep::WorkflowDrawViewmodelStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowDrawViewmodelStep::GetPluginId() const { + return "draw.viewmodel"; +} + +void WorkflowDrawViewmodelStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + if (context.GetBool("frame_skip", false)) return; + + WorkflowStepParameterResolver params; + + auto getStr = [&](const char* name, const std::string& def) -> std::string { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def; + }; + auto getNum = [&](const char* name, float def) -> float { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + + const std::string mesh_name = getStr("mesh", "model"); + const std::string tex_name = getStr("texture", ""); + // Viewmodel offset from camera (right, down, forward) + const float offset_x = getNum("offset_x", 0.35f); + const float offset_y = getNum("offset_y", -0.3f); + const float offset_z = getNum("offset_z", -0.5f); + const float model_scale = getNum("scale", 0.15f); + const float rot_x = getNum("rot_x", 0.0f); + const float rot_y = getNum("rot_y", 0.0f); + const float rot_z = getNum("rot_z", 0.0f); + const float roughness = getNum("roughness", 0.6f); + const float metallic = getNum("metallic", 0.4f); + + auto* pass = context.Get("gpu_render_pass", nullptr); + auto* cmd = context.Get("gpu_command_buffer", nullptr); + auto* pipeline = context.Get("gpu_pipeline_textured", nullptr); + if (!pass || !cmd || !pipeline) return; + + // Get mesh buffers + auto* vb = context.Get("plane_" + mesh_name + "_vb", nullptr); + auto* ib = context.Get("plane_" + mesh_name + "_ib", nullptr); + const auto* mesh_meta = context.TryGet("plane_" + mesh_name); + if (!vb || !ib || !mesh_meta) { + if (logger_) logger_->Warn("draw.viewmodel: Mesh '" + mesh_name + "' not found"); + return; + } + uint32_t index_count = (*mesh_meta)["index_count"]; + + // Build viewmodel MVP: rendered in camera-local space + // The viewmodel uses its own near-field projection to prevent clipping + auto viewMatrix = context.Get("render.view_matrix", glm::mat4(1.0f)); + auto projMatrix = context.Get("render.proj_matrix", glm::mat4(1.0f)); + auto camPos = context.Get("render.camera_pos", glm::vec3(0.0f)); + + // Extract camera basis vectors + glm::vec3 camRight = glm::vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]); + glm::vec3 camUp = glm::vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]); + glm::vec3 camForward = -glm::vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); + + // Position the viewmodel relative to the camera (locked to view) + glm::vec3 modelPos = camPos + camRight * offset_x + camUp * offset_y + camForward * (-offset_z); + + // Build local rotation first (to orient the mesh correctly) + glm::mat4 localRot = glm::mat4(1.0f); + if (rot_x != 0.0f) localRot = glm::rotate(localRot, glm::radians(rot_x), glm::vec3(1, 0, 0)); + if (rot_y != 0.0f) localRot = glm::rotate(localRot, glm::radians(rot_y), glm::vec3(0, 1, 0)); + if (rot_z != 0.0f) localRot = glm::rotate(localRot, glm::radians(rot_z), glm::vec3(0, 0, 1)); + + // Camera orientation matrix (maps local space to world-aligned-with-camera) + glm::mat4 camOrient = glm::mat4(1.0f); + camOrient[0] = glm::vec4(camRight, 0.0f); + camOrient[1] = glm::vec4(camUp, 0.0f); + camOrient[2] = glm::vec4(-camForward, 0.0f); // -forward because camera looks down -Z + + // Final model: translate to position, orient to camera, apply local rotation, then scale + glm::mat4 model = glm::translate(glm::mat4(1.0f), modelPos) * camOrient * localRot * glm::scale(glm::mat4(1.0f), glm::vec3(model_scale)); + + glm::mat4 mvp = projMatrix * viewMatrix * model; + + // Surface normal pointing up from the viewmodel + glm::vec3 surfaceNormal = camUp; + + rendering::VertexUniformData vu = {}; + 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[0] = surfaceNormal.x; vu.normal[1] = surfaceNormal.y; vu.normal[2] = surfaceNormal.z; + 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; + auto shadowVP = context.Get("render.shadow_vp", glm::mat4(1.0f)); + std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16); + + auto fu = context.Get("render.frag_uniforms", rendering::FragmentUniformData{}); + fu.material[0] = roughness; + fu.material[1] = metallic; + + SDL_BindGPUGraphicsPipeline(pass, pipeline); + + // Bind texture if specified, else use a default + SDL_GPUTexture* texture = nullptr; + SDL_GPUSampler* sampler = nullptr; + if (!tex_name.empty()) { + texture = context.Get(tex_name + "_gpu", nullptr); + sampler = context.Get(tex_name + "_sampler", nullptr); + } + // Fall back to floor texture or any available texture + if (!texture) texture = context.Get("floor_texture_gpu", nullptr); + if (!sampler) sampler = context.Get("floor_texture_sampler", nullptr); + + if (texture && sampler) { + auto* shadow_tex = context.Get("shadow_depth_texture", nullptr); + auto* shadow_samp = context.Get("shadow_depth_sampler", nullptr); + if (shadow_tex && shadow_samp) { + SDL_GPUTextureSamplerBinding bindings[2] = {}; + bindings[0].texture = texture; + bindings[0].sampler = sampler; + bindings[1].texture = shadow_tex; + bindings[1].sampler = shadow_samp; + SDL_BindGPUFragmentSamplers(pass, 0, bindings, 2); + } else { + SDL_GPUTextureSamplerBinding binding = {}; + binding.texture = texture; + binding.sampler = sampler; + SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); + } + } + + SDL_GPUBufferBinding vb_binding = {}; + vb_binding.buffer = vb; + SDL_BindGPUVertexBuffers(pass, 0, &vb_binding, 1); + SDL_GPUBufferBinding ib_binding = {}; + ib_binding.buffer = ib; + SDL_BindGPUIndexBuffer(pass, &ib_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu)); + SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu)); + SDL_DrawGPUIndexedPrimitives(pass, index_count, 1, 0, 0, 0); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_geometry_create_flashlight_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_geometry_create_flashlight_step.cpp new file mode 100644 index 000000000..9a768a4fc --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_geometry_create_flashlight_step.cpp @@ -0,0 +1,167 @@ +#include "services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowGeometryCreateFlashlightStep::WorkflowGeometryCreateFlashlightStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowGeometryCreateFlashlightStep::GetPluginId() const { + return "geometry.create_flashlight"; +} + +void WorkflowGeometryCreateFlashlightStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + WorkflowStepParameterResolver params; + + auto getStr = [&](const char* pname, const std::string& def) -> std::string { + const auto* p = params.FindParameter(step, pname); + return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def; + }; + auto getNum = [&](const char* pname, float def) -> float { + const auto* p = params.FindParameter(step, pname); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + + const std::string name = getStr("name", "flashlight"); + const int segments = static_cast(getNum("segments", 12)); + const float body_radius = getNum("body_radius", 0.025f); + const float body_length = getNum("body_length", 0.25f); + const float head_radius = getNum("head_radius", 0.04f); + const float head_length = getNum("head_length", 0.08f); + const float lens_radius = getNum("lens_radius", 0.035f); + + // Vertex format: float3 pos + float2 uv = 20 bytes (matches plane/textured pipeline) + struct PosUvVertex { + float x, y, z; + float u, v; + }; + + std::vector vertices; + std::vector indices; + + const float PI = 3.14159265358979f; + + // Helper: add a cylinder section + auto addCylinder = [&](float r1, float r2, float y_start, float y_end, float uv_start, float uv_end) { + uint16_t base = static_cast(vertices.size()); + + for (int i = 0; i <= segments; ++i) { + float angle = (static_cast(i) / segments) * 2.0f * PI; + float cos_a = std::cos(angle); + float sin_a = std::sin(angle); + float u = static_cast(i) / segments; + + // Bottom ring + vertices.push_back({cos_a * r1, y_start, sin_a * r1, u, uv_start}); + // Top ring + vertices.push_back({cos_a * r2, y_end, sin_a * r2, u, uv_end}); + } + + for (int i = 0; i < segments; ++i) { + uint16_t b = base + static_cast(i * 2); + indices.push_back(b); + indices.push_back(b + 1); + indices.push_back(b + 2); + indices.push_back(b + 2); + indices.push_back(b + 1); + indices.push_back(b + 3); + } + }; + + // Helper: add a disc cap + auto addCap = [&](float radius, float y, float uv_v, bool flip) { + uint16_t center = static_cast(vertices.size()); + vertices.push_back({0.0f, y, 0.0f, 0.5f, uv_v}); + + for (int i = 0; i <= segments; ++i) { + float angle = (static_cast(i) / segments) * 2.0f * PI; + vertices.push_back({std::cos(angle) * radius, y, std::sin(angle) * radius, + 0.5f + 0.5f * std::cos(angle), uv_v}); + } + + for (int i = 0; i < segments; ++i) { + uint16_t a = center + 1 + static_cast(i); + uint16_t b = a + 1; + if (flip) { + indices.push_back(center); indices.push_back(b); indices.push_back(a); + } else { + indices.push_back(center); indices.push_back(a); indices.push_back(b); + } + } + }; + + // Build flashlight geometry (Y-axis is the barrel direction) + // Body: long cylinder (handle/grip) + addCap(body_radius, 0.0f, 0.0f, true); // bottom cap + addCylinder(body_radius, body_radius, 0.0f, body_length, 0.0f, 0.6f); + + // Head: slightly wider cylinder (where the bulb sits) + addCylinder(body_radius, head_radius, body_length, body_length + 0.02f, 0.6f, 0.7f); + addCylinder(head_radius, head_radius, body_length + 0.02f, body_length + head_length, 0.7f, 0.9f); + + // Lens: flat disc at the front (the light-emitting surface) + addCap(lens_radius, body_length + head_length, 1.0f, false); + + const uint32_t vertex_count = static_cast(vertices.size()); + const uint32_t index_count = static_cast(indices.size()); + const uint32_t vertex_size = vertex_count * sizeof(PosUvVertex); + const uint32_t index_size = index_count * sizeof(uint16_t); + + SDL_GPUDevice* device = context.Get("gpu_device", nullptr); + if (!device) throw std::runtime_error("geometry.create_flashlight: GPU device not found"); + + SDL_GPUBufferCreateInfo vbuf_info = {}; + vbuf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + vbuf_info.size = vertex_size; + SDL_GPUBuffer* vertex_buffer = SDL_CreateGPUBuffer(device, &vbuf_info); + + SDL_GPUBufferCreateInfo ibuf_info = {}; + ibuf_info.usage = SDL_GPU_BUFFERUSAGE_INDEX; + ibuf_info.size = index_size; + SDL_GPUBuffer* index_buffer = SDL_CreateGPUBuffer(device, &ibuf_info); + + SDL_GPUTransferBufferCreateInfo tbuf_info = {}; + tbuf_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbuf_info.size = vertex_size + index_size; + SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tbuf_info); + + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(device, transfer, false)); + std::memcpy(mapped, vertices.data(), vertex_size); + std::memcpy(mapped + vertex_size, indices.data(), index_size); + SDL_UnmapGPUTransferBuffer(device, transfer); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device); + SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd); + + SDL_GPUTransferBufferLocation src_vert = {}; src_vert.transfer_buffer = transfer; + SDL_GPUBufferRegion dst_vert = {}; dst_vert.buffer = vertex_buffer; dst_vert.size = vertex_size; + SDL_UploadToGPUBuffer(copy_pass, &src_vert, &dst_vert, false); + + SDL_GPUTransferBufferLocation src_idx = {}; src_idx.transfer_buffer = transfer; src_idx.offset = vertex_size; + SDL_GPUBufferRegion dst_idx = {}; dst_idx.buffer = index_buffer; dst_idx.size = index_size; + SDL_UploadToGPUBuffer(copy_pass, &src_idx, &dst_idx, false); + + SDL_EndGPUCopyPass(copy_pass); + SDL_SubmitGPUCommandBuffer(cmd); + SDL_ReleaseGPUTransferBuffer(device, transfer); + + context.Set("plane_" + name + "_vb", vertex_buffer); + context.Set("plane_" + name + "_ib", index_buffer); + context.Set("plane_" + name, nlohmann::json{ + {"vertex_count", vertex_count}, {"index_count", index_count}, {"stride", 20} + }); + + if (logger_) { + logger_->Info("geometry.create_flashlight: '" + name + "' created (" + + std::to_string(vertex_count) + " verts, " + + std::to_string(index_count) + " indices)"); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_model_load_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_model_load_step.cpp new file mode 100644 index 000000000..c8d7565d2 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_model_load_step.cpp @@ -0,0 +1,184 @@ +#include "services/interfaces/workflow/rendering/workflow_model_load_step.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowModelLoadStep::WorkflowModelLoadStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowModelLoadStep::GetPluginId() const { + return "model.load"; +} + +void WorkflowModelLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + WorkflowStepParameterResolver params; + + auto getStr = [&](const char* name, const std::string& def) -> std::string { + const auto* p = params.FindParameter(step, name); + if (p && p->type == WorkflowParameterValue::Type::String) return p->stringValue; + auto it = step.inputs.find(name); + if (it != step.inputs.end()) { + const auto* ctx = context.TryGet(it->second); + if (ctx) return *ctx; + } + return def; + }; + auto getNum = [&](const char* name, float def) -> float { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + + const std::string file_path = getStr("file_path", ""); + const std::string name = getStr("name", "model"); + const float scale = getNum("scale", 1.0f); + + if (file_path.empty()) { + throw std::runtime_error("model.load: 'file_path' parameter is required"); + } + + SDL_GPUDevice* device = context.Get("gpu_device", nullptr); + if (!device) { + throw std::runtime_error("model.load: GPU device not found in context"); + } + + // Load with Assimp + Assimp::Importer importer; + const aiScene* scene = importer.ReadFile(file_path, + aiProcess_Triangulate | + aiProcess_GenNormals | + aiProcess_FlipUVs | + aiProcess_JoinIdenticalVertices | + aiProcess_PreTransformVertices); + + if (!scene || !scene->mRootNode || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)) { + throw std::runtime_error("model.load: Failed to load '" + file_path + "': " + + importer.GetErrorString()); + } + + // Collect all mesh vertices/indices into a single buffer + // Vertex format matches geometry.create_plane: float3 pos + float2 uv = 20 bytes + struct PosUvVertex { + float x, y, z; + float u, v; + }; + + std::vector vertices; + std::vector indices; + uint16_t baseVertex = 0; + + for (unsigned int m = 0; m < scene->mNumMeshes; ++m) { + const aiMesh* mesh = scene->mMeshes[m]; + + for (unsigned int i = 0; i < mesh->mNumVertices; ++i) { + PosUvVertex vert; + vert.x = mesh->mVertices[i].x * scale; + vert.y = mesh->mVertices[i].y * scale; + vert.z = mesh->mVertices[i].z * scale; + if (mesh->mTextureCoords[0]) { + vert.u = mesh->mTextureCoords[0][i].x; + vert.v = mesh->mTextureCoords[0][i].y; + } else { + vert.u = 0.0f; + vert.v = 0.0f; + } + vertices.push_back(vert); + } + + for (unsigned int f = 0; f < mesh->mNumFaces; ++f) { + const aiFace& face = mesh->mFaces[f]; + for (unsigned int j = 0; j < face.mNumIndices; ++j) { + indices.push_back(static_cast(baseVertex + face.mIndices[j])); + } + } + baseVertex += static_cast(mesh->mNumVertices); + } + + if (vertices.empty()) { + throw std::runtime_error("model.load: No vertices found in '" + file_path + "'"); + } + + const uint32_t vertex_count = static_cast(vertices.size()); + const uint32_t index_count = static_cast(indices.size()); + const uint32_t vertex_size = vertex_count * sizeof(PosUvVertex); + const uint32_t index_size = index_count * sizeof(uint16_t); + + // Create GPU buffers (same pattern as geometry.create_plane) + SDL_GPUBufferCreateInfo vbuf_info = {}; + vbuf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + vbuf_info.size = vertex_size; + SDL_GPUBuffer* vertex_buffer = SDL_CreateGPUBuffer(device, &vbuf_info); + + SDL_GPUBufferCreateInfo ibuf_info = {}; + ibuf_info.usage = SDL_GPU_BUFFERUSAGE_INDEX; + ibuf_info.size = index_size; + SDL_GPUBuffer* index_buffer = SDL_CreateGPUBuffer(device, &ibuf_info); + + if (!vertex_buffer || !index_buffer) { + throw std::runtime_error("model.load: Failed to create GPU buffers"); + } + + // Upload via transfer buffer + SDL_GPUTransferBufferCreateInfo tbuf_info = {}; + tbuf_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbuf_info.size = vertex_size + index_size; + SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tbuf_info); + + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(device, transfer, false)); + std::memcpy(mapped, vertices.data(), vertex_size); + std::memcpy(mapped + vertex_size, indices.data(), index_size); + SDL_UnmapGPUTransferBuffer(device, transfer); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device); + SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd); + + SDL_GPUTransferBufferLocation src_vert = {}; + src_vert.transfer_buffer = transfer; + src_vert.offset = 0; + SDL_GPUBufferRegion dst_vert = {}; + dst_vert.buffer = vertex_buffer; + dst_vert.size = vertex_size; + SDL_UploadToGPUBuffer(copy_pass, &src_vert, &dst_vert, false); + + SDL_GPUTransferBufferLocation src_idx = {}; + src_idx.transfer_buffer = transfer; + src_idx.offset = vertex_size; + SDL_GPUBufferRegion dst_idx = {}; + dst_idx.buffer = index_buffer; + dst_idx.size = index_size; + SDL_UploadToGPUBuffer(copy_pass, &src_idx, &dst_idx, false); + + SDL_EndGPUCopyPass(copy_pass); + SDL_SubmitGPUCommandBuffer(cmd); + SDL_ReleaseGPUTransferBuffer(device, transfer); + + // Store using same convention as geometry.create_plane (plane_ prefix) + context.Set("plane_" + name + "_vb", vertex_buffer); + context.Set("plane_" + name + "_ib", index_buffer); + + nlohmann::json meta = { + {"vertex_count", vertex_count}, + {"index_count", index_count}, + {"stride", 20}, + {"meshes", scene->mNumMeshes}, + {"file", file_path} + }; + context.Set("plane_" + name, meta); + + if (logger_) { + logger_->Info("model.load: '" + name + "' loaded from " + file_path + + " (" + std::to_string(vertex_count) + " verts, " + + std::to_string(index_count) + " indices, " + + std::to_string(scene->mNumMeshes) + " meshes)"); + } +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp index b74238dba..b7f3574ea 100644 --- a/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp +++ b/gameengine/src/services/impl/workflow/rendering/workflow_render_prepare_step.cpp @@ -93,6 +93,40 @@ void WorkflowRenderPrepareStep::Execute(const WorkflowStepDefinition& step, Work fu.light_color[3] = lighting->value("exposure", 1.0f); } + // --- Spotlight / flashlight (reads from context, set by spotlight.setup step) --- + const auto* spot = context.TryGet("spotlight.state"); + if (spot) { + std::string attach = spot->value("attach", "camera"); + glm::vec3 spotPos, spotDir; + auto offset = spot->value("offset", std::vector{0,0,0}); + glm::vec3 off(offset.size()>0?offset[0]:0, offset.size()>1?offset[1]:0, offset.size()>2?offset[2]:0); + + if (attach == "camera") { + spotPos = cameraPos + off; + spotDir = -glm::vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]); + } else { + auto p = spot->value("position", std::vector{0,0,0}); + auto d = spot->value("direction", std::vector{0,0,-1}); + spotPos = glm::vec3(p[0], p[1], p[2]) + off; + spotDir = glm::normalize(glm::vec3(d[0], d[1], d[2])); + } + + fu.flash_pos[0] = spotPos.x; + fu.flash_pos[1] = spotPos.y; + fu.flash_pos[2] = spotPos.z; + fu.flash_pos[3] = std::cos(glm::radians(spot->value("inner_cone", 12.0f))); + fu.flash_dir[0] = spotDir.x; + fu.flash_dir[1] = spotDir.y; + fu.flash_dir[2] = spotDir.z; + fu.flash_dir[3] = std::cos(glm::radians(spot->value("outer_cone", 25.0f))); + auto col = spot->value("color", std::vector{1,1,1}); + float spotIntensity = spot->value("intensity", 2.5f); + fu.flash_color[0] = (col.size() > 0 ? col[0] : 1.0f) * spotIntensity; + fu.flash_color[1] = (col.size() > 1 ? col[1] : 1.0f) * spotIntensity; + fu.flash_color[2] = (col.size() > 2 ? col[2] : 1.0f) * spotIntensity; + fu.flash_color[3] = spot->value("range", 20.0f); + } + context.Set("render.frag_uniforms", fu); if (logger_) { diff --git a/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_setup_step.cpp b/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_setup_step.cpp new file mode 100644 index 000000000..f83d20480 --- /dev/null +++ b/gameengine/src/services/impl/workflow/rendering/workflow_spotlight_setup_step.cpp @@ -0,0 +1,74 @@ +#include "services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp" +#include "services/interfaces/workflow/workflow_step_parameter_resolver.hpp" + +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowSpotlightSetupStep::WorkflowSpotlightSetupStep(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::string WorkflowSpotlightSetupStep::GetPluginId() const { + return "spotlight.setup"; +} + +void WorkflowSpotlightSetupStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + WorkflowStepParameterResolver params; + + auto getNum = [&](const char* name, float def) -> float { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; + auto getStr = [&](const char* name, const std::string& def) -> std::string { + const auto* p = params.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::String) ? p->stringValue : def; + }; + + std::string attach = getStr("attach", "camera"); + float inner_cone = getNum("inner_cone", 12.0f); + float outer_cone = getNum("outer_cone", 25.0f); + float intensity = getNum("intensity", 2.5f); + float range = getNum("range", 20.0f); + float color_r = getNum("color_r", 1.0f); + float color_g = getNum("color_g", 0.95f); + float color_b = getNum("color_b", 0.85f); + float offset_x = getNum("offset_x", 0.0f); + float offset_y = getNum("offset_y", 0.0f); + float offset_z = getNum("offset_z", 0.0f); + + nlohmann::json spotlight; + spotlight["inner_cone"] = inner_cone; + spotlight["outer_cone"] = outer_cone; + spotlight["intensity"] = intensity; + spotlight["range"] = range; + spotlight["color"] = {color_r, color_g, color_b}; + spotlight["attach"] = attach; + spotlight["offset"] = {offset_x, offset_y, offset_z}; + + // If attached to camera, position and direction will be filled per-frame + // by the render.prepare step reading camera state. + // For static spotlights, set explicit position/direction params. + if (attach == "camera") { + // Will be updated each frame in render.prepare from camera state + spotlight["position"] = {0, 0, 0}; + spotlight["direction"] = {0, 0, -1}; + } else { + spotlight["position"] = { + getNum("pos_x", 0.0f), + getNum("pos_y", 0.0f), + getNum("pos_z", 0.0f) + }; + spotlight["direction"] = { + getNum("dir_x", 0.0f), + getNum("dir_y", -1.0f), + getNum("dir_z", 0.0f) + }; + } + + context.Set("spotlight.state", spotlight); +} + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/scene/workflow_scene_add_geometry_step.cpp b/gameengine/src/services/impl/workflow/scene/workflow_scene_add_geometry_step.cpp index 758b5ed0d..02734a56b 100644 --- a/gameengine/src/services/impl/workflow/scene/workflow_scene_add_geometry_step.cpp +++ b/gameengine/src/services/impl/workflow/scene/workflow_scene_add_geometry_step.cpp @@ -5,7 +5,14 @@ #include #include +#include + +#if defined(_WIN32) +#include +#pragma comment(lib, "rpcrt4.lib") +#else #include +#endif namespace sdl3cpp::services::impl { @@ -47,11 +54,20 @@ void WorkflowSceneAddGeometryStep::Execute(const WorkflowStepDefinition& step, obj.objectType = "geometry_object"; // Create a unique object ID +#if defined(_WIN32) + UUID uuid_val; + UuidCreate(&uuid_val); + RPC_CSTR str = nullptr; + UuidToStringA(&uuid_val, &str); + std::string objectId(reinterpret_cast(str)); + RpcStringFreeA(&str); +#else uuid_t uuid_val; uuid_generate(uuid_val); char uuid_str[37]; uuid_unparse(uuid_val, uuid_str); std::string objectId(uuid_str); +#endif // Store object_id in context for reference context.Set(outputKey, objectId); diff --git a/gameengine/src/services/impl/workflow/scene/workflow_scene_create_step.cpp b/gameengine/src/services/impl/workflow/scene/workflow_scene_create_step.cpp index e0747ddca..8566f84d6 100644 --- a/gameengine/src/services/impl/workflow/scene/workflow_scene_create_step.cpp +++ b/gameengine/src/services/impl/workflow/scene/workflow_scene_create_step.cpp @@ -4,7 +4,34 @@ #include #include +#include + +#if defined(_WIN32) +#include +#pragma comment(lib, "rpcrt4.lib") +#else #include +#endif + +namespace { +std::string generate_uuid() { +#if defined(_WIN32) + UUID uuid_val; + UuidCreate(&uuid_val); + RPC_CSTR str = nullptr; + UuidToStringA(&uuid_val, &str); + std::string result(reinterpret_cast(str)); + RpcStringFreeA(&str); + return result; +#else + uuid_t uuid_val; + uuid_generate(uuid_val); + char uuid_str[37]; + uuid_unparse(uuid_val, uuid_str); + return std::string(uuid_str); +#endif +} +} // namespace namespace sdl3cpp::services::impl { @@ -25,12 +52,7 @@ void WorkflowSceneCreateStep::Execute(const WorkflowStepDefinition& step, Workfl WorkflowStepIoResolver resolver; const std::string outputKey = resolver.GetRequiredOutputKey(step, "scene_id"); - // Generate a unique scene ID - uuid_t uuid_val; - uuid_generate(uuid_val); - char uuid_str[37]; - uuid_unparse(uuid_val, uuid_str); - std::string sceneId(uuid_str); + std::string sceneId = generate_uuid(); // Store scene_id in context context.Set(outputKey, sceneId); diff --git a/gameengine/src/services/impl/workflow/workflow_executor.cpp b/gameengine/src/services/impl/workflow/workflow_executor.cpp index be8889f80..061e580c2 100644 --- a/gameengine/src/services/impl/workflow/workflow_executor.cpp +++ b/gameengine/src/services/impl/workflow/workflow_executor.cpp @@ -15,7 +15,10 @@ WorkflowExecutor::WorkflowExecutor(std::shared_ptr regist } void WorkflowExecutor::Execute(const WorkflowDefinition& workflow, WorkflowContext& context) { - if (logger_) { + // Only log step-by-step for the top-level workflow (not sub-workflows in frame loops) + const bool verbose = !context.TryGet("_in_frame_loop"); + + if (logger_ && verbose) { logger_->Trace("WorkflowExecutor", "Execute", "steps=" + std::to_string(workflow.steps.size()), "Starting workflow execution"); @@ -30,18 +33,17 @@ void WorkflowExecutor::Execute(const WorkflowDefinition& workflow, WorkflowConte } continue; } - if (logger_) { + if (logger_ && verbose) { logger_->Info("WorkflowExecutor: executing step " + std::to_string(i + 1) + "/" + std::to_string(workflow.steps.size()) + " plugin='" + step.plugin + "' id='" + step.id + "'"); } handler->Execute(step, context); - if (logger_) { + if (logger_ && verbose) { logger_->Info("WorkflowExecutor: completed step '" + step.plugin + "' id='" + step.id + "'"); } } - if (logger_) { + if (logger_ && verbose) { logger_->Info("WorkflowExecutor: Workflow execution complete (" + std::to_string(workflow.steps.size()) + " steps)"); - logger_->Trace("WorkflowExecutor", "Execute", "", "Workflow execution complete"); } } diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_control_while_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_control_while_step.cpp index 8af765a10..021854fb8 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_control_while_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_control_while_step.cpp @@ -57,6 +57,9 @@ void WorkflowControlWhileStep::Execute( (maxIterations > 0 ? ", max=" + std::to_string(maxIterations) : "")); } + // Suppress per-step logging inside the frame loop for performance + context.Set("_in_frame_loop", true); + // Execute loop uint32_t iteration = 0; while (context.GetBool(conditionKey, false)) { diff --git a/gameengine/src/services/impl/workflow/workflow_registrar.cpp b/gameengine/src/services/impl/workflow/workflow_registrar.cpp index 2686a13b0..ecad7b543 100644 --- a/gameengine/src/services/impl/workflow/workflow_registrar.cpp +++ b/gameengine/src/services/impl/workflow/workflow_registrar.cpp @@ -36,6 +36,10 @@ #include "services/interfaces/workflow/rendering/workflow_frame_end_gpu_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_textured_step.hpp" #include "services/interfaces/workflow/rendering/workflow_lighting_setup_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_model_load_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp" +#include "services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp" #include "services/interfaces/workflow/rendering/workflow_draw_textured_box_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_setup_step.hpp" #include "services/interfaces/workflow/rendering/workflow_shadow_pass_step.hpp" @@ -266,6 +270,10 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr reg registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); + registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); registry->RegisterStep(std::make_shared(logger_)); diff --git a/gameengine/src/services/interfaces/workflow/rendering/rendering_types.hpp b/gameengine/src/services/interfaces/workflow/rendering/rendering_types.hpp index 5b3dc798d..aaabcccce 100644 --- a/gameengine/src/services/interfaces/workflow/rendering/rendering_types.hpp +++ b/gameengine/src/services/interfaces/workflow/rendering/rendering_types.hpp @@ -26,6 +26,9 @@ struct FragmentUniformData { float light_color[4]; float ambient[4]; float material[4]; // x=roughness, y=metallic, zw=unused + float flash_pos[4]; // xyz=position, w=inner cone cos + float flash_dir[4]; // xyz=direction, w=outer cone cos + float flash_color[4]; // rgb=color*intensity, a=range }; } // namespace sdl3cpp::services::rendering diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp new file mode 100644 index 000000000..1f1c114ab --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_draw_viewmodel_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowDrawViewmodelStep : public IWorkflowStep { +public: + explicit WorkflowDrawViewmodelStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp new file mode 100644 index 000000000..6fb536dab --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_geometry_create_flashlight_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowGeometryCreateFlashlightStep : public IWorkflowStep { +public: + explicit WorkflowGeometryCreateFlashlightStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_model_load_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_model_load_step.hpp new file mode 100644 index 000000000..9e58d82f9 --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_model_load_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowModelLoadStep : public IWorkflowStep { +public: + explicit WorkflowModelLoadStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp b/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp new file mode 100644 index 000000000..93b40e13c --- /dev/null +++ b/gameengine/src/services/interfaces/workflow/rendering/workflow_spotlight_setup_step.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "services/interfaces/i_workflow_step.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowSpotlightSetupStep : public IWorkflowStep { +public: + explicit WorkflowSpotlightSetupStep(std::shared_ptr logger); + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; +private: + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl