Add Filament lighting and shadow features and fixes.
- Add a hidden directional light to Filament scenes with no lights to work around soft shadow blackout bug. - Respect quality/shadowsize as default shadow map resolution. - Interpret light/bulbradius in correct units for each shadow type and update XML reference docs. - Support updating the shadow map resolution of live Filament lights. - Clamp light intensities to non-negative values in the Filament debug panel. PiperOrigin-RevId: 957305808 Change-Id: Ic7a3430aaa3b117be7fbb7ace836d68e54cdf65a
This commit is contained in:
committed by
Copybara-Service
parent
0ca53dacf8
commit
7bc1aa9b05
@@ -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>`__
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -187,6 +187,19 @@ bool Ui(std::string_view label, T* value, UiOpts<T> opts = {}) {
|
||||
static_assert(dependent_false<T>::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<T> && !std::is_same_v<T, bool>) {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]});
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
|
||||
#include "render/filament/support/model_lights.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <math/TVecHelpers.h>
|
||||
#include <math/mat4.h>
|
||||
#include <math/mathfwd.h>
|
||||
#include <math/vec3.h>
|
||||
@@ -74,8 +77,7 @@ static UniquePtr<mjrfTexture> 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.
|
||||
|
||||
@@ -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<mjrfLight> fallback_ibl_{nullptr, nullptr};
|
||||
UniquePtr<mjrfLight> fallback_directional_{nullptr, nullptr};
|
||||
UniquePtr<mjrfTexture> fallback_ibl_texture_{nullptr, nullptr};
|
||||
std::vector<UniquePtr<mjrfLight>> 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;
|
||||
|
||||
Reference in New Issue
Block a user