From fea9981787fd73d3a260f6a1587f462d9a2bc6a3 Mon Sep 17 00:00:00 2001 From: olracrafter Date: Thu, 3 Sep 2026 07:54:17 +0200 Subject: [PATCH 1/2] lights: add light cookies --- samples/core/editors/unit.lua | 4 +- samples/core/shaders/lighting.shader | 91 +++++++++++--- src/device/pipeline.cpp | 128 +++++++++++++++++++ src/device/pipeline.h | 36 ++++-- src/lua/lua_api.cpp | 22 ++++ src/resource/render_config_resource.cpp | 7 ++ src/resource/render_config_resource.h | 1 + src/resource/texture_resource.cpp | 7 +- src/resource/texture_resource.h | 2 + src/resource/types.h | 4 +- src/resource/unit_compiler.cpp | 34 ++++- src/world/render_world.cpp | 160 +++++++++++++++++++----- src/world/render_world.h | 17 +++ src/world/types.h | 2 + tools/api/engine_api.vala | 20 ++- tools/level_editor/unit.vala | 8 ++ tools/resource/types.vala | 31 +++++ 17 files changed, 507 insertions(+), 67 deletions(-) diff --git a/samples/core/editors/unit.lua b/samples/core/editors/unit.lua index 4b2a3b4042..52ad8c6a6f 100644 --- a/samples/core/editors/unit.lua +++ b/samples/core/editors/unit.lua @@ -494,7 +494,7 @@ function UnitBox:draw() end end -function UnitBox:set_light(type, range, intensity, angle, cr, cg, cb, bias, cast_shadows) +function UnitBox:set_light(type, range, intensity, angle, cr, cg, cb, bias, cast_shadows, cookie, cookie_scale, cookie_x, cookie_y) local nv, nq, nm = Device.temp_count() local light = RenderWorld.light_instance(self._rw, self._unit_id) if light then @@ -505,6 +505,8 @@ function UnitBox:set_light(type, range, intensity, angle, cr, cg, cb, bias, cast RenderWorld.light_set_spot_angle(self._rw, light, angle) RenderWorld.light_set_shadow_bias(self._rw, light, bias) RenderWorld.light_set_cast_shadows(self._rw, light, cast_shadows) + RenderWorld.light_set_cookie(self._rw, light, cookie) + RenderWorld.light_set_cookie_transform(self._rw, light, Vector3(cookie_scale, cookie_x, cookie_y)) end Device.set_temp_count(nv, nq, nm) end diff --git a/samples/core/shaders/lighting.shader b/samples/core/shaders/lighting.shader index b706929a0e..97eed4695c 100644 --- a/samples/core/shaders/lighting.shader +++ b/samples/core/shaders/lighting.shader @@ -9,7 +9,7 @@ bgfx_shaders = { code = """ #if !defined(NO_LIGHT) - # define LIGHT_SIZE 22 // In vec4 units. + # define LIGHT_SIZE 25 // In vec4 units. # define MAX_NUM_LIGHTS 32 # define MAX_NUM_CASCADES 4 uniform vec4 u_lights_num; // num_dir, num_omni, num_spot @@ -26,6 +26,7 @@ bgfx_shaders = { # define local_lights_shadow_map_samples u_shadow_map_params[1].y SAMPLER2DSHADOW(u_cascaded_shadow_map, 10); SAMPLER2DSHADOW(u_local_lights_shadow_map, 11); + SAMPLER2D(u_lights_cookie_atlas, 13); uniform vec4 u_local_lights_params; # define local_lights_distance_culling u_local_lights_params.x # define local_lights_distance_culling_fade u_local_lights_params.y @@ -189,6 +190,21 @@ bgfx_shaders = { ; } + vec3 sample_light_cookie(vec4 rect, vec2 uv, vec4 transform, float center, float flip_y) + { + uv = (uv - center) / max(transform.x, 0.001) + center + transform.yz; + uv.y = flip_y > 0.0 ? 1.0 - uv.y : uv.y; + vec2 atlas_uv = mix(rect.xy, rect.zw, fract(uv)); + return texture2D(u_lights_cookie_atlas, atlas_uv).rgb; + } + + vec2 equirectangular_uv(vec3 direction) + { + return vec2(atan2(direction.x, direction.z) * (0.5 / M_PI) + 0.5 + , acos(clamp(direction.y, -1.0, 1.0)) / M_PI + ); + } + vec4 lights_data(int offset) { float u = (float(offset) + 0.5) / float(LIGHT_SIZE * MAX_NUM_LIGHTS); @@ -231,7 +247,7 @@ bgfx_shaders = { vec3 sun_color = vec3(1, 1, 1); if (num_dir > 0) { - // Brightest directional light (index == 0) generates cascaded shadow maps. + // Brightest directional light (index == 0) generates cascaded shadow maps and supports a cookie. vec3 light_color = lights_data(loffset + 0).rgb; float intensity = lights_data(loffset + 0).w; vec3 direction = lights_data(loffset + 2).xyz; @@ -248,6 +264,11 @@ bgfx_shaders = { float atlas_size = lights_data(loffset + 21).x; float shadow_bias = lights_data(loffset + 21).y; float cast_shadow = lights_data(loffset + 21).z; + vec4 cookie_rect = lights_data(loffset + 22); + vec4 cookie_up = lights_data(loffset + 23); + // .x is the scale (world-space size in meters of one cookie + // tile), .yz is the (x, y) offset of the cookie pattern. + vec4 cookie_transform = lights_data(loffset + 24); loffset += LIGHT_SIZE; sun_color = light_color; @@ -263,6 +284,14 @@ bgfx_shaders = { , f0 ); + if (cookie_rect.z > 0.0) { + vec3 cookie_right = cross(direction, cookie_up.xyz); + vec2 cookie_uv = vec2(dot(shadow_local.xyz, cookie_right) + , dot(shadow_local.xyz, cookie_up.xyz) + ); + local_radiance *= sample_light_cookie(cookie_rect, cookie_uv, cookie_transform, 0.0, cookie_up.w); + } + if (cast_shadow == 1.0 && receive_shadow) { vec3 shadow_world = shadow_local.xyz; vec3 atlas_offset0 = vec3(atlas_u.x , atlas_v.x , atlas_size); @@ -337,7 +366,11 @@ bgfx_shaders = { float intensity = lights_data(loffset + 0).w; vec3 position = lights_data(loffset + 1).xyz; float range = lights_data(loffset + 1).w; + vec3 direction = lights_data(loffset + 2).xyz; float cast_shadow = lights_data(loffset + 21).z; + vec4 cookie_rect = lights_data(loffset + 22); + vec4 cookie_up = lights_data(loffset + 23); + vec4 cookie_transform = lights_data(loffset + 24); vec3 light_pos = mul(position, tbn); vec3 local_radiance = calc_omni_light(n @@ -353,6 +386,17 @@ bgfx_shaders = { , f0 ); + if (cookie_rect.z > 0.0) { + vec3 to_frag = normalize(shadow_local.xyz - position); + vec3 cookie_right = cross(direction, cookie_up.xyz); + vec3 cookie_direction = vec3(dot(to_frag, cookie_right) + , dot(to_frag, cookie_up.xyz) + , dot(to_frag, direction) + ); + vec2 cookie_uv = equirectangular_uv(cookie_direction); + local_radiance *= sample_light_cookie(cookie_rect, cookie_uv, cookie_transform, 0.5, cookie_up.w); + } + if (cast_shadow == 1.0 && receive_shadow) { // Tetrahedron normals. CONST(vec3 bn) = vec3( 0.0f, 0.81649661f, -0.57735026f); @@ -444,6 +488,8 @@ bgfx_shaders = { vec3 direction = lights_data(loffset + 2).xyz; float spot_angle = lights_data(loffset + 2).w; float cast_shadow = lights_data(loffset + 21).z; + vec4 cookie_rect = lights_data(loffset + 22); + vec4 cookie_transform = lights_data(loffset + 24); vec3 local_radiance = calc_spot_light(n , v @@ -460,28 +506,37 @@ bgfx_shaders = { , f0 ); - if (cast_shadow == 1.0 && receive_shadow) { + if (cookie_rect.z > 0.0 || (cast_shadow == 1.0 && receive_shadow)) { mat4 mvp = mtxFromCols(lights_data(loffset + 3) , lights_data(loffset + 4) , lights_data(loffset + 5) , lights_data(loffset + 6) ); - vec4 atlas_u = lights_data(loffset + 19); - vec4 atlas_v = lights_data(loffset + 20); - float atlas_size = lights_data(loffset + 21).x; - float shadow_bias = lights_data(loffset + 21).y; - vec3 atlas_offset = vec3(atlas_u.x, atlas_v.x, atlas_size); - vec4 atlas_shadow_pos0 = atlas_shadow_coord(mul(mvp, shadow_local), atlas_offset); + vec4 light_clip = mul(mvp, shadow_local); - if (shadow_coord_inside_atlas_tile(atlas_shadow_pos0, atlas_offset)) { - local_radiance *= shadow(u_local_lights_shadow_map - , atlas_shadow_pos0 - , shadow_bias - , local_lights_sm_texel_size - , local_lights_shadow_map_samples - ); - } else { - local_radiance *= 0.0; + if (cookie_rect.z > 0.0) { + vec2 cookie_uv = light_clip.xy / light_clip.w; + local_radiance *= sample_light_cookie(cookie_rect, cookie_uv, cookie_transform, 0.5, 0.0); + } + + if (cast_shadow == 1.0 && receive_shadow) { + vec4 atlas_u = lights_data(loffset + 19); + vec4 atlas_v = lights_data(loffset + 20); + float atlas_size = lights_data(loffset + 21).x; + float shadow_bias = lights_data(loffset + 21).y; + vec3 atlas_offset = vec3(atlas_u.x, atlas_v.x, atlas_size); + vec4 atlas_shadow_pos0 = atlas_shadow_coord(light_clip, atlas_offset); + + if (shadow_coord_inside_atlas_tile(atlas_shadow_pos0, atlas_offset)) { + local_radiance *= shadow(u_local_lights_shadow_map + , atlas_shadow_pos0 + , shadow_bias + , local_lights_sm_texel_size + , local_lights_shadow_map_samples + ); + } else { + local_radiance *= 0.0; + } } } diff --git a/src/device/pipeline.cpp b/src/device/pipeline.cpp index d3d1c15247..5b10de4c67 100644 --- a/src/device/pipeline.cpp +++ b/src/device/pipeline.cpp @@ -3,12 +3,16 @@ * SPDX-License-Identifier: MIT */ +#include "core/memory/allocator.h" +#include "core/memory/globals.h" #include "core/strings/string_id.inl" #include "core/types.h" #include "device/pipeline.h" #include "world/shader_manager.h" #include "core/math/matrix4x4.inl" #include +#define STB_RECT_PACK_IMPLEMENTATION +#include namespace crown { @@ -178,6 +182,12 @@ Pipeline::Pipeline(ShaderManager &sm) , _sun_shadow_map_frame_buffer(BGFX_INVALID_HANDLE) , _local_lights_shadow_map_texture(BGFX_INVALID_HANDLE) , _local_lights_shadow_map_frame_buffer(BGFX_INVALID_HANDLE) + , _u_lights_cookie_atlas(BGFX_INVALID_HANDLE) + , _lights_cookie_atlas_texture(BGFX_INVALID_HANDLE) + , _lights_cookie_atlas_frame_buffer(BGFX_INVALID_HANDLE) + , _lights_cookie_atlas_supported(false) + , _lights_cookie_atlas_packer(NULL) + , _lights_cookie_atlas_packer_nodes(NULL) , _bloom_map(BGFX_INVALID_HANDLE) , _map_pixel_size(BGFX_INVALID_HANDLE) , _bloom_params(BGFX_INVALID_HANDLE) @@ -285,6 +295,39 @@ void Pipeline::create(u16 width, u16 height, const RenderSettings &render_settin _fog_data = bgfx::createUniform("u_fog_data", bgfx::UniformType::Vec4, 2); _lighting_params = bgfx::createUniform("u_lighting_params", bgfx::UniformType::Vec4); + _lights_cookie_atlas_supported = bgfx::isTextureValid(0 + , false + , 1 + , bgfx::TextureFormat::RGBA8 + , BGFX_TEXTURE_RT + ); + _u_lights_cookie_atlas = bgfx::createUniform("u_lights_cookie_atlas", bgfx::UniformType::Sampler); + if (_lights_cookie_atlas_supported) { + const u16 atlas_w = (u16)_render_settings.lights_cookie_atlas_size.x; + const u16 atlas_h = (u16)_render_settings.lights_cookie_atlas_size.y; + _lights_cookie_atlas_texture = bgfx::createTexture2D(atlas_w + , atlas_h + , false + , 1 + , bgfx::TextureFormat::RGBA8 + , BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP + ); + _lights_cookie_atlas_frame_buffer = bgfx::createFrameBuffer(1, &_lights_cookie_atlas_texture); + _lights_cookie_atlas_packer = (stbrp_context *)default_allocator().allocate(sizeof(stbrp_context)); + _lights_cookie_atlas_packer_nodes = (stbrp_node *)default_allocator().allocate(sizeof(stbrp_node) * atlas_w); + } else { + // Keep the sampler valid on renderers without RGBA8 render targets. + const u32 pixel = 0x00000000; + _lights_cookie_atlas_texture = bgfx::createTexture2D(1 + , 1 + , false + , 1 + , bgfx::TextureFormat::RGBA8 + , BGFX_TEXTURE_NONE + , bgfx::copy(&pixel, sizeof(pixel)) + ); + } + _bloom_map = bgfx::createUniform("s_bloom_map", bgfx::UniformType::Sampler); _map_pixel_size = bgfx::createUniform("u_map_pixel_size", bgfx::UniformType::Vec4); _bloom_params = bgfx::createUniform("u_bloom_params", bgfx::UniformType::Vec4); @@ -316,6 +359,21 @@ void Pipeline::destroy() bgfx::destroy(_lights_num); _lights_num = BGFX_INVALID_HANDLE; + // Destroy light cookie resources. + bgfx::destroy(_u_lights_cookie_atlas); + _u_lights_cookie_atlas = BGFX_INVALID_HANDLE; + if (bgfx::isValid(_lights_cookie_atlas_frame_buffer)) + bgfx::destroy(_lights_cookie_atlas_frame_buffer); + _lights_cookie_atlas_frame_buffer = BGFX_INVALID_HANDLE; + bgfx::destroy(_lights_cookie_atlas_texture); + _lights_cookie_atlas_texture = BGFX_INVALID_HANDLE; + if (_lights_cookie_atlas_packer != NULL) { + default_allocator().deallocate(_lights_cookie_atlas_packer); + default_allocator().deallocate(_lights_cookie_atlas_packer_nodes); + _lights_cookie_atlas_packer = NULL; + _lights_cookie_atlas_packer_nodes = NULL; + } + // Destroy local-lights shadow map resources. bgfx::destroy(_u_local_lights_params); _u_local_lights_params = BGFX_INVALID_HANDLE; @@ -619,6 +677,24 @@ void Pipeline::reset(u16 width, u16 height) } else if (id >= View::SM_LOCAL_0 && id < View::SM_LOCAL_LAST) { view_name = "sm_local_lights"; bgfx::setViewFrameBuffer(id, _local_lights_shadow_map_frame_buffer); + } else if (id == View::LOCAL_LIGHTS_COOKIE_ATLAS_CLEAR) { + view_name = "lights_cookie_atlas_clear"; + if (_lights_cookie_atlas_supported) { + bgfx::setViewFrameBuffer(id, _lights_cookie_atlas_frame_buffer); + bgfx::setViewRect(id + , 0 + , 0 + , (u16)_render_settings.lights_cookie_atlas_size.x + , (u16)_render_settings.lights_cookie_atlas_size.y + ); + bgfx::setViewClear(id, BGFX_CLEAR_COLOR, 0x00000000, 1.0f, 0); + } + } else if (id >= View::LOCAL_LIGHTS_COOKIE_ATLAS_0 && id < View::LOCAL_LIGHTS_COOKIE_ATLAS_LAST) { + view_name = "lights_cookie_atlas"; + if (_lights_cookie_atlas_supported) { + bgfx::setViewFrameBuffer(id, _lights_cookie_atlas_frame_buffer); + bgfx::setViewTransform(id, NULL, ortho); + } } else if (id == View::LIGHTS) { view_name = "lights_data"; } else if (id == View::MESH) { @@ -971,6 +1047,58 @@ void Pipeline::render(u16 width, u16 height, const Matrix4x4 &view, const Matrix bgfx::submit(View::BLIT, _blit_shader.program); } +void Pipeline::begin_light_cookie_atlas() +{ + if (!_lights_cookie_atlas_supported) + return; + + const u16 width = (u16)_render_settings.lights_cookie_atlas_size.x; + const u16 height = (u16)_render_settings.lights_cookie_atlas_size.y; + stbrp_init_target(_lights_cookie_atlas_packer + , width + , height + , _lights_cookie_atlas_packer_nodes + , width + ); + bgfx::touch(View::LOCAL_LIGHTS_COOKIE_ATLAS_CLEAR); +} + +Vector4 Pipeline::add_light_cookie(u16 &view, bgfx::TextureHandle texture, u16 width, u16 height) +{ + if (!_lights_cookie_atlas_supported) + return VECTOR4_ZERO; + + stbrp_rect packed = { 0, width, height, 0, 0, 0 }; + stbrp_pack_rects(_lights_cookie_atlas_packer, &packed, 1); + if (!packed.was_packed) + return VECTOR4_ZERO; + + CE_ASSERT(view < View::LOCAL_LIGHTS_COOKIE_ATLAS_LAST, "Too many light cookies"); + bgfx::setViewRect(view, packed.x, packed.y, width, height); + const u32 sampler_flags = BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; + bgfx::setTexture(0, _color_map, texture, sampler_flags); + screenSpaceQuad(width, height, 0.0f, bgfx::getCaps()->originBottomLeft); + bgfx::setState(_blit_shader.state); + bgfx::submit(view++, _blit_shader.program); + + const f32 atlas_w = _render_settings.lights_cookie_atlas_size.x; + const f32 atlas_h = _render_settings.lights_cookie_atlas_size.y; + const f32 min_v = bgfx::getCaps()->originBottomLeft + ? atlas_h - f32(packed.y) - f32(height) + 0.5f + : f32(packed.y) + 0.5f + ; + const f32 max_v = bgfx::getCaps()->originBottomLeft + ? atlas_h - f32(packed.y) - 0.5f + : f32(packed.y) + f32(height) - 0.5f + ; + return { + (f32(packed.x) + 0.5f) / atlas_w, + min_v / atlas_h, + (f32(packed.x) + f32(width) - 0.5f) / atlas_w, + max_v / atlas_h + }; +} + void Pipeline::reload_shaders(const ShaderResource *old_resource, const ShaderResource *new_resource) { CE_UNUSED_2(old_resource, new_resource); diff --git a/src/device/pipeline.h b/src/device/pipeline.h index 28582394c0..bbf743937c 100644 --- a/src/device/pipeline.h +++ b/src/device/pipeline.h @@ -11,7 +11,10 @@ #include "world/types.h" #include -#define LIGHT_SIZE 22 // Size of a light in vec4 units. +struct stbrp_context; +struct stbrp_node; + +#define LIGHT_SIZE 25 // Size of a light in vec4 units. #define MAX_NUM_LIGHTS 32 // Maximum number of lights per frame. #define MAX_NUM_SPRITE_LAYERS 8 #define MAX_NUM_CASCADES 4 @@ -21,6 +24,7 @@ #define LOCAL_LIGHTS_MAX_SHADOW_CASTERS 16 // Maximum number of local shadow-casting lights per frame. CE_STATIC_ASSERT(LOCAL_LIGHTS_MAX_SHADOW_CASTERS <= MAX_NUM_LIGHTS); #define LOCAL_LIGHTS_SM_MAX_VIEWS (LOCAL_LIGHTS_MAX_SHADOW_CASTERS * 4) // Worst case all omni casters. +#define LOCAL_LIGHTS_COOKIE_ATLAS_SLOT 13 #define BLOOM_MIPS 6 struct View @@ -31,17 +35,20 @@ struct View COLOR_1, CASCADE_CLEAR, CASCADE_0, - CASCADE_LAST = CASCADE_0 + MAX_NUM_CASCADES, - SM_LOCAL_CLEAR = CASCADE_LAST, + CASCADE_LAST = CASCADE_0 + MAX_NUM_CASCADES, + SM_LOCAL_CLEAR = CASCADE_LAST, SM_LOCAL_0, - SM_LOCAL_LAST = SM_LOCAL_0 + LOCAL_LIGHTS_SM_MAX_VIEWS, - LIGHTS = SM_LOCAL_LAST, + SM_LOCAL_LAST = SM_LOCAL_0 + LOCAL_LIGHTS_SM_MAX_VIEWS, + LOCAL_LIGHTS_COOKIE_ATLAS_CLEAR = SM_LOCAL_LAST, + LOCAL_LIGHTS_COOKIE_ATLAS_0, + LOCAL_LIGHTS_COOKIE_ATLAS_LAST = LOCAL_LIGHTS_COOKIE_ATLAS_0 + MAX_NUM_LIGHTS, + LIGHTS = LOCAL_LIGHTS_COOKIE_ATLAS_LAST, MESH, BLOOM_COPY, BLOOM_DOWNSAMPLE_0, - BLOOM_DOWNSAMPLE_LAST = BLOOM_DOWNSAMPLE_0 + BLOOM_MIPS - 1, + BLOOM_DOWNSAMPLE_LAST = BLOOM_DOWNSAMPLE_0 + BLOOM_MIPS - 1, BLOOM_UPSAMPLE_0, - BLOOM_UPSAMPLE_LAST = BLOOM_UPSAMPLE_0 + BLOOM_MIPS - 1, + BLOOM_UPSAMPLE_LAST = BLOOM_UPSAMPLE_0 + BLOOM_MIPS - 1, BLOOM_COMBINE, DUMMY_BLIT, VIGNETTE, @@ -116,6 +123,14 @@ struct Pipeline bgfx::UniformHandle _fog_data; bgfx::UniformHandle _lighting_params; + // Light cookies. + bgfx::UniformHandle _u_lights_cookie_atlas; + bgfx::TextureHandle _lights_cookie_atlas_texture; + bgfx::FrameBufferHandle _lights_cookie_atlas_frame_buffer; + bool _lights_cookie_atlas_supported; + stbrp_context *_lights_cookie_atlas_packer; // Re-initialized every frame. + stbrp_node *_lights_cookie_atlas_packer_nodes; // Sized once for fixed atlas width. + // Bloom. bgfx::FrameBufferHandle _bloom_frame_buffers[BLOOM_MIPS]; bgfx::UniformHandle _bloom_map; @@ -171,6 +186,13 @@ struct Pipeline /// void render(u16 width, u16 height, const Matrix4x4 &view, const Matrix4x4 &proj); + /// + void begin_light_cookie_atlas(); + + /// Packs @a texture into the atlas, renders it and returns its normalized + /// texel-center bounds. Returns zero if it does not fit. + Vector4 add_light_cookie(u16 &view, bgfx::TextureHandle texture, u16 width, u16 height); + /// void reload_shaders(const ShaderResource *old_resource, const ShaderResource *new_resource); diff --git a/src/lua/lua_api.cpp b/src/lua/lua_api.cpp index e847599f4d..8adc640a12 100644 --- a/src/lua/lua_api.cpp +++ b/src/lua/lua_api.cpp @@ -2664,6 +2664,8 @@ void load_api(LuaEnvironment &env) ld.intensity = stack.get_float(5); ld.spot_angle = stack.get_float(6); ld.color = stack.get_vector3(7); + ld.cookie = StringId64(); + ld.cookie_transform = { 1.0f, 0.0f, 0.0f }; stack.push_id(stack.get_render_world(1)->light_create(stack.get_unit(2), ld).i); return 1; @@ -2713,6 +2715,11 @@ void load_api(LuaEnvironment &env) stack.push_float(stack.get_render_world(1)->light_shadow_bias(stack.get_light_instance(2))); return 1; }); + env.add_module_function("RenderWorld", "light_cookie_transform", [](lua_State *L) { + LuaStack stack(L, +1); + stack.push_vector3(stack.get_render_world(1)->light_cookie_transform(stack.get_light_instance(2))); + return 1; + }); env.add_module_function("RenderWorld", "light_set_type", [](lua_State *L) { LuaStack stack(L); @@ -2748,6 +2755,21 @@ void load_api(LuaEnvironment &env) stack.get_render_world(1)->light_set_shadow_bias(stack.get_light_instance(2), stack.get_float(3)); return 0; }); + env.add_module_function("RenderWorld", "light_set_cookie", [](lua_State *L) { + LuaStack stack(L); + const char *cookie = stack.get_string(3); + const StringId64 cookie_id = (cookie != NULL && cookie[0] != '\0') + ? stack.get_resource_name(3) + : StringId64() + ; + stack.get_render_world(1)->light_set_cookie(stack.get_light_instance(2), cookie_id); + return 0; + }); + env.add_module_function("RenderWorld", "light_set_cookie_transform", [](lua_State *L) { + LuaStack stack(L); + stack.get_render_world(1)->light_set_cookie_transform(stack.get_light_instance(2), stack.get_vector3(3)); + return 0; + }); env.add_module_function("RenderWorld", "light_set_cast_shadows", [](lua_State *L) { LuaStack stack(L); stack.get_render_world(1)->light_set_cast_shadows(stack.get_light_instance(2), stack.get_bool(3)); diff --git a/src/resource/render_config_resource.cpp b/src/resource/render_config_resource.cpp index 0fe21d90dc..ba6309a25b 100644 --- a/src/resource/render_config_resource.cpp +++ b/src/resource/render_config_resource.cpp @@ -101,6 +101,9 @@ namespace render_settings } else if (cur->first == "local_lights_shadow_map_size") { Value v; v.type = Value::VECTOR2; v.value.v2 = RETURN_IF_ERROR(sjson::parse_vector2(cur->second)); hash_map::set(rs, cur->first.to_string_id(), v); + } else if (cur->first == "lights_cookie_atlas_size") { + Value v; v.type = Value::VECTOR2; v.value.v2 = RETURN_IF_ERROR(sjson::parse_vector2(cur->second)); + hash_map::set(rs, cur->first.to_string_id(), v); } else if (cur->first == "local_lights_shadow_map_quality") { StringId32 quality = RETURN_IF_ERROR(sjson::parse_string_id(cur->second)); Value v; v.type = Value::FLOAT; v.value.f = shadow_map_quality_samples(quality); @@ -192,6 +195,8 @@ namespace render_settings rs.local_lights_shadow_map_size = v.value.v2; rs.shadow_map_params[0].z = 1.0f / v.value.v2.x; rs.shadow_map_params[0].w = 1.0f / v.value.v2.y; + } else if (key == STRING_ID_32("lights_cookie_atlas_size", UINT32_C(0x920613bf))) { + rs.lights_cookie_atlas_size = v.value.v2; } else if (key == STRING_ID_32("local_lights_shadow_map_quality", UINT32_C(0xb6891b6e))) { rs.shadow_map_params[1].y = v.value.f; } else if (key == STRING_ID_32("local_lights", UINT32_C(0x831fd434))) { @@ -290,6 +295,7 @@ namespace render_config_resource_internal rcr.render_settings.local_lights_distance_culling_cutoff = 60.0f; rcr.render_settings.lod_fade_duration = 0.2f; rcr.render_settings.msaa_quality = msaa_quality_samples(STRING_ID_32("ultra", UINT32_C(0xf13839af))); + rcr.render_settings.lights_cookie_atlas_size = { 1024.0f, 1024.0f }; // Parse. if (json_object::has(obj, "render_settings")) { @@ -322,6 +328,7 @@ namespace render_config_resource_internal opts.write(rcr.render_settings.local_lights_distance_culling_cutoff); opts.write(rcr.render_settings.lod_fade_duration); opts.write(rcr.render_settings.msaa_quality); + opts.write(rcr.render_settings.lights_cookie_atlas_size); return 0; } diff --git a/src/resource/render_config_resource.h b/src/resource/render_config_resource.h index d0f3f68307..f8c04c8fce 100644 --- a/src/resource/render_config_resource.h +++ b/src/resource/render_config_resource.h @@ -43,6 +43,7 @@ struct RenderSettings f32 local_lights_distance_culling_cutoff; ///< Distance from camera at which local lights disappear. f32 lod_fade_duration; ///< Duration in seconds of LOD crossfades. u32 msaa_quality; + Vector2 lights_cookie_atlas_size; ///< Fixed total size of the shared light cookie atlas. }; struct RenderConfigResource diff --git a/src/resource/texture_resource.cpp b/src/resource/texture_resource.cpp index 1e2a8415cc..0da7d1dfe9 100644 --- a/src/resource/texture_resource.cpp +++ b/src/resource/texture_resource.cpp @@ -41,6 +41,8 @@ namespace texture_resource_internal tr->mem = bgfx::makeRef(data, size); tr->handle.idx = BGFX_INVALID_HANDLE; + tr->width = 0; + tr->height = 0; return tr; } @@ -48,7 +50,10 @@ namespace texture_resource_internal void online(StringId64 id, ResourceManager &rm) { TextureResource *tr = (TextureResource *)rm.get(RESOURCE_TYPE_TEXTURE, id); - tr->handle = bgfx::createTexture(tr->mem); + bgfx::TextureInfo info; + tr->handle = bgfx::createTexture(tr->mem, BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE, 0, &info); + tr->width = info.width; + tr->height = info.height; } void offline(StringId64 id, ResourceManager &rm) diff --git a/src/resource/texture_resource.h b/src/resource/texture_resource.h index 7bf8b801f5..27bb50094c 100644 --- a/src/resource/texture_resource.h +++ b/src/resource/texture_resource.h @@ -18,6 +18,8 @@ struct TextureResource { const bgfx::Memory *mem; bgfx::TextureHandle handle; + u16 width; + u16 height; }; } // namespace crown diff --git a/src/resource/types.h b/src/resource/types.h index 958bdc7411..b44dec7b62 100644 --- a/src/resource/types.h +++ b/src/resource/types.h @@ -85,7 +85,7 @@ struct Platform #define RESOURCE_VERSION_STATE_MACHINE RESOURCE_VERSION(9) #define RESOURCE_VERSION_CONFIG RESOURCE_VERSION(2) #define RESOURCE_VERSION_FONT RESOURCE_VERSION(1) -#define RESOURCE_VERSION_UNIT RESOURCE_VERSION(25) +#define RESOURCE_VERSION_UNIT RESOURCE_VERSION(26) #define RESOURCE_VERSION_LEVEL (RESOURCE_VERSION_UNIT + 6) //!< Level embeds UnitResource #define RESOURCE_VERSION_MATERIAL RESOURCE_VERSION(10) #define RESOURCE_VERSION_MESH RESOURCE_VERSION(12) @@ -93,7 +93,7 @@ struct Platform #define RESOURCE_VERSION_MESH_ANIMATION RESOURCE_VERSION(4) #define RESOURCE_VERSION_PACKAGE RESOURCE_VERSION(11) #define RESOURCE_VERSION_PHYSICS_CONFIG RESOURCE_VERSION(5) -#define RESOURCE_VERSION_RENDER_CONFIG RESOURCE_VERSION(8) +#define RESOURCE_VERSION_RENDER_CONFIG RESOURCE_VERSION(9) #define RESOURCE_VERSION_STAT_CONFIG RESOURCE_VERSION(1) #define RESOURCE_VERSION_SCRIPT RESOURCE_VERSION(4) #define RESOURCE_VERSION_SHADER RESOURCE_VERSION(18) diff --git a/src/resource/unit_compiler.cpp b/src/resource/unit_compiler.cpp index 195be7930c..cf95727cdd 100644 --- a/src/resource/unit_compiler.cpp +++ b/src/resource/unit_compiler.cpp @@ -360,6 +360,12 @@ static s32 compile_lod_group(Buffer &output, UnitCompiler &compiler, FlatJsonObj return 0; } +static f32 parse_float_or(FlatJsonObject &obj, const char *key, f32 fallback) +{ + const char *value = flat_json_object::get(obj, key); + return value != NULL ? sjson::parse_float(value) : fallback; +} + static s32 compile_light(Buffer &output, UnitCompiler &compiler, FlatJsonObject &obj, CompileOptions &opts) { CE_UNUSED(compiler); @@ -381,10 +387,7 @@ static s32 compile_light(Buffer &output, UnitCompiler &compiler, FlatJsonObject ld.intensity = RETURN_IF_ERROR(sjson::parse_float (flat_json_object::get(obj, "data.intensity"))); ld.spot_angle = RETURN_IF_ERROR(sjson::parse_float (flat_json_object::get(obj, "data.spot_angle"))); ld.color = RETURN_IF_ERROR(sjson::parse_vector3(flat_json_object::get(obj, "data.color"))); - ld.shadow_bias = 0.0004f; - if (flat_json_object::has(obj, "data.shadow_bias")) { - ld.shadow_bias = RETURN_IF_ERROR(sjson::parse_float(flat_json_object::get(obj, "data.shadow_bias"))); - } + ld.shadow_bias = RETURN_IF_ERROR(parse_float_or(obj, "data.shadow_bias", 0.0004f)); ld.flags = 0u; if (flat_json_object::has(obj, "data.cast_shadows")) { bool cast_shadows = RETURN_IF_ERROR(sjson::parse_bool(flat_json_object::get(obj, "data.cast_shadows"))); @@ -393,8 +396,30 @@ static s32 compile_light(Buffer &output, UnitCompiler &compiler, FlatJsonObject ld.flags |= RenderableFlags::SHADOW_CASTER; } + ld.cookie = StringId64(); + const char *cookie_json = flat_json_object::get(obj, "data.cookie"); + if (cookie_json != NULL) { + DynamicString cookie(ta); + RETURN_IF_ERROR(sjson::parse_string(cookie, cookie_json)); + + if (!cookie.empty()) { + WARN_IF_MISSING(UNIT_COMPILER, "texture" + , cookie.c_str() + , opts + ); + opts.add_requirement("texture", cookie.c_str()); + ld.cookie = RETURN_IF_ERROR(sjson::parse_resource_name(cookie_json)); + } + } + + ld.cookie_transform = { 1.0f, 0.0f, 0.0f }; + ld.cookie_transform.x = RETURN_IF_ERROR(parse_float_or(obj, "data.cookie_scale", 1.0f)); + ld.cookie_transform.y = RETURN_IF_ERROR(parse_float_or(obj, "data.cookie_x", 0.0f)); + ld.cookie_transform.z = RETURN_IF_ERROR(parse_float_or(obj, "data.cookie_y", 0.0f)); + FileBuffer fb(output); BinaryWriter bw(fb); + bw.write(ld.cookie); bw.write(ld.type); bw.write(ld.range); bw.write(ld.intensity); @@ -402,6 +427,7 @@ static s32 compile_light(Buffer &output, UnitCompiler &compiler, FlatJsonObject bw.write(ld.color); bw.write(ld.shadow_bias); bw.write(ld.flags); + bw.write(ld.cookie_transform); return 0; } diff --git a/src/world/render_world.cpp b/src/world/render_world.cpp index fc2ec9d57c..eec2d27d80 100644 --- a/src/world/render_world.cpp +++ b/src/world/render_world.cpp @@ -25,6 +25,7 @@ #include "resource/render_config_resource.h" #include "resource/resource_manager.h" #include "resource/sprite_resource.inl" +#include "resource/texture_resource.h" #include "world/debug_line.h" #include "world/material.h" #include "world/material_manager.h" @@ -41,6 +42,8 @@ LOG_SYSTEM(RENDER_WORLD, "render_world") namespace crown { +CE_STATIC_ASSERT(sizeof(RenderWorld::LightManager::ShaderData) == LIGHT_SIZE * sizeof(Vector4)); + // Extract bounding sphere in local space. static Sphere local_sphere(const RenderWorld::LightManager &lm, u32 i) { @@ -61,6 +64,37 @@ static Sphere local_sphere(const RenderWorld::LightManager &lm, u32 i) return s; } +static Vector3 prepare_light_cookie(Pipeline &pipeline + , ResourceManager &resource_manager + , RenderWorld::LightManager::ShaderData &shader + , StringId64 cookie + , const Matrix4x4 &world + , u16 &view + ) +{ + Vector3 light_up = y(world); + light_up -= shader.direction * dot(light_up, shader.direction); + normalize(light_up); + shader.cookie_up = { + light_up.x, + light_up.y, + light_up.z, + bgfx::getCaps()->originBottomLeft ? 1.0f : 0.0f + }; + + shader.cookie_rect = VECTOR4_ZERO; + if (cookie != StringId64()) { + const TextureResource *texture = (const TextureResource *)resource_manager.get(RESOURCE_TYPE_TEXTURE, cookie); + shader.cookie_rect = pipeline.add_light_cookie(view + , texture->handle + , texture->width + , texture->height + ); + } + + return light_up; +} + static Sphere obb_sphere(const OBB &obb) { const Vector3 scl = scale(obb.tm); @@ -1024,6 +1058,19 @@ f32 RenderWorld::light_shadow_bias(LightId light) return _light_manager._data.shader[light.i].shadow_bias; } +StringId64 RenderWorld::light_cookie(LightId light) +{ + CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); + return _light_manager._data.cookie[light.i]; +} + +Vector3 RenderWorld::light_cookie_transform(LightId light) +{ + CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); + const Vector4 &transform = _light_manager._data.shader[light.i].cookie_transform; + return { transform.x, transform.y, transform.z }; +} + void RenderWorld::light_set_color(LightId light, const Color4 &col) { CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); @@ -1072,6 +1119,18 @@ void RenderWorld::light_set_shadow_bias(LightId light, f32 bias) _light_manager._data.shader[light.i].shadow_bias = bias; } +void RenderWorld::light_set_cookie(LightId light, StringId64 cookie) +{ + CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); + _light_manager._data.cookie[light.i] = cookie; +} + +void RenderWorld::light_set_cookie_transform(LightId light, const Vector3 &transform) +{ + CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); + _light_manager._data.shader[light.i].cookie_transform = { transform.x, transform.y, transform.z, 0.0f }; +} + void RenderWorld::light_set_cast_shadows(LightId light, bool cast_shadows) { CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); @@ -1690,6 +1749,11 @@ static void draw_mesh(RenderWorld::MeshManager &mesh pipeline->set_local_lights_params_uniform(); pipeline->set_global_lighting_params(&global_lighting_desc); bgfx::setTexture(LOCAL_LIGHTS_SHADOW_MAP_SLOT, pipeline->_u_local_lights_shadow_map, pipeline->_local_lights_shadow_map_texture); + bgfx::setTexture(LOCAL_LIGHTS_COOKIE_ATLAS_SLOT + , pipeline->_u_lights_cookie_atlas + , pipeline->_lights_cookie_atlas_texture + , BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP + ); mesh.set_instance_data(object_id, *scene_graph); mesh._data.material[object_id]->bind(View::MESH); @@ -1841,6 +1905,9 @@ void RenderWorld::render(f32 dt return lm._data.shader[in_a].intensity > lm._data.shader[in_b].intensity; }); + u16 cookie_view_id = View::LOCAL_LIGHTS_COOKIE_ATLAS_0; + _pipeline->begin_light_cookie_atlas(); + // Render directional lights. for (u32 i = 0; i < array::size(lm._directional_lights) && num_lights < MAX_NUM_LIGHTS; ++i) { const u32 L = lm._directional_lights[i]; @@ -1852,6 +1919,11 @@ void RenderWorld::render(f32 dt && shadow_range_valid ; + lid.shader[L].cookie_rect = VECTOR4_ZERO; + + if (i == 0) + prepare_light_cookie(*_pipeline, *_resource_manager, lid.shader[L], lid.cookie[L], lid.world[L], cookie_view_id); + // CSMs are only computed for the brightest directional light (index = 0) in the scene. if (render_shadow) { Matrix4x4 light_proj; @@ -2056,6 +2128,14 @@ void RenderWorld::render(f32 dt && distance_squared(camera_pos, light_sphere.c) <= shadow_radius*shadow_radius ; + const Vector3 light_up = prepare_light_cookie(*_pipeline + , *_resource_manager + , shader + , lid.cookie[light_id] + , lid.world[light_id] + , cookie_view_id + ); + if (lid.type[light_id] == LightType::SPOT) { const bool cast_shadows = (lid.flag[light_id] & RenderableFlags::SHADOW_CASTER) != 0; const bool render_shadow = cast_shadows @@ -2066,13 +2146,10 @@ void RenderWorld::render(f32 dt shader.cast_shadows = f32(render_shadow); - if (render_shadow) { - cur_tile = num_tiles++; - - culling_set::cull_spheres(_cullable_shadow_casters, light_sphere, 0, array::size(_cullable_shadow_casters.id)); - culling_set::remove_culled(_cullable_shadow_casters); - - // Compute light view-proj matrix. + // Spot cookies use the same light projection as spot shadows, but + // they must not depend on a shadow map actually being rendered. + // Local shadows may be disabled, distance-culled or over budget. + if (render_shadow || shader.cookie_rect.z > 0.0f) { Matrix4x4 light_view; Matrix4x4 light_proj; const Vector3 &light_dir = shader.direction; @@ -2080,8 +2157,7 @@ void RenderWorld::render(f32 dt const f32 near = 0.1f; const Vector3 at = light_pos + light_dir; const Vector3 eye = light_pos - light_dir * near; // Move light eye backwards to compensate for non-zero near proj plane. - const Vector3 up = VECTOR3_UP; - bx::mtxLookAt(to_float_ptr(light_view), { eye.x, eye.y, eye.z }, { at.x, at.y, at.z }, { up.x, up.y, up.z }, bx::Handedness::Right); + bx::mtxLookAt(to_float_ptr(light_view), { eye.x, eye.y, eye.z }, { at.x, at.y, at.z }, { light_up.x, light_up.y, light_up.z }, bx::Handedness::Right); bx::mtxProj(to_float_ptr(light_proj) , fdeg(shader.spot_angle) * 2.0f , 1.0f // Square depth texture. @@ -2093,29 +2169,36 @@ void RenderWorld::render(f32 dt shader.mvp[0] = light_view * light_proj * crop; - Vector4 rect = { - f32(tile_size * (cur_tile % tile_cols)), - f32(tile_size * (cur_tile / tile_cols)), - f32(tile_size), - f32(tile_size) - }; - - shader.atlas_u.x = rect.x / _pipeline->_render_settings.local_lights_shadow_map_size.x; - shader.atlas_v.x = caps->originBottomLeft - ? 1.0f - ((rect.y + rect.w) / _pipeline->_render_settings.local_lights_shadow_map_size.x) - : rect.y / _pipeline->_render_settings.local_lights_shadow_map_size.x - ; - shader.map_size = rect.w / _pipeline->_render_settings.local_lights_shadow_map_size.x; - - bgfx::setViewRect(sm_local_view_id - , (u16)rect.x - , (u16)rect.y - , (u16)rect.z - , (u16)rect.w - ); - bgfx::setViewTransform(sm_local_view_id, to_float_ptr(light_view), to_float_ptr(light_proj)); - _mesh_manager.draw_shadow_casters(sm_local_view_id, *_scene_graph); - ++sm_local_view_id; + if (render_shadow) { + cur_tile = num_tiles++; + + culling_set::cull_spheres(_cullable_shadow_casters, light_sphere, 0, array::size(_cullable_shadow_casters.id)); + culling_set::remove_culled(_cullable_shadow_casters); + + Vector4 rect = { + f32(tile_size * (cur_tile % tile_cols)), + f32(tile_size * (cur_tile / tile_cols)), + f32(tile_size), + f32(tile_size) + }; + + shader.atlas_u.x = rect.x / _pipeline->_render_settings.local_lights_shadow_map_size.x; + shader.atlas_v.x = caps->originBottomLeft + ? 1.0f - ((rect.y + rect.w) / _pipeline->_render_settings.local_lights_shadow_map_size.x) + : rect.y / _pipeline->_render_settings.local_lights_shadow_map_size.x + ; + shader.map_size = rect.w / _pipeline->_render_settings.local_lights_shadow_map_size.x; + + bgfx::setViewRect(sm_local_view_id + , (u16)rect.x + , (u16)rect.y + , (u16)rect.z + , (u16)rect.w + ); + bgfx::setViewTransform(sm_local_view_id, to_float_ptr(light_view), to_float_ptr(light_proj)); + _mesh_manager.draw_shadow_casters(sm_local_view_id, *_scene_graph); + ++sm_local_view_id; + } } array::push_back(lm._local_lights_spot, light_id); @@ -3565,6 +3648,7 @@ void RenderWorld::LightManager::allocate(u32 num) + num*sizeof(f32) + alignof(f32) + num*sizeof(f32) + alignof(f32) + num*sizeof(u32) + alignof(u32) + + num*sizeof(StringId64) + alignof(StringId64) ; LightInstanceData new_data; @@ -3578,6 +3662,7 @@ void RenderWorld::LightManager::allocate(u32 num) new_data.type = (u32 *)memory::align_top(new_data.prev_flags + num, alignof(u32)); new_data.world = (Matrix4x4 *)memory::align_top(new_data.type + num, alignof(Matrix4x4)); new_data.shader = (ShaderData *)memory::align_top(new_data.world + num, alignof(ShaderData)); + new_data.cookie = (StringId64 *)memory::align_top(new_data.shader + num, alignof(StringId64)); memcpy(new_data.unit, _data.unit, _data.size * sizeof(*new_data.unit)); memcpy(new_data.flag, _data.flag, _data.size * sizeof(*new_data.flag)); @@ -3585,6 +3670,7 @@ void RenderWorld::LightManager::allocate(u32 num) memcpy(new_data.type, _data.type, _data.size * sizeof(*new_data.type)); memcpy(new_data.world, _data.world, _data.size * sizeof(*new_data.world)); memcpy(new_data.shader, _data.shader, _data.size * sizeof(ShaderData)); + memcpy(new_data.cookie, _data.cookie, _data.size * sizeof(*new_data.cookie)); _allocator->deallocate(_data.buffer); _data = new_data; @@ -3634,10 +3720,19 @@ void RenderWorld::LightManager::create_instances(const void *components_data _data.shader[last].atlas_u = VECTOR4_ZERO; _data.shader[last].atlas_v = VECTOR4_ZERO; _data.shader[last].map_size = 0.0f; + _data.shader[last].cookie_rect = VECTOR4_ZERO; + _data.shader[last].cookie_up = VECTOR4_ZERO; + _data.shader[last].cookie_transform = { + lights[i].cookie_transform.x, + lights[i].cookie_transform.y, + lights[i].cookie_transform.z, + 0.0f + }; _data.shader[last].mvp[0] = MATRIX4X4_IDENTITY; _data.shader[last].mvp[1] = MATRIX4X4_IDENTITY; _data.shader[last].mvp[2] = MATRIX4X4_IDENTITY; _data.shader[last].mvp[3] = MATRIX4X4_IDENTITY; + _data.cookie[last] = lights[i].cookie; _dirty = true; ++_data.size; @@ -3662,6 +3757,7 @@ void RenderWorld::LightManager::destroy(LightId light) _data.type[light.i] = _data.type[last]; _data.world[light.i] = _data.world[last]; _data.shader[light.i] = _data.shader[last]; + _data.cookie[light.i] = _data.cookie[last]; --_data.size; diff --git a/src/world/render_world.h b/src/world/render_world.h index c9ca8948dd..252ecc4865 100644 --- a/src/world/render_world.h +++ b/src/world/render_world.h @@ -217,6 +217,12 @@ struct RenderWorld /// Returns the shadow bias of the @a light. f32 light_shadow_bias(LightId light); + /// Returns the cookie texture resource of the @a light, or StringId64() if none. + StringId64 light_cookie(LightId light); + + /// Returns the cookie scale and local X/Y offsets of the @a light. + Vector3 light_cookie_transform(LightId light); + /// Sets the @a type of the @a light. void light_set_type(LightId light, LightType::Enum type); @@ -235,6 +241,13 @@ struct RenderWorld /// Sets the shadow @a bias of the @a light. void light_set_shadow_bias(LightId light, f32 bias); + /// Sets the @a cookie texture resource of the @a light. Use StringId64() + /// to remove any cookie currently assigned to the light. + void light_set_cookie(LightId light, StringId64 cookie); + + /// Sets the cookie @a transform: scale followed by local X/Y offsets. + void light_set_cookie_transform(LightId light, const Vector3 &transform); + /// Sets whether the @a light casts shadows. void light_set_cast_shadows(LightId light, bool cast_shadows); @@ -758,6 +771,9 @@ struct RenderWorld f32 shadow_bias; f32 cast_shadows; f32 _pad; + Vector4 cookie_rect; // 22 (min_u, min_v, max_u, max_v) texel centers of normalized light's cookie tile. + Vector4 cookie_up; // 23 World-space light up.xyz and render-target originBottomLeft in w. + Vector4 cookie_transform; // 24 Scale in x, local X/Y offsets in yz. }; struct LightInstanceData @@ -772,6 +788,7 @@ struct RenderWorld u32 *type; // LightType::Enum Matrix4x4 *world; ShaderData *shader; + StringId64 *cookie; }; Allocator *_allocator; diff --git a/src/world/types.h b/src/world/types.h index 839b531954..823e17ef6e 100644 --- a/src/world/types.h +++ b/src/world/types.h @@ -397,6 +397,7 @@ struct AnimationStateMachineDesc /// @ingroup World struct LightDesc { + StringId64 cookie; ///< Cookie texture resource, or StringId64() for none. u32 type; ///< LightType::Enum f32 range; ///< In meters. f32 intensity; ///< @@ -404,6 +405,7 @@ struct LightDesc Vector3 color; ///< Color of the light. f32 shadow_bias; ///< u32 flags; ///< RenderableFlags::Enum + Vector3 cookie_transform; ///< Scale followed by the local X/Y offsets. }; /// Global lighting description. diff --git a/tools/api/engine_api.vala b/tools/api/engine_api.vala index 24a4e2d0a3..9673801a80 100644 --- a/tools/api/engine_api.vala +++ b/tools/api/engine_api.vala @@ -439,9 +439,13 @@ namespace LevelEditorApi , Vector3 color , double shadow_bias , bool cast_shadows + , string cookie + , double cookie_scale + , double cookie_x + , double cookie_y ) { - return "LevelEditor:add_light_component(\"%s\", \"%s\", \"%s\", %.17g, %.17g, %.17g, %s, %.17g, %s)".printf(id.to_string() + return "LevelEditor:add_light_component(\"%s\", \"%s\", \"%s\", %.17g, %.17g, %.17g, %s, %.17g, %s, \"%s\", %.17g, %.17g, %.17g)".printf(id.to_string() , component_id.to_string() , type , range @@ -450,6 +454,10 @@ namespace LevelEditorApi , Lua.vector3_elements(color) , shadow_bias , Lua.bool(cast_shadows) + , cookie + , cookie_scale + , cookie_x + , cookie_y ); } @@ -581,9 +589,13 @@ namespace LevelEditorApi , Vector3 color , double shadow_bias , bool cast_shadows + , string cookie + , double cookie_scale + , double cookie_x + , double cookie_y ) { - return "LevelEditor._objects[\"%s\"]:set_light(\"%s\", %.17g, %.17g, %.17g, %s, %.17g, %s)".printf(id.to_string() + return "LevelEditor._objects[\"%s\"]:set_light(\"%s\", %.17g, %.17g, %.17g, %s, %.17g, %s, \"%s\", %.17g, %.17g, %.17g)".printf(id.to_string() , type , range , intensity @@ -591,6 +603,10 @@ namespace LevelEditorApi , Lua.vector3_elements(color) , shadow_bias , Lua.bool(cast_shadows) + , cookie + , cookie_scale + , cookie_x + , cookie_y ); } diff --git a/tools/level_editor/unit.vala b/tools/level_editor/unit.vala index fd9bf8282b..21e9b8f630 100644 --- a/tools/level_editor/unit.vala +++ b/tools/level_editor/unit.vala @@ -655,6 +655,10 @@ public struct Unit , unit.get_component_vector3(component_id, "data.color") , unit.get_component_double (component_id, "data.shadow_bias", 0.0001) , unit.get_component_bool (component_id, "data.cast_shadows", true) + , unit.get_component_resource(component_id, "data.cookie") + , unit.get_component_double (component_id, "data.cookie_scale", 1.0) + , unit.get_component_double (component_id, "data.cookie_x", 0.0) + , unit.get_component_double (component_id, "data.cookie_y", 0.0) ); sb.append(s); } else if (db.object_type(component_id) == OBJECT_TYPE_ANIMATION_STATE_MACHINE) { @@ -978,6 +982,10 @@ public struct Unit , unit.get_component_vector3(component_id, "data.color") , unit.get_component_double (component_id, "data.shadow_bias", 0.0001) , unit.get_component_bool (component_id, "data.cast_shadows", true) + , unit.get_component_resource(component_id, "data.cookie") + , unit.get_component_double (component_id, "data.cookie_scale", 1.0) + , unit.get_component_double (component_id, "data.cookie_x", 0.0) + , unit.get_component_double (component_id, "data.cookie_y", 0.0) )); } else if (component_type == OBJECT_TYPE_ANIMATION_STATE_MACHINE) { sb.append(LevelEditorApi.set_animation_state_machine(unit_id diff --git a/tools/resource/types.vala b/tools/resource/types.vala index f112b60065..74d2b04e1b 100644 --- a/tools/resource/types.vala +++ b/tools/resource/types.vala @@ -483,6 +483,37 @@ public static void create_object_types(Database database) tooltip = _("Enable shadow casting.") }, PropertyDefinition() + { + type = PropertyType.RESOURCE, + name = "data.cookie", + label = _("Cookie"), + resource_type = OBJECT_TYPE_TEXTURE, + deffault = "", + tooltip = _("Light cookie pattern."), + }, + PropertyDefinition() + { + type = PropertyType.DOUBLE, + name = "data.cookie_scale", + min = 0.001, + deffault = 1.0, + tooltip = _("World size in meters of one tile of the cookie pattern (Directional), or a zoom factor for the projected cookie (Omni, Spot)."), + }, + PropertyDefinition() + { + type = PropertyType.DOUBLE, + name = "data.cookie_x", + deffault = 0.0, + tooltip = _("Offset of the cookie pattern along X axis."), + }, + PropertyDefinition() + { + type = PropertyType.DOUBLE, + name = "data.cookie_y", + deffault = 0.0, + tooltip = _("Offset of the cookie pattern along Y axis."), + }, + PropertyDefinition() { type = PropertyType.DOUBLE, name = "spawn_order", From 2895748007d0de29f2346c73c94cd39f9dd0c0f8 Mon Sep 17 00:00:00 2001 From: olracrafter Date: Mon, 7 Sep 2026 09:56:23 +0200 Subject: [PATCH 2/2] lights: cookies: adjustments --- samples/core/editors/unit.lua | 2 +- samples/core/renderer/default.render_config | 5 ++ src/device/device.cpp | 7 ++ src/device/pipeline.cpp | 25 +++--- src/device/pipeline.h | 1 - src/lua/lua_api.cpp | 9 +-- src/resource/render_config_resource.cpp | 6 ++ src/resource/render_config_resource.h | 3 +- src/world/render_world.cpp | 86 +++++++++++++++------ src/world/render_world.h | 26 +++++-- 10 files changed, 119 insertions(+), 51 deletions(-) diff --git a/samples/core/editors/unit.lua b/samples/core/editors/unit.lua index 52ad8c6a6f..41bb4667ae 100644 --- a/samples/core/editors/unit.lua +++ b/samples/core/editors/unit.lua @@ -506,7 +506,7 @@ function UnitBox:set_light(type, range, intensity, angle, cr, cg, cb, bias, cast RenderWorld.light_set_shadow_bias(self._rw, light, bias) RenderWorld.light_set_cast_shadows(self._rw, light, cast_shadows) RenderWorld.light_set_cookie(self._rw, light, cookie) - RenderWorld.light_set_cookie_transform(self._rw, light, Vector3(cookie_scale, cookie_x, cookie_y)) + RenderWorld.light_set_cookie_scale_and_offset(self._rw, light, Vector3(cookie_scale, cookie_x, cookie_y)) end Device.set_temp_count(nv, nq, nm) end diff --git a/samples/core/renderer/default.render_config b/samples/core/renderer/default.render_config index 007dc10f21..30221ee338 100644 --- a/samples/core/renderer/default.render_config +++ b/samples/core/renderer/default.render_config @@ -32,6 +32,11 @@ render_settings = { // Whether distance culling for local lights is enabled. // local_lights_distance_culling = false + // Whether cookie textures are enabled for lights (sun and local lights alike). + // lights_cookie_enabled = true + // Fixed total size of the shared atlas that light cookies are packed into. + // lights_cookie_atlas_size = [ 1024 1024 ] + // Distance from camera at which local lights start to fade. // local_lights_distance_culling_fade = 30 // Distance from camera at which local lights disappear. diff --git a/src/device/device.cpp b/src/device/device.cpp index bb37569889..2b31c64e56 100644 --- a/src/device/device.cpp +++ b/src/device/device.cpp @@ -1057,6 +1057,13 @@ void Device::refresh(const char *json) refresh_lua = true; } else if (resource_type == RESOURCE_TYPE_TEXTURE) { _material_manager->reload_textures((TextureResource *)old_resource, (TextureResource *)new_resource); + + ListNode *cur; + list_for_each(cur, &_worlds) + { + World *w = (World *)container_of(cur, World, _node); + w->_render_world->reload_light_cookies((TextureResource *)old_resource, (TextureResource *)new_resource); + } } else if (resource_type == RESOURCE_TYPE_SHADER) { _pipeline->reload_shaders((ShaderResource *)old_resource, (ShaderResource *)new_resource); _material_manager->reload_shaders((ShaderResource *)old_resource, (ShaderResource *)new_resource); diff --git a/src/device/pipeline.cpp b/src/device/pipeline.cpp index 5b10de4c67..61c308002c 100644 --- a/src/device/pipeline.cpp +++ b/src/device/pipeline.cpp @@ -185,7 +185,6 @@ Pipeline::Pipeline(ShaderManager &sm) , _u_lights_cookie_atlas(BGFX_INVALID_HANDLE) , _lights_cookie_atlas_texture(BGFX_INVALID_HANDLE) , _lights_cookie_atlas_frame_buffer(BGFX_INVALID_HANDLE) - , _lights_cookie_atlas_supported(false) , _lights_cookie_atlas_packer(NULL) , _lights_cookie_atlas_packer_nodes(NULL) , _bloom_map(BGFX_INVALID_HANDLE) @@ -295,14 +294,14 @@ void Pipeline::create(u16 width, u16 height, const RenderSettings &render_settin _fog_data = bgfx::createUniform("u_fog_data", bgfx::UniformType::Vec4, 2); _lighting_params = bgfx::createUniform("u_lighting_params", bgfx::UniformType::Vec4); - _lights_cookie_atlas_supported = bgfx::isTextureValid(0 - , false - , 1 - , bgfx::TextureFormat::RGBA8 - , BGFX_TEXTURE_RT - ); _u_lights_cookie_atlas = bgfx::createUniform("u_lights_cookie_atlas", bgfx::UniformType::Sampler); - if (_lights_cookie_atlas_supported) { + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) != 0 + && !bgfx::isTextureValid(0, false, 1, bgfx::TextureFormat::RGBA8, BGFX_TEXTURE_RT) + ) { + // Renderer does not support RGBA8 render targets; fall back to no cookies. + _render_settings.flags &= ~RenderSettingsFlags::LIGHTS_COOKIE; + } + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) != 0) { const u16 atlas_w = (u16)_render_settings.lights_cookie_atlas_size.x; const u16 atlas_h = (u16)_render_settings.lights_cookie_atlas_size.y; _lights_cookie_atlas_texture = bgfx::createTexture2D(atlas_w @@ -316,7 +315,7 @@ void Pipeline::create(u16 width, u16 height, const RenderSettings &render_settin _lights_cookie_atlas_packer = (stbrp_context *)default_allocator().allocate(sizeof(stbrp_context)); _lights_cookie_atlas_packer_nodes = (stbrp_node *)default_allocator().allocate(sizeof(stbrp_node) * atlas_w); } else { - // Keep the sampler valid on renderers without RGBA8 render targets. + // Keep the sampler valid when light cookies are disabled or unsupported. const u32 pixel = 0x00000000; _lights_cookie_atlas_texture = bgfx::createTexture2D(1 , 1 @@ -679,7 +678,7 @@ void Pipeline::reset(u16 width, u16 height) bgfx::setViewFrameBuffer(id, _local_lights_shadow_map_frame_buffer); } else if (id == View::LOCAL_LIGHTS_COOKIE_ATLAS_CLEAR) { view_name = "lights_cookie_atlas_clear"; - if (_lights_cookie_atlas_supported) { + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) != 0) { bgfx::setViewFrameBuffer(id, _lights_cookie_atlas_frame_buffer); bgfx::setViewRect(id , 0 @@ -691,7 +690,7 @@ void Pipeline::reset(u16 width, u16 height) } } else if (id >= View::LOCAL_LIGHTS_COOKIE_ATLAS_0 && id < View::LOCAL_LIGHTS_COOKIE_ATLAS_LAST) { view_name = "lights_cookie_atlas"; - if (_lights_cookie_atlas_supported) { + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) != 0) { bgfx::setViewFrameBuffer(id, _lights_cookie_atlas_frame_buffer); bgfx::setViewTransform(id, NULL, ortho); } @@ -1049,7 +1048,7 @@ void Pipeline::render(u16 width, u16 height, const Matrix4x4 &view, const Matrix void Pipeline::begin_light_cookie_atlas() { - if (!_lights_cookie_atlas_supported) + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) == 0) return; const u16 width = (u16)_render_settings.lights_cookie_atlas_size.x; @@ -1065,7 +1064,7 @@ void Pipeline::begin_light_cookie_atlas() Vector4 Pipeline::add_light_cookie(u16 &view, bgfx::TextureHandle texture, u16 width, u16 height) { - if (!_lights_cookie_atlas_supported) + if ((_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) == 0) return VECTOR4_ZERO; stbrp_rect packed = { 0, width, height, 0, 0, 0 }; diff --git a/src/device/pipeline.h b/src/device/pipeline.h index bbf743937c..6fcdc2383b 100644 --- a/src/device/pipeline.h +++ b/src/device/pipeline.h @@ -127,7 +127,6 @@ struct Pipeline bgfx::UniformHandle _u_lights_cookie_atlas; bgfx::TextureHandle _lights_cookie_atlas_texture; bgfx::FrameBufferHandle _lights_cookie_atlas_frame_buffer; - bool _lights_cookie_atlas_supported; stbrp_context *_lights_cookie_atlas_packer; // Re-initialized every frame. stbrp_node *_lights_cookie_atlas_packer_nodes; // Sized once for fixed atlas width. diff --git a/src/lua/lua_api.cpp b/src/lua/lua_api.cpp index 8adc640a12..b5d26ce64d 100644 --- a/src/lua/lua_api.cpp +++ b/src/lua/lua_api.cpp @@ -2715,11 +2715,6 @@ void load_api(LuaEnvironment &env) stack.push_float(stack.get_render_world(1)->light_shadow_bias(stack.get_light_instance(2))); return 1; }); - env.add_module_function("RenderWorld", "light_cookie_transform", [](lua_State *L) { - LuaStack stack(L, +1); - stack.push_vector3(stack.get_render_world(1)->light_cookie_transform(stack.get_light_instance(2))); - return 1; - }); env.add_module_function("RenderWorld", "light_set_type", [](lua_State *L) { LuaStack stack(L); @@ -2765,9 +2760,9 @@ void load_api(LuaEnvironment &env) stack.get_render_world(1)->light_set_cookie(stack.get_light_instance(2), cookie_id); return 0; }); - env.add_module_function("RenderWorld", "light_set_cookie_transform", [](lua_State *L) { + env.add_module_function("RenderWorld", "light_set_cookie_scale_and_offset", [](lua_State *L) { LuaStack stack(L); - stack.get_render_world(1)->light_set_cookie_transform(stack.get_light_instance(2), stack.get_vector3(3)); + stack.get_render_world(1)->light_set_cookie_scale_and_offset(stack.get_light_instance(2), stack.get_vector3(3)); return 0; }); env.add_module_function("RenderWorld", "light_set_cast_shadows", [](lua_State *L) { diff --git a/src/resource/render_config_resource.cpp b/src/resource/render_config_resource.cpp index ba6309a25b..a324d432ce 100644 --- a/src/resource/render_config_resource.cpp +++ b/src/resource/render_config_resource.cpp @@ -104,6 +104,9 @@ namespace render_settings } else if (cur->first == "lights_cookie_atlas_size") { Value v; v.type = Value::VECTOR2; v.value.v2 = RETURN_IF_ERROR(sjson::parse_vector2(cur->second)); hash_map::set(rs, cur->first.to_string_id(), v); + } else if (cur->first == "lights_cookie_enabled") { + Value v; v.type = Value::BOOL; v.value.b = RETURN_IF_ERROR(sjson::parse_bool(cur->second)); + hash_map::set(rs, cur->first.to_string_id(), v); } else if (cur->first == "local_lights_shadow_map_quality") { StringId32 quality = RETURN_IF_ERROR(sjson::parse_string_id(cur->second)); Value v; v.type = Value::FLOAT; v.value.f = shadow_map_quality_samples(quality); @@ -197,6 +200,8 @@ namespace render_settings rs.shadow_map_params[0].w = 1.0f / v.value.v2.y; } else if (key == STRING_ID_32("lights_cookie_atlas_size", UINT32_C(0x920613bf))) { rs.lights_cookie_atlas_size = v.value.v2; + } else if (key == STRING_ID_32("lights_cookie_enabled", UINT32_C(0x9b57d075))) { + set_flag(rs.flags, RenderSettingsFlags::LIGHTS_COOKIE, v.value.b); } else if (key == STRING_ID_32("local_lights_shadow_map_quality", UINT32_C(0xb6891b6e))) { rs.shadow_map_params[1].y = v.value.f; } else if (key == STRING_ID_32("local_lights", UINT32_C(0x831fd434))) { @@ -269,6 +274,7 @@ namespace render_config_resource_internal | RenderSettingsFlags::LOCAL_LIGHTS_SHADOWS | RenderSettingsFlags::SUN_SHADOW_CONTRIBUTION_CULLING | RenderSettingsFlags::BLOOM + | RenderSettingsFlags::LIGHTS_COOKIE ; rcr.render_settings.sun_shadow_map_size = { 4096.0f, 4096.0f }; rcr.render_settings.local_lights_shadow_map_size = { 2048.0f, 2048.0f }; diff --git a/src/resource/render_config_resource.h b/src/resource/render_config_resource.h index f8c04c8fce..1439026663 100644 --- a/src/resource/render_config_resource.h +++ b/src/resource/render_config_resource.h @@ -24,7 +24,8 @@ struct RenderSettingsFlags MSAA = u32(1) << 5, ///< Whether multisample AA is enabled. OBJECT_CONTRIBUTION_CULLING = u32(1) << 6, ///< Whether contribution culling for visible objects is enabled. SUN_SHADOW_CONTRIBUTION_CULLING = u32(1) << 7, ///< Whether contribution culling for sun shadows is enabled. - SELECTION = u32(1) << 8 ///< Whether selection rendering is enabled. + SELECTION = u32(1) << 8, ///< Whether selection rendering is enabled. + LIGHTS_COOKIE = u32(1) << 9 ///< Whether cookie textures for lights are enabled. }; }; diff --git a/src/world/render_world.cpp b/src/world/render_world.cpp index eec2d27d80..05c066f39b 100644 --- a/src/world/render_world.cpp +++ b/src/world/render_world.cpp @@ -65,9 +65,8 @@ static Sphere local_sphere(const RenderWorld::LightManager &lm, u32 i) } static Vector3 prepare_light_cookie(Pipeline &pipeline - , ResourceManager &resource_manager , RenderWorld::LightManager::ShaderData &shader - , StringId64 cookie + , const RenderWorld::LightManager::CookieData &cookie , const Matrix4x4 &world , u16 &view ) @@ -83,12 +82,11 @@ static Vector3 prepare_light_cookie(Pipeline &pipeline }; shader.cookie_rect = VECTOR4_ZERO; - if (cookie != StringId64()) { - const TextureResource *texture = (const TextureResource *)resource_manager.get(RESOURCE_TYPE_TEXTURE, cookie); + if (bgfx::isValid(cookie.handle)) { shader.cookie_rect = pipeline.add_light_cookie(view - , texture->handle - , texture->width - , texture->height + , cookie.handle + , cookie.width + , cookie.height ); } @@ -1007,7 +1005,9 @@ LightId RenderWorld::light_create(UnitId unit, const LightDesc &ld) { u32 unit_index = 0; _light_manager.create_instances(&ld, 1, &unit, &unit_index); - return light_instance(unit); + LightId light = light_instance(unit); + light_set_cookie(light, ld.cookie); + return light; } void RenderWorld::light_destroy(LightId light) @@ -1064,13 +1064,6 @@ StringId64 RenderWorld::light_cookie(LightId light) return _light_manager._data.cookie[light.i]; } -Vector3 RenderWorld::light_cookie_transform(LightId light) -{ - CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); - const Vector4 &transform = _light_manager._data.shader[light.i].cookie_transform; - return { transform.x, transform.y, transform.z }; -} - void RenderWorld::light_set_color(LightId light, const Color4 &col) { CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); @@ -1123,12 +1116,25 @@ void RenderWorld::light_set_cookie(LightId light, StringId64 cookie) { CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); _light_manager._data.cookie[light.i] = cookie; + + if (cookie != StringId64()) { + const TextureResource *texture = (const TextureResource *)_resource_manager->get(RESOURCE_TYPE_TEXTURE, cookie); + _light_manager._data.cookie_data[light.i] = { texture->handle, texture->width, texture->height }; +#if CROWN_CAN_RELOAD + _light_manager._data.cookie_resource[light.i] = texture; +#endif + } else { + _light_manager._data.cookie_data[light.i] = { BGFX_INVALID_HANDLE, 0, 0 }; +#if CROWN_CAN_RELOAD + _light_manager._data.cookie_resource[light.i] = NULL; +#endif + } } -void RenderWorld::light_set_cookie_transform(LightId light, const Vector3 &transform) +void RenderWorld::light_set_cookie_scale_and_offset(LightId light, const Vector3 &scale_and_offset) { CE_ASSERT(light.i < _light_manager._data.size, "Index out of bounds"); - _light_manager._data.shader[light.i].cookie_transform = { transform.x, transform.y, transform.z, 0.0f }; + _light_manager._data.shader[light.i].cookie_transform = { scale_and_offset.x, scale_and_offset.y, scale_and_offset.z, 0.0f }; } void RenderWorld::light_set_cast_shadows(LightId light, bool cast_shadows) @@ -1905,8 +1911,11 @@ void RenderWorld::render(f32 dt return lm._data.shader[in_a].intensity > lm._data.shader[in_b].intensity; }); + const bool lights_cookie_enabled = (_pipeline->_render_settings.flags & RenderSettingsFlags::LIGHTS_COOKIE) != 0; + u16 cookie_view_id = View::LOCAL_LIGHTS_COOKIE_ATLAS_0; - _pipeline->begin_light_cookie_atlas(); + if (lights_cookie_enabled) + _pipeline->begin_light_cookie_atlas(); // Render directional lights. for (u32 i = 0; i < array::size(lm._directional_lights) && num_lights < MAX_NUM_LIGHTS; ++i) { @@ -1921,8 +1930,8 @@ void RenderWorld::render(f32 dt lid.shader[L].cookie_rect = VECTOR4_ZERO; - if (i == 0) - prepare_light_cookie(*_pipeline, *_resource_manager, lid.shader[L], lid.cookie[L], lid.world[L], cookie_view_id); + if (i == 0 && lights_cookie_enabled) + prepare_light_cookie(*_pipeline, lid.shader[L], lid.cookie_data[L], lid.world[L], cookie_view_id); // CSMs are only computed for the brightest directional light (index = 0) in the scene. if (render_shadow) { @@ -2129,9 +2138,8 @@ void RenderWorld::render(f32 dt ; const Vector3 light_up = prepare_light_cookie(*_pipeline - , *_resource_manager , shader - , lid.cookie[light_id] + , lid.cookie_data[light_id] , lid.world[light_id] , cookie_view_id ); @@ -2648,6 +2656,20 @@ void RenderWorld::reload_sprites(const SpriteResource *old_resource, const Sprit } } +void RenderWorld::reload_light_cookies(const TextureResource *old_resource, const TextureResource *new_resource) +{ +#if CROWN_CAN_RELOAD + for (u32 i = 0; i < _light_manager._data.size; ++i) { + if (_light_manager._data.cookie_resource[i] == old_resource) { + _light_manager._data.cookie_resource[i] = new_resource; + _light_manager._data.cookie_data[i] = { new_resource->handle, new_resource->width, new_resource->height }; + } + } +#else + CE_UNUSED_2(old_resource, new_resource); +#endif +} + void RenderWorld::MeshManager::allocate(u32 num) { CE_ENSURE(num > _data.size); @@ -3649,6 +3671,10 @@ void RenderWorld::LightManager::allocate(u32 num) + num*sizeof(f32) + alignof(f32) + num*sizeof(u32) + alignof(u32) + num*sizeof(StringId64) + alignof(StringId64) + + num*sizeof(CookieData) + alignof(CookieData) +#if CROWN_CAN_RELOAD + + num*sizeof(TextureResource *) + alignof(TextureResource *) +#endif ; LightInstanceData new_data; @@ -3663,6 +3689,10 @@ void RenderWorld::LightManager::allocate(u32 num) new_data.world = (Matrix4x4 *)memory::align_top(new_data.type + num, alignof(Matrix4x4)); new_data.shader = (ShaderData *)memory::align_top(new_data.world + num, alignof(ShaderData)); new_data.cookie = (StringId64 *)memory::align_top(new_data.shader + num, alignof(StringId64)); + new_data.cookie_data = (CookieData *)memory::align_top(new_data.cookie + num, alignof(CookieData)); +#if CROWN_CAN_RELOAD + new_data.cookie_resource = (const TextureResource **)memory::align_top(new_data.cookie_data + num, alignof(TextureResource *)); +#endif memcpy(new_data.unit, _data.unit, _data.size * sizeof(*new_data.unit)); memcpy(new_data.flag, _data.flag, _data.size * sizeof(*new_data.flag)); @@ -3671,6 +3701,10 @@ void RenderWorld::LightManager::allocate(u32 num) memcpy(new_data.world, _data.world, _data.size * sizeof(*new_data.world)); memcpy(new_data.shader, _data.shader, _data.size * sizeof(ShaderData)); memcpy(new_data.cookie, _data.cookie, _data.size * sizeof(*new_data.cookie)); + memcpy(new_data.cookie_data, _data.cookie_data, _data.size * sizeof(*new_data.cookie_data)); +#if CROWN_CAN_RELOAD + memcpy(new_data.cookie_resource, _data.cookie_resource, _data.size * sizeof(*new_data.cookie_resource)); +#endif _allocator->deallocate(_data.buffer); _data = new_data; @@ -3733,6 +3767,10 @@ void RenderWorld::LightManager::create_instances(const void *components_data _data.shader[last].mvp[2] = MATRIX4X4_IDENTITY; _data.shader[last].mvp[3] = MATRIX4X4_IDENTITY; _data.cookie[last] = lights[i].cookie; + _data.cookie_data[last] = { BGFX_INVALID_HANDLE, 0, 0 }; +#if CROWN_CAN_RELOAD + _data.cookie_resource[last] = NULL; +#endif _dirty = true; ++_data.size; @@ -3758,6 +3796,10 @@ void RenderWorld::LightManager::destroy(LightId light) _data.world[light.i] = _data.world[last]; _data.shader[light.i] = _data.shader[last]; _data.cookie[light.i] = _data.cookie[last]; + _data.cookie_data[light.i] = _data.cookie_data[last]; +#if CROWN_CAN_RELOAD + _data.cookie_resource[light.i] = _data.cookie_resource[last]; +#endif --_data.size; diff --git a/src/world/render_world.h b/src/world/render_world.h index 252ecc4865..0fa713d3e5 100644 --- a/src/world/render_world.h +++ b/src/world/render_world.h @@ -220,9 +220,6 @@ struct RenderWorld /// Returns the cookie texture resource of the @a light, or StringId64() if none. StringId64 light_cookie(LightId light); - /// Returns the cookie scale and local X/Y offsets of the @a light. - Vector3 light_cookie_transform(LightId light); - /// Sets the @a type of the @a light. void light_set_type(LightId light, LightType::Enum type); @@ -245,8 +242,8 @@ struct RenderWorld /// to remove any cookie currently assigned to the light. void light_set_cookie(LightId light, StringId64 cookie); - /// Sets the cookie @a transform: scale followed by local X/Y offsets. - void light_set_cookie_transform(LightId light, const Vector3 &transform); + /// Sets the cookie @a scale, followed by its local X/Y offsets. + void light_set_cookie_scale_and_offset(LightId light, const Vector3 &scale_and_offset); /// Sets whether the @a light casts shadows. void light_set_cast_shadows(LightId light, bool cast_shadows); @@ -468,6 +465,9 @@ struct RenderWorld /// void reload_sprites(const SpriteResource *old_resource, const SpriteResource *new_resource); + /// + void reload_light_cookies(const TextureResource *old_resource, const TextureResource *new_resource); + /// Callback to customize drawing of objects. typedef void (*DrawOverride)(u8 view_id, UnitId unit_id, RenderWorld *rw); @@ -776,6 +776,16 @@ struct RenderWorld Vector4 cookie_transform; // 24 Scale in x, local X/Y offsets in yz. }; + // Resolved cookie texture data, cached so the render loop never has to call + // ResourceManager::get(). Refreshed by light_set_cookie() and, in reloadable + // builds, by reload_light_cookies(). + struct CookieData + { + bgfx::TextureHandle handle; + u16 width; + u16 height; + }; + struct LightInstanceData { u32 size; @@ -788,7 +798,11 @@ struct RenderWorld u32 *type; // LightType::Enum Matrix4x4 *world; ShaderData *shader; - StringId64 *cookie; + StringId64 *cookie; // Cookie resource name, or StringId64() for none. + CookieData *cookie_data; +#if CROWN_CAN_RELOAD + const TextureResource **cookie_resource; +#endif }; Allocator *_allocator;