// // KRLight.cpp // Kraken Engine // // Copyright 2025 Kearwood Gilbert. All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other materials // provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY KEARWOOD GILBERT ''AS IS'' AND ANY EXPRESS OR IMPLIED // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KEARWOOD GILBERT OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // The views and conclusions contained in the software and documentation are those of the // authors and should not be interpreted as representing official policies, either expressed // or implied, of Kearwood Gilbert. // #include "KREngine-common.h" #include "KRLight.h" #include "KRNode.h" #include "KRCamera.h" #include "KRContext.h" #include "KRPipelineManager.h" #include "KRPipeline.h" #include "KRDirectionalLight.h" #include "KRSpotLight.h" #include "KRPointLight.h" #include "KRRenderPass.h" using namespace hydra; /* static */ void KRLight::InitNodeInfo(KrNodeInfo* nodeInfo) { KRNode::InitNodeInfo(nodeInfo); nodeInfo->light.casts_shadow = true; nodeInfo->light.color = Vector3::One(); nodeInfo->light.decay_start = 0.0f; nodeInfo->light.dust_particle_density = 0.1f; nodeInfo->light.dust_particle_intensity = 1.0f; nodeInfo->light.dust_particle_size = 1.0f; nodeInfo->light.flare_occlusion_size = 0.05f; nodeInfo->light.flare_size = 0.0f; nodeInfo->light.flare_texture = -1; nodeInfo->light.intensity = 1.0f; nodeInfo->light.light_shafts = true; } KRLight::KRLight(KRScene& scene, std::string name) : KRNode(scene, name) , m_flareTexture(KRTextureBinding(KRTexture::TEXTURE_USAGE_LIGHT_FLARE)) { m_occlusionQuery = 0; // Initialize shadow buffers m_cShadowBuffers = 0; for (int iBuffer = 0; iBuffer < KRENGINE_MAX_SHADOW_BUFFERS; iBuffer++) { shadowFramebuffer[iBuffer] = 0; shadowDepthTexture[iBuffer] = 0; shadowValid[iBuffer] = false; } } KRLight::~KRLight() { if (m_occlusionQuery) { GLDEBUG(glDeleteQueriesEXT(1, &m_occlusionQuery)); m_occlusionQuery = 0; } allocateShadowBuffers(0); } tinyxml2::XMLElement* KRLight::saveXML(tinyxml2::XMLNode* parent) { tinyxml2::XMLElement* e = KRNode::saveXML(parent); m_intensity.save(e); m_color.save(e); m_decayStart.save(e); m_flareSize.save(e); m_flareOcclusionSize.save(e); m_casts_shadow.save(e); m_light_shafts.save(e); m_dust_particle_density.save(e); m_dust_particle_size.save(e); m_dust_particle_intensity.save(e); e->SetAttribute("flare_texture", m_flareTexture.getName().c_str()); return e; } void KRLight::loadXML(tinyxml2::XMLElement* e) { KRNode::loadXML(e); m_color.load(e); m_intensity.load(e); m_decayStart.load(e); m_flareSize.load(e); m_flareOcclusionSize.load(e); m_casts_shadow.load(e); m_light_shafts.load(e); m_dust_particle_density.load(e); m_dust_particle_size.load(e); m_dust_particle_intensity.load(e); const char* szFlareTexture = e->Attribute("flare_texture"); if (szFlareTexture) { m_flareTexture.set(szFlareTexture); } else { m_flareTexture.clear(); } } void KRLight::setFlareTexture(std::string flare_texture) { m_flareTexture.set(flare_texture); } void KRLight::setFlareSize(float flare_size) { m_flareSize = flare_size; } void KRLight::setFlareOcclusionSize(float occlusion_size) { m_flareOcclusionSize = occlusion_size; } void KRLight::setIntensity(float intensity) { m_intensity = intensity; } float KRLight::getIntensity() const { return m_intensity; } const Vector3& KRLight::getColor() { return m_color; } void KRLight::setColor(const Vector3& color) { m_color = color; } void KRLight::setDecayStart(float decayStart) { m_decayStart = decayStart; } float KRLight::getDecayStart() const { return m_decayStart; } void KRLight::getResourceBindings(std::list& bindings) { KRNode::getResourceBindings(bindings); bindings.push_back(&m_flareTexture); } void KRLight::render(RenderInfo& ri) { KRNode::render(ri); if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_SHADOWMAP && (ri.camera->settings.volumetric_environment_enable || ri.camera->settings.dust_particle_enable || (ri.camera->settings.m_cShadowBuffers > 0 && m_casts_shadow))) { allocateShadowBuffers(configureShadowBufferViewports(*ri.viewport)); renderShadowBuffers(ri); } if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_ADDITIVE_PARTICLES && ri.camera->settings.dust_particle_enable) { // Render brownian particles for dust floating in air if (m_cShadowBuffers >= 1 && shadowValid[0] && m_dust_particle_density > 0.0f && m_dust_particle_size > 0.0f && m_dust_particle_intensity > 0.0f) { if (ri.viewport->visible(getBounds()) || true) { // FINDME, HACK need to remove "|| true"? float particle_range = 600.0f; int particle_count = (int)(m_dust_particle_density * pow(particle_range, 3)); if (particle_count > KRMeshManager::KRENGINE_MAX_RANDOM_PARTICLES) { particle_count = KRMeshManager::KRENGINE_MAX_RANDOM_PARTICLES; } Matrix4 particleModelMatrix; particleModelMatrix.scale(particle_range); // Scale the box symetrically to ensure that we don't have an uneven distribution of particles for different angles of the view frustrum particleModelMatrix.translate(ri.viewport->getCameraPosition()); std::vector this_directional_light; std::vector this_spot_light; std::vector this_point_light; KRDirectionalLight* directional_light = dynamic_cast(this); KRSpotLight* spot_light = dynamic_cast(this); KRPointLight* point_light = dynamic_cast(this); if (directional_light) { this_directional_light.push_back(directional_light); } if (spot_light) { this_spot_light.push_back(spot_light); } if (point_light) { this_point_light.push_back(point_light); } PipelineInfo info{}; std::string shader_name("dust_particle"); info.shader_name = &shader_name; info.pCamera = ri.camera; info.point_lights = &this_point_light; info.directional_lights = &this_directional_light; info.spot_lights = &this_spot_light; info.renderPass = ri.renderPass; info.rasterMode = RasterMode::kAdditive; info.cullMode = CullMode::kCullNone; info.vertexAttributes = (1 << KRMesh::KRENGINE_ATTRIB_VERTEX) | (1 << KRMesh::KRENGINE_ATTRIB_TEXUVA); info.modelFormat = ModelFormat::KRENGINE_MODEL_FORMAT_TRIANGLES; KRPipeline* pParticleShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info); pParticleShader->setPushConstant(ShaderValue::dust_particle_color, m_color.val * ri.camera->settings.dust_particle_intensity * m_dust_particle_intensity * m_intensity); pParticleShader->setPushConstant(ShaderValue::particle_origin, Matrix4::DotWDiv(Matrix4::Invert(particleModelMatrix), Vector3::Zero())); pParticleShader->bind(ri, particleModelMatrix); // TODO: Pass light index to shader m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &m_pContext->getMeshManager()->KRENGINE_VBO_DATA_RANDOM_PARTICLES, 1.0f); vkCmdDraw(ri.commandBuffer, particle_count * 3, 1, 0, 0); } } } if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_VOLUMETRIC_EFFECTS_ADDITIVE && ri.camera->settings.volumetric_environment_enable && m_light_shafts) { std::string shader_name = ri.camera->settings.volumetric_environment_downsample != 0 ? "volumetric_fog_downsampled" : "volumetric_fog"; std::vector this_directional_light; std::vector this_spot_light; std::vector this_point_light; KRDirectionalLight* directional_light = dynamic_cast(this); KRSpotLight* spot_light = dynamic_cast(this); KRPointLight* point_light = dynamic_cast(this); if (directional_light) { this_directional_light.push_back(directional_light); } if (spot_light) { this_spot_light.push_back(spot_light); } if (point_light) { this_point_light.push_back(point_light); } PipelineInfo info{}; info.shader_name = &shader_name; info.pCamera = ri.camera; info.point_lights = &this_point_light; info.directional_lights = &this_directional_light; info.spot_lights = &this_spot_light; info.renderPass = ri.renderPass; info.rasterMode = RasterMode::kAdditive; info.cullMode = CullMode::kCullNone; info.vertexAttributes = (1 << KRMesh::KRENGINE_ATTRIB_VERTEX); info.modelFormat = ModelFormat::KRENGINE_MODEL_FORMAT_TRIANGLES; KRPipeline* pFogShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info); int slice_count = (int)(ri.camera->settings.volumetric_environment_quality * 495.0) + 5; float slice_near = -ri.camera->settings.getPerspectiveNearZ(); float slice_far = -ri.camera->settings.volumetric_environment_max_distance; float slice_spacing = (slice_far - slice_near) / slice_count; pFogShader->setPushConstant(ShaderValue::slice_depth_scale, Vector2::Create(slice_near, slice_spacing)); pFogShader->setPushConstant(ShaderValue::light_color, (m_color.val * ri.camera->settings.volumetric_environment_intensity * m_intensity * -slice_spacing / 10.0f)); pFogShader->bind(ri, Matrix4()); // TODO: Pass indexes of lights to shader m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &m_pContext->getMeshManager()->KRENGINE_VBO_DATA_VOLUMETRIC_LIGHTING, 1.0f); vkCmdDraw(ri.commandBuffer, slice_count * 6, 1, 0, 0); } if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_PARTICLE_OCCLUSION) { if (m_flareTexture.isBound() && m_flareSize > 0.0f) { KRMesh* sphereModel = getContext().getMeshManager()->getMesh("__sphere"); if (sphereModel) { Matrix4 occlusion_test_sphere_matrix = Matrix4(); occlusion_test_sphere_matrix.scale(m_localScale * m_flareOcclusionSize); occlusion_test_sphere_matrix.translate(m_localTranslation); if (m_parentNode) { occlusion_test_sphere_matrix *= m_parentNode->getModelMatrix(); } PipelineInfo info{}; std::string shader_name("occlusion_test"); info.shader_name = &shader_name; info.pCamera = ri.camera; info.point_lights = &ri.point_lights; info.directional_lights = &ri.directional_lights; info.spot_lights = &ri.spot_lights; info.renderPass = ri.renderPass; info.rasterMode = RasterMode::kAdditive; info.cullMode = CullMode::kCullNone; info.modelFormat = sphereModel->getModelFormat(); info.vertexAttributes = sphereModel->getVertexAttributes(); KRPipeline* pPipeline = getContext().getPipelineManager()->getPipeline(*ri.surface, info); pPipeline->bind(ri, occlusion_test_sphere_matrix); GLDEBUG(glGenQueriesEXT(1, &m_occlusionQuery)); #if TARGET_OS_IPHONE || defined(ANDROID) GLDEBUG(glBeginQueryEXT(GL_ANY_SAMPLES_PASSED_EXT, m_occlusionQuery)); #else GLDEBUG(glBeginQuery(GL_SAMPLES_PASSED, m_occlusionQuery)); #endif sphereModel->renderNoMaterials(ri.commandBuffer, ri.renderPass, getName(), "occlusion_test", 1.0f); #if TARGET_OS_IPHONE || defined(ANDROID) GLDEBUG(glEndQueryEXT(GL_ANY_SAMPLES_PASSED_EXT)); #else GLDEBUG(glEndQuery(GL_SAMPLES_PASSED)); #endif } } } if (ri.renderPass->getType() == RenderPassType::RENDER_PASS_ADDITIVE_PARTICLES) { if (m_flareTexture.isBound() && m_flareSize > 0.0f) { if (m_occlusionQuery) { int params = 0; GLDEBUG(glGetQueryObjectuivEXT(m_occlusionQuery, GL_QUERY_RESULT_EXT, ¶ms)); GLDEBUG(glDeleteQueriesEXT(1, &m_occlusionQuery)); if (params) { KRMeshManager::KRVBOData& vertices = getContext().getMeshManager()->KRENGINE_VBO_DATA_2D_SQUARE_VERTICES; // Render light flare on transparency pass PipelineInfo info{}; std::string shader_name("flare"); info.shader_name = &shader_name; info.pCamera = ri.camera; info.point_lights = &ri.point_lights; info.directional_lights = &ri.directional_lights; info.spot_lights = &ri.spot_lights; info.renderPass = ri.renderPass; info.rasterMode = RasterMode::kAdditiveNoTest; info.cullMode = CullMode::kCullNone; info.vertexAttributes = vertices.getVertexAttributes(); info.modelFormat = ModelFormat::KRENGINE_MODEL_FORMAT_STRIP; KRPipeline* pShader = getContext().getPipelineManager()->getPipeline(*ri.surface, info); pShader->setPushConstant(ShaderValue::material_alpha, 1.0f); pShader->setImageBinding("diffuseTexture", m_flareTexture.get(), getContext().getSamplerManager()->DEFAULT_CLAMPED_SAMPLER); pShader->bind(ri, getModelMatrix()); m_pContext->getMeshManager()->bindVBO(ri.commandBuffer, &vertices, 1.0f); vkCmdDraw(ri.commandBuffer, 4, 1, 0, 0); } } } } } void KRLight::allocateShadowBuffers(int cBuffers) { // First deallocate buffers no longer needed for (int iShadow = cBuffers; iShadow < KRENGINE_MAX_SHADOW_BUFFERS; iShadow++) { if (shadowDepthTexture[iShadow]) { GLDEBUG(glDeleteTextures(1, shadowDepthTexture + iShadow)); shadowDepthTexture[iShadow] = 0; } if (shadowFramebuffer[iShadow]) { GLDEBUG(glDeleteFramebuffers(1, shadowFramebuffer + iShadow)); shadowFramebuffer[iShadow] = 0; } } // Allocate newly required buffers for (int iShadow = 0; iShadow < cBuffers; iShadow++) { Vector2 viewportSize = m_shadowViewports[iShadow].getSize(); if (!shadowDepthTexture[iShadow]) { shadowValid[iShadow] = false; GLDEBUG(glGenFramebuffers(1, shadowFramebuffer + iShadow)); GLDEBUG(glGenTextures(1, shadowDepthTexture + iShadow)); // ===== Create offscreen shadow framebuffer object ===== GLDEBUG(glBindFramebuffer(GL_FRAMEBUFFER, shadowFramebuffer[iShadow])); // ----- Create Depth Texture for shadowFramebuffer ----- // TODO - Vulkan Refactoring. Note: shadowDepthTexture Sampler needs clamp-to-edge and linear filtering GLDEBUG(glBindTexture(GL_TEXTURE_2D, shadowDepthTexture[iShadow])); GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); // m_pContext->getTextureManager()->_setWrapModeS(shadowDepthTexture[iShadow], GL_CLAMP_TO_EDGE); // m_pContext->getTextureManager()->_setWrapModeT(shadowDepthTexture[iShadow], GL_CLAMP_TO_EDGE); #if GL_EXT_shadow_samplers GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_EXT, GL_COMPARE_REF_TO_TEXTURE_EXT)); // TODO - Detect GL_EXT_shadow_samplers and only activate if available GLDEBUG(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_EXT, GL_LEQUAL)); // TODO - Detect GL_EXT_shadow_samplers and only activate if available #endif GLDEBUG(glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, (int)viewportSize.x, (int)viewportSize.y, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL)); GLDEBUG(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTexture[iShadow], 0)); } } m_cShadowBuffers = cBuffers; } void KRLight::deleteBuffers() { // Called when this light wasn't used in the last frame, so we can free the resources for use by other lights allocateShadowBuffers(0); } void KRLight::invalidateShadowBuffers() { for (int iShadow = 0; iShadow < m_cShadowBuffers; iShadow++) { shadowValid[iShadow] = false; } } int KRLight::configureShadowBufferViewports(const KRViewport& viewport) { return 0; } void KRLight::renderShadowBuffers(RenderInfo& ri) { KRViewport* prevViewport = ri.viewport; for (int iShadow = 0; iShadow < m_cShadowBuffers; iShadow++) { if (!shadowValid[iShadow]) { shadowValid[iShadow] = true; GLDEBUG(glBindFramebuffer(GL_FRAMEBUFFER, shadowFramebuffer[iShadow])); GLDEBUG(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTexture[iShadow], 0)); GLDEBUG(glViewport(0, 0, (int)m_shadowViewports[iShadow].getSize().x, (int)m_shadowViewports[iShadow].getSize().y)); GLDEBUG(glClearDepthf(0.0f)); GLDEBUG(glClear(GL_DEPTH_BUFFER_BIT)); GLDEBUG(glViewport(1, 1, (int)m_shadowViewports[iShadow].getSize().x - 2, (int)m_shadowViewports[iShadow].getSize().y - 2)); GLDEBUG(glClearDepthf(1.0f)); GLDEBUG(glClear(GL_DEPTH_BUFFER_BIT)); GLDEBUG(glDisable(GL_DITHER)); // Use shader program PipelineInfo info{}; std::string shader_name("ShadowShader"); info.shader_name = &shader_name; info.pCamera = ri.camera; info.renderPass = ri.renderPass; info.rasterMode = RasterMode::kOpaqueLessTest; // TODO - This is sub-optimal. Evaluate increasing depth buffer resolution instead of disabling depth test. info.cullMode = CullMode::kCullNone; // Disabling culling, which eliminates some self-cast shadow artifacts KRPipeline* shadowShader = m_pContext->getPipelineManager()->getPipeline(*ri.surface, info); ri.viewport = &m_shadowViewports[iShadow]; shadowShader->bind(ri, Matrix4()); m_shadowViewports[iShadow].expireOcclusionResults(m_pContext->getCurrentFrame()); getScene().render(ri); } } ri.viewport = prevViewport; } int KRLight::getShadowBufferCount() { int cBuffers = 0; for (int iBuffer = 0; iBuffer < m_cShadowBuffers; iBuffer++) { if (shadowValid[iBuffer]) { cBuffers++; } else { break; } } return cBuffers; } int* KRLight::getShadowTextures() { return shadowDepthTexture; } KRViewport* KRLight::getShadowViewports() { return m_shadowViewports; } bool KRLight::getShaderValue(ShaderValue value, float* output) const { switch (value) { case ShaderValue::light_intensity: *output = m_intensity; return true; case ShaderValue::light_decay_start: *output = getDecayStart(); return true; case ShaderValue::light_cutoff: *output = KRLIGHT_MIN_INFLUENCE; return true; case ShaderValue::flare_size: *output = m_flareSize; return true; case ShaderValue::dust_particle_size: *output = m_dust_particle_size; return true; } return KRNode::getShaderValue(value, output); } bool KRLight::getShaderValue(ShaderValue value, hydra::Vector3* output) const { switch (value) { case ShaderValue::light_position: *output = m_localTranslation; return true; case ShaderValue::light_color: *output = m_color; return true; } return KRNode::getShaderValue(value, output); }