Move ModelObjects and LightManager to filament/support.
PiperOrigin-RevId: 931136854 Change-Id: I3ce3b7265c5d3c9a49fa84be46e87a76c6e2c8b5
This commit is contained in:
committed by
Copybara-Service
parent
1ae561407a
commit
01d5be61e3
@@ -55,6 +55,10 @@ target_sources(${MUJOCO_FILAMENT_TARGET_NAME}
|
||||
core/texture.h
|
||||
support/filament_util.h
|
||||
support/filament_util.cc
|
||||
support/light_manager.h
|
||||
support/light_manager.cc
|
||||
support/model_objects.h
|
||||
support/model_objects.cc
|
||||
)
|
||||
|
||||
target_include_directories(${MUJOCO_FILAMENT_TARGET_NAME}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2025 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "render/filament/support/light_manager.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <math/mat4.h>
|
||||
#include <math/mathfwd.h>
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "render/filament/mjrfilament_cpp.h"
|
||||
#include "render/filament/mjrfilament.h"
|
||||
#include "render/filament/support/filament_util.h"
|
||||
#include "render/filament/support/model_objects.h"
|
||||
|
||||
namespace mujoco {
|
||||
|
||||
using filament::math::float3;
|
||||
using filament::math::float4;
|
||||
using filament::math::mat3;
|
||||
using filament::math::mat4;
|
||||
|
||||
static UniquePtr<mjrfTexture> CreateFallbackIndirectLightTexture(
|
||||
mjrfContext* ctx) {
|
||||
const std::string filename = ResolveFilamentAssetPath("ibl.ktx");
|
||||
mjResource* resource =
|
||||
mju_openResource("", filename.c_str(), nullptr, nullptr, 0);
|
||||
if (!resource) {
|
||||
mju_error("Failed to open resource: %s", filename.c_str());
|
||||
}
|
||||
const void* bytes = nullptr;
|
||||
const int nbytes = mju_readResource(resource, &bytes);
|
||||
if (bytes == nullptr || nbytes <= 0) {
|
||||
mju_error("Failed to read resource: %s", filename.c_str());
|
||||
}
|
||||
|
||||
mjrfTextureConfig config;
|
||||
mjrf_defaultTextureConfig(&config);
|
||||
config.width = 1;
|
||||
config.height = 1;
|
||||
config.sampler_type = mjTEXTURE_CUBE;
|
||||
config.format = mjPIXEL_FORMAT_KTX;
|
||||
config.color_space = mjCOLORSPACE_AUTO;
|
||||
|
||||
auto texture = CreateTexture(ctx, config);
|
||||
|
||||
mjrfTextureData payload;
|
||||
mjrf_defaultTextureData(&payload);
|
||||
payload.bytes = bytes;
|
||||
payload.nbytes = nbytes;
|
||||
payload.release = +[](void* user_data) {
|
||||
mju_closeResource((mjResource*)user_data);
|
||||
};
|
||||
payload.user_data = resource;
|
||||
|
||||
mjrf_setTextureData(texture.get(), &payload);
|
||||
return texture;
|
||||
}
|
||||
|
||||
LightManager::LightManager(mjrfContext* ctx, mjrfScene* scene,
|
||||
ModelObjects* model_objects)
|
||||
: ctx_(ctx), scene_(scene) {
|
||||
const mjModel* model = model_objects->GetModel();
|
||||
default_shadow_map_size_ = ReadElement(
|
||||
model, "filament.shadows.map_size", default_shadow_map_size_);
|
||||
default_vsm_blur_width_ = ReadElement(
|
||||
model, "filament.shadows.vsm_blur_width", default_vsm_blur_width_);
|
||||
fallback_head_light_intensity_ =
|
||||
ReadElement(model, "filament.fallback.head_light_intensity",
|
||||
fallback_head_light_intensity_);
|
||||
fallback_scene_light_intensity_ =
|
||||
ReadElement(model, "filament.fallback.scene_light_intensity",
|
||||
fallback_scene_light_intensity_);
|
||||
fallback_environment_light_intensity_ =
|
||||
ReadElement(model, "filament.fallback.environment_light_intensity",
|
||||
fallback_environment_light_intensity_);
|
||||
Prepare(model_objects);
|
||||
}
|
||||
|
||||
LightManager::~LightManager() {
|
||||
for (auto& iter : lights_) {
|
||||
mjrf_removeLightFromScene(scene_, iter.get());
|
||||
}
|
||||
lights_.clear();
|
||||
if (fallback_ibl_) {
|
||||
mjrf_removeLightFromScene(scene_, fallback_ibl_.get());
|
||||
}
|
||||
fallback_ibl_.reset();
|
||||
}
|
||||
|
||||
void LightManager::Prepare(ModelObjects* model_objects) {
|
||||
const mjModel* model = model_objects->GetModel();
|
||||
|
||||
bool has_image_based_light = false;
|
||||
float total_light_intensity = 0.0f;
|
||||
for (int i = 0; i < model->nlight; ++i) {
|
||||
total_light_intensity += model->light_intensity[i];
|
||||
|
||||
if (model->light_type[i] == mjLIGHT_IMAGE) {
|
||||
mjrfLightParams params;
|
||||
mjrf_defaultLightParams(¶ms);
|
||||
params.type = mjLIGHT_IMAGE;
|
||||
params.texture = model_objects->GetTexture(model->light_texid[i]);
|
||||
params.intensity = model->light_intensity[i];
|
||||
auto light_obj = CreateLight(ctx_, params);
|
||||
mjrf_addLightToScene(scene_, light_obj.get());
|
||||
lights_.emplace_back(std::move(light_obj));
|
||||
has_image_based_light = true;
|
||||
} else {
|
||||
mjrfLightParams params;
|
||||
mjrf_defaultLightParams(¶ms);
|
||||
params.color[0] = model->light_diffuse[0];
|
||||
params.color[1] = model->light_diffuse[1];
|
||||
params.color[2] = model->light_diffuse[2];
|
||||
params.type = (mjtLightType)model->light_type[i];
|
||||
params.cast_shadows = model->light_castshadow[i];
|
||||
params.bulb_radius = model->light_bulbradius[i];
|
||||
params.range = model->light_range[i];
|
||||
params.intensity = model->light_intensity[i];
|
||||
params.shadow_map_size = default_shadow_map_size_;
|
||||
params.vsm_blur_width = default_vsm_blur_width_;
|
||||
if (params.type == mjLIGHT_SPOT) {
|
||||
params.spot_cone_angle = model->light_cutoff[i];
|
||||
}
|
||||
|
||||
auto light_obj = CreateLight(ctx_, params);
|
||||
mjrf_addLightToScene(scene_, light_obj.get());
|
||||
lights_.emplace_back(std::move(light_obj));
|
||||
}
|
||||
}
|
||||
|
||||
// Add a placeholder (black) headlight as our last light. Going forward, we'll
|
||||
// assume lights_.back() is always the headlight.
|
||||
{
|
||||
mjrfLightParams params;
|
||||
mjrf_defaultLightParams(¶ms);
|
||||
// We break with the spec here slightly and use a spot light for the head
|
||||
// light instead of a directional params. This is because filament only
|
||||
// supports a single directional light, and we'd rather allow a scene
|
||||
// light to be that directional params. It's also a bit odd for a
|
||||
// directional light to move with the camera.
|
||||
params.type = mjLIGHT_SPOT;
|
||||
params.cast_shadows = 0;
|
||||
params.intensity = 0.0f;
|
||||
params.spot_cone_angle = 90.0f;
|
||||
auto light_obj = CreateLight(ctx_, params);
|
||||
mjrf_addLightToScene(scene_, light_obj.get());
|
||||
lights_.emplace_back(std::move(light_obj));
|
||||
}
|
||||
|
||||
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.
|
||||
mjrfLightParams params;
|
||||
mjrf_defaultLightParams(¶ms);
|
||||
params.type = mjLIGHT_IMAGE;
|
||||
params.intensity = 10.0f;
|
||||
fallback_ibl_ = CreateLight(ctx_, params);
|
||||
mjrf_addLightToScene(scene_, fallback_ibl_.get());
|
||||
}
|
||||
|
||||
// There are no "physical" lights in the scene which means we're likely
|
||||
// dealing with a "classic renderer" scene. In this case, let's add a
|
||||
// default environment light and set the light intensity ourselves.
|
||||
if (total_light_intensity == 0.0f) {
|
||||
// Create a fallback environment light.
|
||||
fallback_ibl_texture_ = CreateFallbackIndirectLightTexture(ctx_);
|
||||
|
||||
mjrfLightParams params;
|
||||
mjrf_defaultLightParams(¶ms);
|
||||
params.type = mjLIGHT_IMAGE;
|
||||
params.texture = fallback_ibl_texture_.get();
|
||||
params.intensity = fallback_environment_light_intensity_;
|
||||
fallback_ibl_ = CreateLight(ctx_, params);
|
||||
mjrf_addLightToScene(scene_, fallback_ibl_.get());
|
||||
|
||||
// Distribute the fallback scene light intensity among the lights.
|
||||
const float intensity = fallback_scene_light_intensity_ / lights_.size();
|
||||
for (auto& light : lights_) {
|
||||
if (light) {
|
||||
const bool is_headlight = (light == lights_.back());
|
||||
mjrf_setLightIntensity(light.get(),
|
||||
is_headlight ? fallback_head_light_intensity_
|
||||
: intensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mjrf_setSceneSkybox(scene_, model_objects->GetSkyboxTexture());
|
||||
}
|
||||
|
||||
mjrfLight* LightManager::GetLight(int index) {
|
||||
if (index < 0 || index >= lights_.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return lights_[index].get();
|
||||
}
|
||||
} // namespace mujoco
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2025 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_LIGHT_MANAGER_H_
|
||||
#define MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_LIGHT_MANAGER_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "render/filament/mjrfilament.h"
|
||||
#include "render/filament/mjrfilament_cpp.h"
|
||||
#include "render/filament/support/model_objects.h"
|
||||
|
||||
namespace mujoco {
|
||||
|
||||
// Manages Light entities for an mjrfScene.
|
||||
class LightManager {
|
||||
public:
|
||||
LightManager(mjrfContext* ctx, mjrfScene* scene, ModelObjects* model_objects);
|
||||
~LightManager();
|
||||
|
||||
// Returns the light with the given index in the mjModel. Note that an extra
|
||||
// headlight is assigned of the index `nlight`.
|
||||
mjrfLight* GetLight(int index);
|
||||
|
||||
LightManager(const LightManager&) = delete;
|
||||
LightManager& operator=(const LightManager&) = delete;
|
||||
|
||||
private:
|
||||
void Prepare(ModelObjects* model_objects);
|
||||
|
||||
mjrfContext* ctx_ = nullptr;
|
||||
mjrfScene* scene_ = nullptr;
|
||||
UniquePtr<mjrfLight> fallback_ibl_{nullptr, nullptr};
|
||||
UniquePtr<mjrfTexture> fallback_ibl_texture_{nullptr, nullptr};
|
||||
std::vector<UniquePtr<mjrfLight>> lights_;
|
||||
int default_shadow_map_size_ = 2048;
|
||||
float default_vsm_blur_width_ = 0.0f;
|
||||
float fallback_head_light_intensity_ = 0.f;
|
||||
float fallback_scene_light_intensity_ = 80'000.f;
|
||||
float fallback_environment_light_intensity_ = 5'000.f;
|
||||
};
|
||||
|
||||
} // namespace mujoco
|
||||
|
||||
#endif // MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_LIGHT_MANAGER_H_
|
||||
@@ -0,0 +1,532 @@
|
||||
// Copyright 2026 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "render/filament/support/model_objects.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <math/TVecHelpers.h>
|
||||
#include <math/vec2.h>
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "render/filament/mjrfilament.h"
|
||||
#include "render/filament/mjrfilament_cpp.h"
|
||||
#include "render/filament/support/filament_util.h"
|
||||
|
||||
namespace mujoco {
|
||||
|
||||
using filament::math::float2;
|
||||
using filament::math::float3;
|
||||
using filament::math::float4;
|
||||
|
||||
enum class MeshType {
|
||||
kNormal,
|
||||
kConvexHull,
|
||||
kHeightField,
|
||||
};
|
||||
|
||||
struct MeshBuilder {
|
||||
MeshBuilder(int nvertices) : nvertices(nvertices) {
|
||||
positions.reserve(nvertices);
|
||||
orientations.reserve(nvertices);
|
||||
uvs.reserve(nvertices);
|
||||
}
|
||||
|
||||
void Append(const float3& position, const float4& orientation,
|
||||
const float2& uv) {
|
||||
positions.push_back(position);
|
||||
orientations.push_back(orientation);
|
||||
uvs.push_back(uv);
|
||||
bounds_min = min(bounds_min, position);
|
||||
bounds_max = max(bounds_max, position);
|
||||
}
|
||||
|
||||
int nvertices = 0;
|
||||
float3 bounds_min = {FLT_MAX, FLT_MAX, FLT_MAX};
|
||||
float3 bounds_max = {-FLT_MAX, -FLT_MAX, -FLT_MAX};
|
||||
std::vector<float3> positions;
|
||||
std::vector<float4> orientations;
|
||||
std::vector<float2> uvs;
|
||||
};
|
||||
|
||||
static bool UseFaceNormal(const float3& face_normal,
|
||||
const float3& mesh_normal) {
|
||||
// clang-format off
|
||||
return (face_normal[0] * mesh_normal[0] +
|
||||
face_normal[1] * mesh_normal[1] +
|
||||
face_normal[2] * mesh_normal[2]) < 0.8f;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
static void FillConvexHullBuffer(MeshBuilder& builder, const mjModel* model,
|
||||
int meshid) {
|
||||
const int numvert = model->mesh_graph[model->mesh_graphadr[meshid]];
|
||||
const int numface = model->mesh_graph[model->mesh_graphadr[meshid] + 1];
|
||||
if (builder.nvertices != numface * 3) {
|
||||
mju_error("Invalid vertex count (%d vs %d).", builder.nvertices, numface * 3);
|
||||
return;
|
||||
}
|
||||
|
||||
const int dataadr = model->mesh_graphadr[meshid] + 2;
|
||||
const int vertadr = model->mesh_vertadr[meshid];
|
||||
const float* vertices = model->mesh_vert + (3 * vertadr);
|
||||
const int texcoordadr = model->mesh_texcoordadr[meshid];
|
||||
const float* texcoords = texcoordadr >= 0 ? model->mesh_texcoord + (2 * texcoordadr) : nullptr;
|
||||
|
||||
for (int face = 0; face < numface; ++face) {
|
||||
const int j = dataadr + (3 * numvert) + (3 * numface) + (3 * face);
|
||||
const float3 p1 = ReadFloat3(vertices, model->mesh_graph[j + 0]);
|
||||
const float3 p2 = ReadFloat3(vertices, model->mesh_graph[j + 1]);
|
||||
const float3 p3 = ReadFloat3(vertices, model->mesh_graph[j + 2]);
|
||||
const float4 orientation = CalculateOrientation(p1, p2, p3);
|
||||
const float2 uv1 = texcoords ? ReadFloat2(texcoords, model->mesh_graph[j + 0]) : float2(0, 0);
|
||||
const float2 uv2 = texcoords ? ReadFloat2(texcoords, model->mesh_graph[j + 1]) : float2(0, 0);
|
||||
const float2 uv3 = texcoords ? ReadFloat2(texcoords, model->mesh_graph[j + 2]) : float2(0, 0);
|
||||
builder.Append(p1, orientation, uv1);
|
||||
builder.Append(p2, orientation, uv2);
|
||||
builder.Append(p3, orientation, uv3);
|
||||
}
|
||||
}
|
||||
|
||||
static void FillMeshBuffer(MeshBuilder& builder, const mjModel* model, int meshid) {
|
||||
const int faceadr = model->mesh_faceadr[meshid];
|
||||
const int facenum = model->mesh_facenum[meshid];
|
||||
if (builder.nvertices != facenum * 3) {
|
||||
mju_error("Invalid vertex count (%d vs %d).", builder.nvertices, facenum * 3);
|
||||
return;
|
||||
}
|
||||
|
||||
const int vertadr = model->mesh_vertadr[meshid];
|
||||
const float* vertices = model->mesh_vert + (3 * vertadr);
|
||||
const int normaladr = model->mesh_normaladr[meshid];
|
||||
const float* normals = model->mesh_normal + 3 * normaladr;
|
||||
const int texcoordadr = model->mesh_texcoordadr[meshid];
|
||||
const float* texcoords = texcoordadr >= 0 ? model->mesh_texcoord + (2 * texcoordadr) : nullptr;
|
||||
|
||||
for (int i = 0; i < facenum; ++i) {
|
||||
const int face = 3 * (faceadr + i);
|
||||
|
||||
const float3 p1 = ReadFloat3(vertices, model->mesh_face[face + 0]);
|
||||
const float3 p2 = ReadFloat3(vertices, model->mesh_face[face + 1]);
|
||||
const float3 p3 = ReadFloat3(vertices, model->mesh_face[face + 2]);
|
||||
const float3 face_normal = CalculateNormal(p1, p2, p3);
|
||||
const float3 n1 = ReadFloat3(normals, model->mesh_facenormal[face + 0]);
|
||||
const float3 n2 = ReadFloat3(normals, model->mesh_facenormal[face + 1]);
|
||||
const float3 n3 = ReadFloat3(normals, model->mesh_facenormal[face + 2]);
|
||||
const float2 uv1 = texcoords ? ReadFloat2(texcoords, model->mesh_facetexcoord[face + 0]) : float2(0, 0);
|
||||
const float2 uv2 = texcoords ? ReadFloat2(texcoords, model->mesh_facetexcoord[face + 1]) : float2(0, 0);
|
||||
const float2 uv3 = texcoords ? ReadFloat2(texcoords, model->mesh_facetexcoord[face + 2]) : float2(0, 0);
|
||||
|
||||
if (UseFaceNormal(face_normal, n1)) {
|
||||
builder.Append(p1, CalculateOrientation(face_normal), uv1);
|
||||
} else {
|
||||
builder.Append(p1, CalculateOrientation(n1), uv1);
|
||||
}
|
||||
|
||||
if (UseFaceNormal(face_normal, n2)) {
|
||||
builder.Append(p2, CalculateOrientation(face_normal), uv2);
|
||||
} else {
|
||||
builder.Append(p2, CalculateOrientation(n2), uv2);
|
||||
}
|
||||
|
||||
if (UseFaceNormal(face_normal, n3)) {
|
||||
builder.Append(p3, CalculateOrientation(face_normal), uv3);
|
||||
} else {
|
||||
builder.Append(p3, CalculateOrientation(n3), uv3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
|
||||
int hfieldid) {
|
||||
auto append_tri = [&](float3 a, float3 b, float3 c) {
|
||||
float4 orientation = CalculateOrientation(a, b, c);
|
||||
builder.Append(a, orientation, float2(0, 0));
|
||||
builder.Append(b, orientation, float2(0, 0));
|
||||
builder.Append(c, orientation, float2(0, 0));
|
||||
};
|
||||
auto append_quad = [&](float3 a, float3 b, float3 c, float3 d) {
|
||||
append_tri(a, b, d);
|
||||
append_tri(d, b, c);
|
||||
};
|
||||
|
||||
const float* data = model->hfield_data + model->hfield_adr[hfieldid];
|
||||
const int nrow = model->hfield_nrow[hfieldid];
|
||||
const int ncol = model->hfield_ncol[hfieldid];
|
||||
const float height = 0.5f * (nrow - 1);
|
||||
const float width = 0.5f * (ncol - 1);
|
||||
float sz[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
sz[i] = static_cast<float>(model->hfield_size[4 * hfieldid + i]);
|
||||
}
|
||||
|
||||
auto get_pos = [=](int r, int c) {
|
||||
const float x = sz[0] * (c / width - 1.0f);
|
||||
const float y = sz[1] * (r / height - 1.0f);
|
||||
const float z = sz[2] * data[(r * ncol) + c];
|
||||
return float3{x, y, z};
|
||||
};
|
||||
|
||||
// For each quad defined by 4 points in the height field, we will create 4
|
||||
// triangles by introducing a vertex in the middle of the quad.
|
||||
// a---b
|
||||
// |\ /|
|
||||
// | m |
|
||||
// |/ \|
|
||||
// d---c
|
||||
for (int row = 0; row < nrow - 1; ++row) {
|
||||
for (int col = 0; col < ncol - 1; ++col) {
|
||||
const float3 a = get_pos(row, col);
|
||||
const float3 b = get_pos(row, col + 1);
|
||||
const float3 c = get_pos(row + 1, col + 1);
|
||||
const float3 d = get_pos(row + 1, col);
|
||||
|
||||
const float mid_x = (a.x + b.x) * 0.5f;
|
||||
const float mid_y = (a.y + d.y) * 0.5f;
|
||||
|
||||
// To determine the height of the middle vertex, we look at the heights
|
||||
// of the opposing corners (i.e. {a, c} and {b, d}). Our goal is to avoid
|
||||
// creating any odd bumps or valleys in the height field if possible.
|
||||
//
|
||||
// If one of the two opposing corners are of the same height, then we
|
||||
// set the middle vertex such that we're effectively rendering two
|
||||
// triangles, preventing an odd bump. Otherwise, we use the higher
|
||||
// midpoint between two opposing corners to prevent valleys.
|
||||
// 0---0 0---0 6---4
|
||||
// |\ | | /| |\ /|
|
||||
// | 0 | | 0 | | 7 |
|
||||
// | \| |/ | |/ \|
|
||||
// 2---0 0---2 0---8
|
||||
float mid_z = 0;
|
||||
if (a.z == c.z && b.z != d.z) {
|
||||
mid_z = a.z;
|
||||
} else if (a.z != c.z && b.z == d.z) {
|
||||
mid_z = b.z;
|
||||
} else {
|
||||
const float mid_z_ac = (a.z + c.z) * 0.5f;
|
||||
const float mid_z_bd = (b.z + d.z) * 0.5f;
|
||||
mid_z = std::max(mid_z_ac, mid_z_bd);
|
||||
}
|
||||
|
||||
const float3 mid = {mid_x, mid_y, mid_z};
|
||||
append_tri(a, b, mid);
|
||||
append_tri(b, c, mid);
|
||||
append_tri(c, d, mid);
|
||||
append_tri(d, a, mid);
|
||||
}
|
||||
}
|
||||
// Build the left edge.
|
||||
for (int row = 0; row < nrow - 1; ++row) {
|
||||
const float3 a = get_pos(row, 0);
|
||||
const float3 b = get_pos(row + 1, 0);
|
||||
const float3 c = {b.x, b.y, -sz[3]};
|
||||
const float3 d = {a.x, a.y, -sz[3]};
|
||||
append_quad(a, b, c, d);
|
||||
}
|
||||
// Build the right edge.
|
||||
for (int row = 0; row < nrow - 1; ++row) {
|
||||
const float3 a = get_pos(row + 1, ncol - 1);
|
||||
const float3 b = get_pos(row, ncol - 1);
|
||||
const float3 c = {b.x, b.y, -sz[3]};
|
||||
const float3 d = {a.x, a.y, -sz[3]};
|
||||
append_quad(a, b, c, d);
|
||||
}
|
||||
// Build the front edge.
|
||||
for (int col = 0; col < ncol - 1; ++col) {
|
||||
const float3 a = get_pos(0, col);
|
||||
const float3 b = {a.x, a.y, -sz[3]};
|
||||
const float3 d = get_pos(0, col + 1);
|
||||
const float3 c = {d.x, d.y, -sz[3]};
|
||||
append_quad(a, b, c, d);
|
||||
}
|
||||
// Build the back edge.
|
||||
for (int col = 0; col < ncol - 1; ++col) {
|
||||
const float3 a = get_pos(nrow - 1, col + 1);
|
||||
const float3 b = {a.x, a.y, -sz[3]};
|
||||
const float3 d = get_pos(nrow - 1, col);
|
||||
const float3 c = {d.x, d.y, -sz[3]};
|
||||
append_quad(a, b, c, d);
|
||||
}
|
||||
// Build the base. We use the visualization quality as the size rather than
|
||||
// the height field dimensions.
|
||||
const float base_width = (0.5f * model->vis.quality.numquads);
|
||||
const float base_height = (0.5f * model->vis.quality.numquads);
|
||||
for (int row = 0; row < model->vis.quality.numquads; ++row) {
|
||||
for (int col = 0; col < model->vis.quality.numquads; ++col) {
|
||||
const float x0 = sz[0] * ((col + 0) / base_width - 1.0f);
|
||||
const float x1 = sz[0] * ((col + 1) / base_width - 1.0f);
|
||||
const float y0 = sz[1] * ((row + 0) / base_height - 1.0f);
|
||||
const float y1 = sz[1] * ((row + 1) / base_height - 1.0f);
|
||||
append_quad({x0, y0, -sz[3]}, {x0, y1, -sz[3]}, {x1, y1, -sz[3]},
|
||||
{x1, y0, -sz[3]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int CalculateHeightFieldVertexCount(const mjModel* model, int hfieldid) {
|
||||
const int nrow = model->hfield_nrow[hfieldid];
|
||||
const int ncol = model->hfield_ncol[hfieldid];
|
||||
|
||||
// For details, see the logic in FillHeightFieldBuffer for how many vertices
|
||||
// we need. But, in general...
|
||||
|
||||
// We use 4 triangles (i.e. 12 vertices) per quad.
|
||||
const int surface_count = 12 * (nrow - 1) * (ncol - 1);
|
||||
// We use 1 quad (i.e. 6 vertices) per edge element. We double this because
|
||||
// we have two edges per dimension (e.g. left/right and front/back).
|
||||
const int edge_count = (12 * (nrow - 1)) + (12 * (ncol - 1));
|
||||
// We use 1 quad (i.e. 6 vertices) per base element. We use the visualization
|
||||
// quality as the size rather than the height field dimensions.
|
||||
const int base_count =
|
||||
6 * model->vis.quality.numquads * model->vis.quality.numquads;
|
||||
|
||||
const int total_count = surface_count + edge_count + base_count;
|
||||
return total_count;
|
||||
}
|
||||
|
||||
static bool HasUvs(const mjModel* model, int id, MeshType mesh_type) {
|
||||
return mesh_type != MeshType::kHeightField &&
|
||||
model->mesh_texcoordadr[id] >= 0;
|
||||
}
|
||||
|
||||
static bool IsValidIndex(const mjModel* model, int id, MeshType mesh_type) {
|
||||
switch (mesh_type) {
|
||||
case MeshType::kNormal:
|
||||
return id >= 0 && id < model->nmesh;
|
||||
case MeshType::kConvexHull:
|
||||
return id >= 0 && id < model->nmesh;
|
||||
case MeshType::kHeightField:
|
||||
return id >= 0 && id < model->nhfield;
|
||||
}
|
||||
}
|
||||
|
||||
static int GetNumVertices(const mjModel* model, int id, MeshType mesh_type) {
|
||||
switch (mesh_type) {
|
||||
case MeshType::kNormal:
|
||||
return 3 * model->mesh_facenum[id];
|
||||
case MeshType::kConvexHull:
|
||||
return 3 * model->mesh_graph[model->mesh_graphadr[id] + 1];
|
||||
case MeshType::kHeightField:
|
||||
return CalculateHeightFieldVertexCount(model, id);
|
||||
}
|
||||
}
|
||||
|
||||
static void UpdateMeshData(mjrfMeshData* data, const mjModel* model, int id,
|
||||
MeshType mesh_type) {
|
||||
if (!IsValidIndex(model, id, mesh_type)) {
|
||||
mju_error("Invalid index %d for type %d", id, mesh_type);
|
||||
return;
|
||||
}
|
||||
|
||||
const int num_vertices = GetNumVertices(model, id, mesh_type);
|
||||
const bool has_uvs = HasUvs(model, id, mesh_type);
|
||||
|
||||
MeshBuilder* builder = new MeshBuilder(num_vertices);
|
||||
data->user_data = builder;
|
||||
data->release = [](void* user_data) {
|
||||
delete static_cast<MeshBuilder*>(user_data);
|
||||
};
|
||||
|
||||
switch (mesh_type) {
|
||||
case MeshType::kNormal:
|
||||
FillMeshBuffer(*builder, model, id);
|
||||
break;
|
||||
case MeshType::kConvexHull:
|
||||
FillConvexHullBuffer(*builder, model, id);
|
||||
break;
|
||||
case MeshType::kHeightField:
|
||||
FillHeightFieldBuffer(*builder, model, id);
|
||||
break;
|
||||
}
|
||||
|
||||
data->primitive_type = mjMESH_PRIMITIVE_TYPE_TRIANGLES;
|
||||
data->nvertices = num_vertices;
|
||||
data->nindices = data->nvertices;
|
||||
data->indices = nullptr;
|
||||
data->index_type = data->nvertices >= std::numeric_limits<uint16_t>::max()
|
||||
? mjINDEX_TYPE_U32
|
||||
: mjINDEX_TYPE_U16;
|
||||
data->nattributes = has_uvs ? 3 : 2;
|
||||
data->attributes[0].usage = mjVERTEX_ATTRIBUTE_USAGE_POSITION;
|
||||
data->attributes[0].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT3;
|
||||
data->attributes[0].bytes = builder->positions.data();
|
||||
data->attributes[1].usage = mjVERTEX_ATTRIBUTE_USAGE_TANGENTS;
|
||||
data->attributes[1].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT4;
|
||||
data->attributes[1].bytes = builder->orientations.data();
|
||||
if (has_uvs) {
|
||||
data->attributes[2].usage = mjVERTEX_ATTRIBUTE_USAGE_UV;
|
||||
data->attributes[2].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT2;
|
||||
data->attributes[2].bytes = builder->uvs.data();
|
||||
}
|
||||
data->bounds_min[0] = builder->bounds_min.x;
|
||||
data->bounds_min[1] = builder->bounds_min.y;
|
||||
data->bounds_min[2] = builder->bounds_min.z;
|
||||
data->bounds_max[0] = builder->bounds_max.x;
|
||||
data->bounds_max[1] = builder->bounds_max.y;
|
||||
data->bounds_max[2] = builder->bounds_max.z;
|
||||
}
|
||||
|
||||
ModelObjects::ModelObjects(const mjModel* model, mjrfContext* ctx)
|
||||
: model_(model), ctx_(ctx) {
|
||||
|
||||
for (int i = 0; i < model_->ntex; ++i) {
|
||||
UploadTexture(model_, i);
|
||||
}
|
||||
for (int i = 0; i < model_->nmesh; ++i) {
|
||||
UploadMesh(model_, i);
|
||||
}
|
||||
for (int i = 0; i < model_->nhfield; ++i) {
|
||||
UploadHeightField(model_, i);
|
||||
}
|
||||
|
||||
specular_multiplier_ = ReadElement(
|
||||
model_, "filament.phong.specular_multiplier", specular_multiplier_);
|
||||
shininess_multiplier_ = ReadElement(
|
||||
model_, "filament.phong.shininess_multiplier", shininess_multiplier_);
|
||||
emissive_multiplier_ = ReadElement(
|
||||
model_, "filament.phong.emissive_multiplier", emissive_multiplier_);
|
||||
}
|
||||
|
||||
void ModelObjects::UploadMesh(const mjModel* model, int id) {
|
||||
if (model != model_) {
|
||||
mju_error("Model mismatch.");
|
||||
}
|
||||
if (id < 0 || id >= model->nmesh) {
|
||||
mju_error("Invalid mesh index %d", id);
|
||||
}
|
||||
meshes_.erase(id);
|
||||
convex_hulls_.erase(id);
|
||||
|
||||
mjrfMeshData data;
|
||||
mjrf_defaultMeshData(&data);
|
||||
UpdateMeshData(&data, model, id, MeshType::kNormal);
|
||||
meshes_.insert_or_assign(id, CreateMesh(ctx_, data));
|
||||
|
||||
if (model->mesh_graphadr[id] >= 0) {
|
||||
mjrfMeshData convex_hull_data;
|
||||
mjrf_defaultMeshData(&convex_hull_data);
|
||||
UpdateMeshData(&convex_hull_data, model, id, MeshType::kConvexHull);
|
||||
convex_hulls_.insert_or_assign(id, CreateMesh(ctx_, convex_hull_data));
|
||||
}
|
||||
}
|
||||
|
||||
void ModelObjects::UploadTexture(const mjModel* model, int id) {
|
||||
if (model != model_) {
|
||||
mju_error("Model mismatch.");
|
||||
}
|
||||
if (id < 0 || id >= model->ntex) {
|
||||
mju_error("Invalid texture index: %d", id);
|
||||
}
|
||||
|
||||
mjrfTextureConfig config;
|
||||
mjrf_defaultTextureConfig(&config);
|
||||
config.width = model->tex_width[id];
|
||||
config.height = model->tex_height[id];
|
||||
config.sampler_type = (mjtTexture)model->tex_type[id];
|
||||
config.color_space = (mjtColorSpace)model->tex_colorspace[id];
|
||||
switch (model->tex_nchannel[id]) {
|
||||
case 1:
|
||||
config.format = mjPIXEL_FORMAT_R8;
|
||||
break;
|
||||
case 3:
|
||||
config.format = mjPIXEL_FORMAT_RGB8;
|
||||
break;
|
||||
case 4:
|
||||
config.format = mjPIXEL_FORMAT_RGBA8;
|
||||
break;
|
||||
default:
|
||||
mju_error("Unsupported texture format: %d", model->tex_nchannel[id]);
|
||||
break;
|
||||
}
|
||||
if (config.height == 1 && model->tex_nchannel[id] == 1) {
|
||||
config.format = mjPIXEL_FORMAT_KTX;
|
||||
}
|
||||
|
||||
mjrfTextureData payload;
|
||||
mjrf_defaultTextureData(&payload);
|
||||
payload.bytes = model->tex_data + model->tex_adr[id];
|
||||
payload.nbytes =
|
||||
model->tex_width[id] * model->tex_height[id] * model->tex_nchannel[id];
|
||||
// We assume that the model has the same lifetime as the engine.
|
||||
payload.user_data = nullptr;
|
||||
payload.release = nullptr;
|
||||
|
||||
auto texture = CreateTexture(ctx_, config);
|
||||
mjrf_setTextureData(texture.get(), &payload);
|
||||
textures_.insert_or_assign(id, std::move(texture));
|
||||
}
|
||||
|
||||
void ModelObjects::UploadHeightField(const mjModel* model, int id) {
|
||||
if (model != model_) {
|
||||
mju_error("Model mismatch.");
|
||||
}
|
||||
if (id < 0 || id >= model->nhfield) {
|
||||
mju_error("Invalid height field index %d", id);
|
||||
}
|
||||
|
||||
height_fields_.erase(id);
|
||||
|
||||
mjrfMeshData data;
|
||||
mjrf_defaultMeshData(&data);
|
||||
UpdateMeshData(&data, model, id, MeshType::kHeightField);
|
||||
height_fields_.insert_or_assign(id, CreateMesh(ctx_, data));
|
||||
}
|
||||
|
||||
const mjrfMesh* ModelObjects::GetMesh(int data_id) const {
|
||||
// As defined by mjv_updateScene:
|
||||
// original mesh: mesh_id * 2
|
||||
// convex hull: (mesh_id * 2) + 1
|
||||
const int mesh_id = data_id / 2;
|
||||
if (data_id % 2 == 0) {
|
||||
auto it = meshes_.find(mesh_id);
|
||||
return it != meshes_.end() ? it->second.get() : nullptr;
|
||||
} else {
|
||||
auto it = convex_hulls_.find(mesh_id);
|
||||
return it != convex_hulls_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const mjrfMesh* ModelObjects::GetHeightField(int hfield_id) const {
|
||||
if (auto it = height_fields_.find(hfield_id); it != height_fields_.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
mju_error("Unknown height field %d", hfield_id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const mjrfTexture* ModelObjects::GetTexture(int tex_id) const {
|
||||
if (auto it = textures_.find(tex_id); it != textures_.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
mju_error("Unknown texture %d", tex_id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const mjrfTexture* ModelObjects::GetSkyboxTexture() const {
|
||||
for (auto& iter : textures_) {
|
||||
if (model_->tex_type[iter.first] == mjTEXTURE_SKYBOX) {
|
||||
return iter.second.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace mujoco
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_MODEL_OBJECTS_H_
|
||||
#define MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_MODEL_OBJECTS_H_
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <mujoco/mjmodel.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "render/filament/mjrfilament.h"
|
||||
#include "render/filament/mjrfilament_cpp.h"
|
||||
|
||||
namespace mujoco {
|
||||
|
||||
// Creates and owns meshes and textures read from an mjModel.
|
||||
class ModelObjects {
|
||||
public:
|
||||
ModelObjects(const mjModel* model, mjrfContext* ctx);
|
||||
|
||||
// Uploads a new mesh from the model with the given id.
|
||||
void UploadMesh(const mjModel* model, int id);
|
||||
|
||||
// Uploads a new texture from the model with the given id.
|
||||
void UploadTexture(const mjModel* model, int id);
|
||||
|
||||
// Uploads a new height field from the model with the given id.
|
||||
void UploadHeightField(const mjModel* model, int id);
|
||||
|
||||
// Returns the mjModel mesh with the given data_id. The data_id is the
|
||||
// mesh_id * 2 of the mesh in the mjModel.
|
||||
const mjrfMesh* GetMesh(int data_id) const;
|
||||
|
||||
// Returns the mjModel convex hulll mesh with the given data_id. The data_id
|
||||
// is the (mesh_id * 2) + 1 of the mesh in the mjModel.
|
||||
const mjrfMesh* GetConvexHull(int data_id) const;
|
||||
|
||||
// Returns the mjModel height field mesh with the given id.
|
||||
const mjrfMesh* GetHeightField(int hfield_id) const;
|
||||
|
||||
// Returns the mjModel texture with the given id.
|
||||
const mjrfTexture* GetTexture(int tex_id) const;
|
||||
|
||||
// Returns the skybox texture in the mjModel.
|
||||
const mjrfTexture* GetSkyboxTexture() const;
|
||||
|
||||
// Returns the mjModel from which the Model Objects are created.
|
||||
const mjModel* GetModel() const { return model_; }
|
||||
|
||||
// Returns the multipliers used for mapping legacy material properties to
|
||||
// filament material properties.
|
||||
float GetSpecularMultiplier() const { return specular_multiplier_; }
|
||||
float GetShininessMultiplier() const { return shininess_multiplier_; }
|
||||
float GetEmissiveMultiplier() const { return emissive_multiplier_; }
|
||||
|
||||
ModelObjects(const ModelObjects&) = delete;
|
||||
ModelObjects& operator=(const ModelObjects&) = delete;
|
||||
|
||||
private:
|
||||
const mjModel* model_ = nullptr;
|
||||
mjrfContext* ctx_ = nullptr;
|
||||
std::unordered_map<int, UniquePtr<mjrfMesh>> meshes_;
|
||||
std::unordered_map<int, UniquePtr<mjrfMesh>> convex_hulls_;
|
||||
std::unordered_map<int, UniquePtr<mjrfMesh>> height_fields_;
|
||||
std::unordered_map<int, UniquePtr<mjrfTexture>> textures_;
|
||||
float specular_multiplier_ = 0.2f;
|
||||
float shininess_multiplier_ = 0.1f;
|
||||
float emissive_multiplier_ = 0.3f;
|
||||
};
|
||||
|
||||
} // namespace mujoco
|
||||
|
||||
#endif // MUJOCO_SRC_RENDER_FILAMENT_SUPPORT_MODEL_OBJECTS_H_
|
||||
Reference in New Issue
Block a user