diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index bc379660..7ef7d097 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -3429,6 +3429,15 @@ Enables or disables the light. Sets the intensity of the light, in candela. +.. _mjrf_setLightShadowMapSize: + +`mjrf_setLightShadowMapSize <#mjrf_setLightShadowMapSize>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjrf_setLightShadowMapSize + +Sets the resolution of the light's shadow map, in texels. + .. _mjrf_setLightColor: `mjrf_setLightColor <#mjrf_setLightColor>`__ diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 350c5586..8decce54 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3301,8 +3301,8 @@ Attributes may be applied or ignored depending on the lighting model being used. .. _body-light-bulbradius: :at:`bulbradius`: :at-val:`real, "0.02"` - The radius of the light source which can affect shadow softness depending on the - renderer. This only applies to spotlights. + The radius of the light-emitting surface. Larger radii produce softer shadows in renderers that + support soft shadows. Ignored by the classic renderer. .. _body-light-attenuation: diff --git a/doc/includes/references.h b/doc/includes/references.h index f5771569..8ed1b8cf 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3442,6 +3442,7 @@ mjrfLight* mjrf_createLight(mjrfContext* ctx, const mjrfLightParams* params); void mjrf_destroyLight(mjrfLight* light); void mjrf_setLightEnabled(mjrfLight* light, mjtBool enabled); void mjrf_setLightIntensity(mjrfLight* light, float intensity); +void mjrf_setLightShadowMapSize(mjrfLight* light, int map_size); void mjrf_setLightColor(mjrfLight* light, const float color[3]); void mjrf_setLightTransform(mjrfLight* light, const float position[3], const float direction[3]); int mjrf_getLightType(const mjrfLight* light); diff --git a/include/mujoco/mjrfilament.h b/include/mujoco/mjrfilament.h index ed4be639..87e53aa1 100644 --- a/include/mujoco/mjrfilament.h +++ b/include/mujoco/mjrfilament.h @@ -281,6 +281,9 @@ void mjrf_setLightEnabled(mjrfLight* light, mjtBool enabled); // Sets the intensity of the light, in candela. void mjrf_setLightIntensity(mjrfLight* light, float intensity); +// Sets the resolution of the light's shadow map, in texels. +void mjrf_setLightShadowMapSize(mjrfLight* light, int map_size); + // Sets the RGB color of the light. void mjrf_setLightColor(mjrfLight* light, const float color[3]); diff --git a/src/experimental/filament/compat/scene_bridge.cc b/src/experimental/filament/compat/scene_bridge.cc index 6d728479..55c1fea2 100644 --- a/src/experimental/filament/compat/scene_bridge.cc +++ b/src/experimental/filament/compat/scene_bridge.cc @@ -100,6 +100,8 @@ mat4 CalculateClipFromWorld(const mjrRect& viewport, const mjrCamera& cam) { void SceneBridge::Update(const mjrRect& viewport, const mjvScene* scene) { const mjModel* model = model_objects_->GetModel(); + model_lights_->Update(); + mjtNum hpos[3], hfwd[3]; float headpos[3], gazedir[3]; mjv_cameraInModel(hpos, hfwd, nullptr, scene); diff --git a/src/render/filament/core/imgui_editor.cc b/src/render/filament/core/imgui_editor.cc index a3548f03..88937d74 100644 --- a/src/render/filament/core/imgui_editor.cc +++ b/src/render/filament/core/imgui_editor.cc @@ -187,6 +187,19 @@ bool Ui(std::string_view label, T* value, UiOpts opts = {}) { static_assert(dependent_false::value, "Unsupported type"); } + // The Drag widgets above do not enforce bounds when only one of min/max is + // given, and typed input (ctrl+click) bypasses Slider bounds; clamp here. + if constexpr (std::is_arithmetic_v && !std::is_same_v) { + if (changed) { + if (opts.min.has_value()) { + *value = std::max(*value, *opts.min); + } + if (opts.max.has_value()) { + *value = std::min(*value, *opts.max); + } + } + } + if (modified) { ImGui::PopStyleColor(); ImGui::SetItemTooltip("RMB to revert change"); @@ -565,7 +578,7 @@ void DrawIndirectLightGui(SceneView* scene_view) { float intensity = 10000.0f; if (ibl) { intensity = ibl->getIntensity(); - if (Ui("Intensity", &intensity, {.step = 1000.0f, .fstep = 10000.0f})) { + if (Ui("Intensity", &intensity, {.min = 0.0f, .step = 1000.0f, .fstep = 10000.0f})) { ibl->setIntensity(intensity); } } @@ -586,7 +599,7 @@ void DrawLightGui(filament::LightManager& lm, lm.setDirection(li, direction); } float intensity = lm.getIntensity(li); - if (Ui("Intensity", &intensity, {.step = 1000.0f, .fstep = 10000.0f})) { + if (Ui("Intensity", &intensity, {.min = 0.0f, .step = 1000.0f, .fstep = 10000.0f})) { lm.setIntensityCandela(li, intensity); } float falloff = lm.getFalloff(li); diff --git a/src/render/filament/core/light.cc b/src/render/filament/core/light.cc index 489e766f..96af80c3 100644 --- a/src/render/filament/core/light.cc +++ b/src/render/filament/core/light.cc @@ -165,6 +165,18 @@ void Light::SetIntensity(float intensity) { } } +void Light::SetShadowMapSize(int map_size) { + params_.shadow_map_size = map_size; + if (ibl_) { + return; + } + filament::LightManager& lm = engine_->getLightManager(); + const filament::LightManager::Instance li = lm.getInstance(entity_); + filament::LightManager::ShadowOptions opts = lm.getShadowOptions(li); + opts.mapSize = map_size; + lm.setShadowOptions(li, opts); +} + void Light::Enable() { if (!enabled_) { enabled_ = true; diff --git a/src/render/filament/core/light.h b/src/render/filament/core/light.h index acc885e7..d0ee756a 100644 --- a/src/render/filament/core/light.h +++ b/src/render/filament/core/light.h @@ -51,6 +51,9 @@ class Light : public mjrfLight { // Sets the intensity of this light, in candela. void SetIntensity(float intensity); + // Sets the resolution of this light's shadow map, in texels. + void SetShadowMapSize(int map_size); + // Returns the type of the light. mjtLightType GetType() const; diff --git a/src/render/filament/mjrfilament.cc b/src/render/filament/mjrfilament.cc index 7635abcb..1be7e708 100644 --- a/src/render/filament/mjrfilament.cc +++ b/src/render/filament/mjrfilament.cc @@ -234,6 +234,10 @@ void mjrf_setLightIntensity(mjrfLight* light, float intensity) { mujoco::Light::downcast(light)->SetIntensity(intensity); } +void mjrf_setLightShadowMapSize(mjrfLight* light, int map_size) { + mujoco::Light::downcast(light)->SetShadowMapSize(map_size); +} + void mjrf_setLightColor(mjrfLight* light, const float color[3]) { mujoco::Light::downcast(light)->SetColor({color[0], color[1], color[2]}); } diff --git a/src/render/filament/support/model_lights.cc b/src/render/filament/support/model_lights.cc index 65c1d044..d264d342 100644 --- a/src/render/filament/support/model_lights.cc +++ b/src/render/filament/support/model_lights.cc @@ -14,10 +14,13 @@ #include "render/filament/support/model_lights.h" +#include #include +#include #include #include +#include #include #include #include @@ -74,8 +77,7 @@ static UniquePtr CreateFallbackIndirectLightTexture( ModelLights::ModelLights(mjrfScene* scene, ModelObjects* model_objects) : scene_(scene), model_objects_(model_objects) { const mjModel* model = model_objects->GetModel(); - default_shadow_map_size_ = - ReadElement(model, "filament.shadows.map_size", default_shadow_map_size_); + shadowsize_ = model->vis.quality.shadowsize; fallback_head_light_intensity_ = ReadElement(model, "filament.fallback.head_light_intensity", fallback_head_light_intensity_); @@ -97,16 +99,28 @@ ModelLights::~ModelLights() { mjrf_removeLightFromScene(scene_, fallback_ibl_.get()); } fallback_ibl_.reset(); + if (fallback_directional_) { + mjrf_removeLightFromScene(scene_, fallback_directional_.get()); + } + fallback_directional_.reset(); } void ModelLights::Prepare() { mjrfContext* ctx = model_objects_->GetContext(); const mjModel* model = model_objects_->GetModel(); + // Default to the shadow map resolution in mjVisual, like the classic + // renderer. Filament clamps shadow map sizes to 2048. + int default_shadow_map_size = std::min(model->vis.quality.shadowsize, 2048); + default_shadow_map_size = + ReadElement(model, "filament.shadows.map_size", default_shadow_map_size); + bool has_image_based_light = false; + bool has_directional_light = false; float total_light_intensity = 0.0f; for (int i = 0; i < model->nlight; ++i) { total_light_intensity += model->light_intensity[i]; + has_directional_light |= model->light_type[i] == mjLIGHT_DIRECTIONAL; if (model->light_type[i] == mjLIGHT_IMAGE) { mjrfLightParams params; @@ -126,13 +140,42 @@ void ModelLights::Prepare() { params.color[2] = model->light_diffuse[2]; params.type = (mjtLightType)model->light_type[i]; params.cast_shadows = model->light_castshadow[i]; - // The bulb_radius is only used by DPCF or PCSS shadows. For VSM shadows, - // we use light_bulbradius to control the blur width. - params.bulb_radius = model->light_bulbradius[i]; - params.vsm_blur_width = model->light_bulbradius[i]; + // light_bulbradius is the radius of the emitting surface in meters. + // Filament's DPCF/PCSS internally scale shadowBulbRadius from meters + // to light-space texels, so we pass it through in meters for all types. + // VSM requires a single uniform blur width for the map; we approximate + // this using the angular size of the bulb from the scene center. + const float bulb_radius = model->light_bulbradius[i]; + const float map_size = default_shadow_map_size; + const float3 to_center = ReadFloat3(model->stat.center) - + ReadFloat3(model->light_pos0, i); + const float distance = std::max(length(to_center), 1e-6f); + const float bulb_angle = bulb_radius / distance; + params.bulb_radius = bulb_radius; + switch (params.type) { + case mjLIGHT_SPOT: { + const float fov = + 2.0f * model->light_cutoff[i] * std::numbers::pi / 180.0f; + params.vsm_blur_width = bulb_angle * map_size / fov; + break; + } + case mjLIGHT_POINT: + params.vsm_blur_width = + bulb_angle * map_size / (0.5f * std::numbers::pi); + break; + case mjLIGHT_DIRECTIONAL: { + const float coverage = + 2.0f * model->vis.map.shadowclip * model->stat.extent; + params.vsm_blur_width = bulb_radius * map_size / coverage; + break; + } + default: + break; + } + params.vsm_blur_width = std::min(params.vsm_blur_width, 125.0f); params.range = model->light_range[i]; params.intensity = model->light_intensity[i]; - params.shadow_map_size = default_shadow_map_size_; + params.shadow_map_size = default_shadow_map_size; if (params.type == mjLIGHT_SPOT) { params.spot_cone_angle = model->light_cutoff[i]; } @@ -162,6 +205,23 @@ void ModelLights::Prepare() { lights_.emplace_back(std::move(light_obj)); } + // Workaround for an upstream filament bug, present since 1.74.0 + // (https://github.com/google/filament/issues/10249): under the non-PCF + // shadow types (VSM/DPCF/PCSS), a scene where a punctual (spot/point) light + // casts shadows renders fully black unless a directional light is also + // present. Mere presence suffices: zero intensity, shadows off. Keep + // filament's directional slot occupied with an invisible light whenever the + // model has no directional light. + if (!has_directional_light) { + mjrfLightParams params; + mjrf_defaultLightParams(¶ms); + params.type = mjLIGHT_DIRECTIONAL; + params.cast_shadows = 0; + params.intensity = 0.0f; + fallback_directional_ = CreateLight(ctx, params); + mjrf_addLightToScene(scene_, fallback_directional_.get()); + } + if (!has_image_based_light && total_light_intensity > 0.0f) { // Create a black indirect light to ensure that the skybox is // oriented to respect mujoco's Z-up convention. @@ -203,7 +263,22 @@ void ModelLights::Prepare() { mjrf_setSceneSkybox(scene_, model_objects_->GetSkyboxTexture()); } +void ModelLights::UpdateShadowMapSize() { + const mjModel* model = model_objects_->GetModel(); + if (model->vis.quality.shadowsize != shadowsize_) { + shadowsize_ = model->vis.quality.shadowsize; + const int map_size = std::min(shadowsize_, 2048); + for (auto& light : lights_) { + mjrf_setLightShadowMapSize(light.get(), map_size); + } + } +} + void ModelLights::Update(const mjData* data) { + UpdateShadowMapSize(); + if (data == nullptr) { + return; + } const mjModel* model = model_objects_->GetModel(); for (int i = 0; i <= model->nlight; ++i) { // Light with index nlight is the headlight. diff --git a/src/render/filament/support/model_lights.h b/src/render/filament/support/model_lights.h index cd5a6676..e90755a4 100644 --- a/src/render/filament/support/model_lights.h +++ b/src/render/filament/support/model_lights.h @@ -31,7 +31,7 @@ class ModelLights { ~ModelLights(); // Updates the state of the lights in the scene. - void Update(const mjData* data); + void Update(const mjData* data = nullptr); // Returns the light with the given index in the mjModel. Note that an extra // headlight is assigned of the index `nlight`. @@ -43,13 +43,19 @@ class ModelLights { private: void Prepare(); + // Applies changes of mjVisual quality.shadowsize to the lights' shadow + // maps. A filament.shadows.map_size custom element remains in effect until + // shadowsize is edited. + void UpdateShadowMapSize(); + mjrfScene* scene_ = nullptr; ModelObjects* model_objects_ = nullptr; UniquePtr fallback_ibl_{nullptr, nullptr}; + UniquePtr fallback_directional_{nullptr, nullptr}; UniquePtr fallback_ibl_texture_{nullptr, nullptr}; std::vector> lights_; - int default_shadow_map_size_ = 2048; + int shadowsize_ = 0; float fallback_head_light_intensity_ = 0.f; float fallback_scene_light_intensity_ = 80'000.f; float fallback_environment_light_intensity_ = 5'000.f;