|
| 1 | +// Copyright 2025 DeepMind Technologies Limited |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#include "experimental/filament/compat/light_manager.h" |
| 16 | + |
| 17 | +#include <memory> |
| 18 | +#include <string> |
| 19 | +#include <utility> |
| 20 | + |
| 21 | +#include <math/mat4.h> |
| 22 | +#include <math/mathfwd.h> |
| 23 | +#include <math/vec3.h> |
| 24 | +#include <math/vec4.h> |
| 25 | +#include <mujoco/mujoco.h> |
| 26 | +#include "experimental/filament/compat/model_objects.h" |
| 27 | +#include "experimental/filament/filament_util.h" |
| 28 | +#include "experimental/filament/render_context_filament_cpp.h" |
| 29 | +#include "experimental/filament/render_context_filament.h" |
| 30 | + |
| 31 | +namespace mujoco { |
| 32 | + |
| 33 | +using filament::math::float3; |
| 34 | +using filament::math::float4; |
| 35 | +using filament::math::mat3; |
| 36 | +using filament::math::mat4; |
| 37 | + |
| 38 | +static UniquePtr<mjrTexture> CreateFallbackIndirectLightTexture( |
| 39 | + mjrfContext* ctx) { |
| 40 | + const std::string filename = ResolveFilamentAssetPath("ibl.ktx"); |
| 41 | + mjResource* resource = |
| 42 | + mju_openResource("", filename.c_str(), nullptr, nullptr, 0); |
| 43 | + if (!resource) { |
| 44 | + mju_error("Failed to open resource: %s", filename.c_str()); |
| 45 | + } |
| 46 | + const void* bytes = nullptr; |
| 47 | + const int nbytes = mju_readResource(resource, &bytes); |
| 48 | + if (bytes == nullptr || nbytes <= 0) { |
| 49 | + mju_error("Failed to read resource: %s", filename.c_str()); |
| 50 | + } |
| 51 | + |
| 52 | + mjrTextureConfig config; |
| 53 | + mjr_defaultTextureConfig(&config); |
| 54 | + config.width = 1; |
| 55 | + config.height = 1; |
| 56 | + config.sampler_type = mjTEXTURE_CUBE; |
| 57 | + config.format = mjPIXEL_FORMAT_KTX; |
| 58 | + config.color_space = mjCOLORSPACE_AUTO; |
| 59 | + |
| 60 | + auto texture = CreateTexture(ctx, config); |
| 61 | + |
| 62 | + mjrTextureData payload; |
| 63 | + mjr_defaultTextureData(&payload); |
| 64 | + payload.bytes = bytes; |
| 65 | + payload.nbytes = nbytes; |
| 66 | + payload.release_callback = +[](void* user_data) { |
| 67 | + mju_closeResource((mjResource*)user_data); |
| 68 | + }; |
| 69 | + payload.user_data = resource; |
| 70 | + |
| 71 | + mjrf_setTextureData(texture.get(), &payload); |
| 72 | + return texture; |
| 73 | +} |
| 74 | + |
| 75 | +LightManager::LightManager(mjrfContext* ctx, mjrScene* scene, |
| 76 | + ModelObjects* model_objects) |
| 77 | + : ctx_(ctx), scene_(scene) { |
| 78 | + const mjModel* model = model_objects->GetModel(); |
| 79 | + default_shadow_map_size_ = ReadElement( |
| 80 | + model, "filament.shadows.map_size", default_shadow_map_size_); |
| 81 | + default_vsm_blur_width_ = ReadElement( |
| 82 | + model, "filament.shadows.vsm_blur_width", default_vsm_blur_width_); |
| 83 | + fallback_head_light_intensity_ = |
| 84 | + ReadElement(model, "filament.fallback.head_light_intensity", |
| 85 | + fallback_head_light_intensity_); |
| 86 | + fallback_scene_light_intensity_ = |
| 87 | + ReadElement(model, "filament.fallback.scene_light_intensity", |
| 88 | + fallback_scene_light_intensity_); |
| 89 | + fallback_environment_light_intensity_ = |
| 90 | + ReadElement(model, "filament.fallback.environment_light_intensity", |
| 91 | + fallback_environment_light_intensity_); |
| 92 | + Prepare(model_objects); |
| 93 | +} |
| 94 | + |
| 95 | +LightManager::~LightManager() { |
| 96 | + for (auto& iter : lights_) { |
| 97 | + mjrf_removeLightFromScene(scene_, iter.get()); |
| 98 | + } |
| 99 | + lights_.clear(); |
| 100 | + if (fallback_ibl_) { |
| 101 | + mjrf_removeLightFromScene(scene_, fallback_ibl_.get()); |
| 102 | + } |
| 103 | + fallback_ibl_.reset(); |
| 104 | +} |
| 105 | + |
| 106 | +void LightManager::Prepare(ModelObjects* model_objects) { |
| 107 | + const mjModel* model = model_objects->GetModel(); |
| 108 | + |
| 109 | + bool has_image_based_light = false; |
| 110 | + float total_light_intensity = 0.0f; |
| 111 | + for (int i = 0; i < model->nlight; ++i) { |
| 112 | + total_light_intensity += model->light_intensity[i]; |
| 113 | + |
| 114 | + if (model->light_type[i] == mjLIGHT_IMAGE) { |
| 115 | + mjrLightParams params; |
| 116 | + mjr_defaultLightParams(¶ms); |
| 117 | + params.type = mjLIGHT_IMAGE; |
| 118 | + params.texture = model_objects->GetTexture(model->light_texid[i]); |
| 119 | + params.intensity = model->light_intensity[i]; |
| 120 | + auto light_obj = CreateLight(ctx_, params); |
| 121 | + mjrf_addLightToScene(scene_, light_obj.get()); |
| 122 | + lights_.emplace_back(std::move(light_obj)); |
| 123 | + has_image_based_light = true; |
| 124 | + } else { |
| 125 | + mjrLightParams params; |
| 126 | + mjr_defaultLightParams(¶ms); |
| 127 | + params.color[0] = model->light_diffuse[0]; |
| 128 | + params.color[1] = model->light_diffuse[1]; |
| 129 | + params.color[2] = model->light_diffuse[2]; |
| 130 | + params.type = (mjtLightType)model->light_type[i]; |
| 131 | + params.cast_shadows = model->light_castshadow[i]; |
| 132 | + params.bulb_radius = model->light_bulbradius[i]; |
| 133 | + params.range = model->light_range[i]; |
| 134 | + params.intensity = model->light_intensity[i]; |
| 135 | + params.shadow_map_size = default_shadow_map_size_; |
| 136 | + params.vsm_blur_width = default_vsm_blur_width_; |
| 137 | + if (params.type == mjLIGHT_SPOT) { |
| 138 | + params.spot_cone_angle = model->light_cutoff[i]; |
| 139 | + } |
| 140 | + |
| 141 | + auto light_obj = CreateLight(ctx_, params); |
| 142 | + mjrf_addLightToScene(scene_, light_obj.get()); |
| 143 | + lights_.emplace_back(std::move(light_obj)); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + // Add a placeholder (black) headlight as our last light. Going forward, we'll |
| 148 | + // assume lights_.back() is always the headlight. |
| 149 | + { |
| 150 | + mjrLightParams params; |
| 151 | + mjr_defaultLightParams(¶ms); |
| 152 | + // We break with the spec here slightly and use a spot light for the head |
| 153 | + // light instead of a directional params. This is because filament only |
| 154 | + // supports a single directional light, and we'd rather allow a scene |
| 155 | + // light to be that directional params. It's also a bit odd for a |
| 156 | + // directional light to move with the camera. |
| 157 | + params.type = mjLIGHT_SPOT; |
| 158 | + params.cast_shadows = 0; |
| 159 | + params.intensity = 0.0f; |
| 160 | + params.spot_cone_angle = 90.0f; |
| 161 | + auto light_obj = CreateLight(ctx_, params); |
| 162 | + mjrf_addLightToScene(scene_, light_obj.get()); |
| 163 | + lights_.emplace_back(std::move(light_obj)); |
| 164 | + } |
| 165 | + |
| 166 | + if (!has_image_based_light && total_light_intensity > 0.0f) { |
| 167 | + // Create a black indirect light to ensure that the skybox is |
| 168 | + // oriented to respect mujoco's Z-up convention. |
| 169 | + mjrLightParams params; |
| 170 | + mjr_defaultLightParams(¶ms); |
| 171 | + params.type = mjLIGHT_IMAGE; |
| 172 | + params.intensity = 10.0f; |
| 173 | + fallback_ibl_ = CreateLight(ctx_, params); |
| 174 | + mjrf_addLightToScene(scene_, fallback_ibl_.get()); |
| 175 | + } |
| 176 | + |
| 177 | + // There are no "physical" lights in the scene which means we're likely |
| 178 | + // dealing with a "classic renderer" scene. In this case, let's add a |
| 179 | + // default environment light and set the light intensity ourselves. |
| 180 | + if (total_light_intensity == 0.0f) { |
| 181 | + // Create a fallback environment light. |
| 182 | + fallback_ibl_texture_ = CreateFallbackIndirectLightTexture(ctx_); |
| 183 | + |
| 184 | + mjrLightParams params; |
| 185 | + mjr_defaultLightParams(¶ms); |
| 186 | + params.type = mjLIGHT_IMAGE; |
| 187 | + params.texture = fallback_ibl_texture_.get(); |
| 188 | + params.intensity = fallback_environment_light_intensity_; |
| 189 | + fallback_ibl_ = CreateLight(ctx_, params); |
| 190 | + mjrf_addLightToScene(scene_, fallback_ibl_.get()); |
| 191 | + |
| 192 | + // Distribute the fallback scene light intensity among the lights. |
| 193 | + const float intensity = fallback_scene_light_intensity_ / lights_.size(); |
| 194 | + for (auto& light : lights_) { |
| 195 | + if (light) { |
| 196 | + const bool is_headlight = (light == lights_.back()); |
| 197 | + mjrf_setLightIntensity(light.get(), |
| 198 | + is_headlight ? fallback_head_light_intensity_ |
| 199 | + : intensity); |
| 200 | + } |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + mjrf_setSceneSkybox(scene_, model_objects->GetSkyboxTexture()); |
| 205 | +} |
| 206 | + |
| 207 | +mjrLight* LightManager::GetLight(int index) { |
| 208 | + if (index < 0 || index >= lights_.size()) { |
| 209 | + return nullptr; |
| 210 | + } |
| 211 | + return lights_[index].get(); |
| 212 | +} |
| 213 | +} // namespace mujoco |
0 commit comments