Introduce outline render passes and shader.

Use the jump flood algorithm to render an outline around selected
objects. This requires using custom post-processing shaders and
render passes which is managed by a new "Outliner" class.

PiperOrigin-RevId: 929212147
Change-Id: Ie6f475c7c677df7b0d4fc030a8e9520ff9dd03af
This commit is contained in:
Haroon Qureshi
2026-06-09 08:31:11 -07:00
committed by Copybara-Service
parent e6e5229126
commit b083ae487e
16 changed files with 726 additions and 30 deletions
+5
View File
@@ -43,6 +43,8 @@ target_sources(${MUJOCO_FILAMENT_TARGET_NAME}
filament/mesh.h
filament/object_manager.cc
filament/object_manager.h
filament/outliner.cc
filament/outliner.h
filament/reflection_manager.cc
filament/reflection_manager.h
filament/render_target.cc
@@ -117,6 +119,9 @@ set(MATERIAL_FILES
phong_cube_fade.mat
phong_cube.mat
phong_cube_reflect.mat
outline_composite.mat
outline_flatten.mat
outline_jumpflood.mat
unlit_decor.mat
unlit_depth.mat
unlit_segmentation.mat
@@ -0,0 +1,82 @@
// 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.
// This material uses the results of the Jump Flooding Algorithm to draw an
// outline around an object. The outline is drawn by checking the distance to
// the nearest edge point (as computed by the algorithm) and, if within the
// desired width, setting the color of the pixel to the desired outline color.
material {
name : outline_composite,
blending : transparent,
culling : none,
depthWrite : false,
depthCulling : false,
parameters : [
{ type : sampler2d, name : source },
{ type : float4, name : color },
{ type : float, name : width }
],
variables : [
vertex
],
domain : postprocess
}
vertex {
void postProcessVertex(inout PostProcessVertexInputs postProcess) {
postProcess.vertex.xy = uvToRenderTargetUV(postProcess.normalizedUV);
}
}
fragment {
vec2 unpack(vec4 value) {
float x = value.r + value.g / 255.0;
float y = value.b + value.a / 255.0;
// Reverse scale and bias
return (vec2(x, y) - 0.001) / 0.999;
}
void postProcess(inout PostProcessInputs postProcess) {
vec4 tex = texture(materialParams_source, variable_vertex.xy);
if (tex == vec4(0.0)) {
discard;
}
vec2 edge = unpack(tex);
// Calculate distance to the nearest edge in pixel units.
vec2 size = vec2(textureSize(materialParams_source, 0));
vec2 diff = (edge - variable_vertex.xy) * size;
float dist = length(diff);
// Exclude the interior of the selected object itself.
if (dist <= 0.5) {
discard;
}
// Only draw outline if the pixel is within 'width' of an edge.
float width = materialParams.width;
if (dist >= width) {
discard;
}
// Inner edge anti-aliasing (smooths transition from object to outline).
// Outer edge anti-aliasing (fades at the outline boundary).
float inner_alpha = smoothstep(0.0, 1.0, dist);
float outer_alpha = smoothstep(width, width - 1.0, dist);
float alpha = outer_alpha * inner_alpha;
postProcess.color = mix(vec4(0), materialParams.color, alpha * materialParams.color.a);
}
}
@@ -0,0 +1,54 @@
// 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.
// This material writes the the screen-space position of each fragment as the
// output color of that fragment. This will provide us with the initial data for
// the Jump Flood Algorithm. See "outline_jumpflood.mat" for more details.
material {
name : outline_flatten,
shadingModel : unlit,
culling : none,
depthWrite : false
}
fragment {
// Encode a vec2 value with the range [0-1] into a 32-bit (RGBA) value.
// We do this by packing each coordinate into a 16-bit value. Values are
// adjusted to ensure [0x0000] is reserved for "untouched" pixels rather
// than the pixel at the position (0, 0).
vec4 pack(vec2 value) {
// Scale and bias the value to avoid (0,0,0,0).
vec2 adjusted = value * 0.999 + 0.001;
// Pack float [0, 1] into two 8-bit channels.
float val_x = adjusted.x * 255.0;
float r_x = floor(val_x) / 255.0;
float g_x = fract(val_x);
float val_y = adjusted.y * 255.0;
float r_y = floor(val_y) / 255.0;
float g_y = fract(val_y);
return vec4(r_x, g_x, r_y, g_y);
}
void material(inout MaterialInputs material) {
prepareMaterial(material);
// Get the screen-space position of the fragment, normalized to [0-1].
vec2 uv = gl_FragCoord.xy * frameUniforms.resolution.zw;
material.baseColor = pack(uv);
}
}
@@ -0,0 +1,110 @@
// 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.
// This material implements a single pass of the Jump Flooding Algorithm (JFA).
// This algorithm is used to efficiently compute a distance transform where each
// pixel stores the UV coordinates of its closest edge point. In each pass,
// pixels look at their neighbors at a distance 'step' to see if they have a
// closer edge point.
material {
name : outline_jumpflood,
culling : none,
depthWrite : false,
depthCulling : false,
parameters : [
{ type : sampler2d, name : source },
{ type : float, name : step }
],
variables : [
vertex
],
domain : postprocess
}
vertex {
void postProcessVertex(inout PostProcessVertexInputs postProcess) {
postProcess.vertex.xy = uvToRenderTargetUV(postProcess.normalizedUV);
}
}
fragment {
// Encode a vec2 value with the range [0-1] into a 32-bit (RGBA) value.
// We do this by packing each coordinate into a 16-bit value. Values are
// adjusted to ensure [0x0000] is reserved for "untouched" pixels rather
// than the pixel at the position (0, 0).
vec4 pack(vec2 value) {
// Scale and bias the value to avoid (0,0,0,0).
vec2 adjusted = value * 0.999 + 0.001;
// Pack float [0, 1] into two 8-bit channels.
float val_x = adjusted.x * 255.0;
float r_x = floor(val_x) / 255.0;
float g_x = fract(val_x);
float val_y = adjusted.y * 255.0;
float r_y = floor(val_y) / 255.0;
float g_y = fract(val_y);
return vec4(r_x, g_x, r_y, g_y);
}
vec2 unpack(vec4 value) {
float x = value.r + value.g / 255.0;
float y = value.b + value.a / 255.0;
// Reverse scale and bias
return (vec2(x, y) - 0.001) / 0.999;
}
vec3 jump(vec3 best, vec2 offset) {
vec2 uv = variable_vertex.xy + offset;
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
return best;
}
vec4 tex = texture(materialParams_source, uv);
if (tex == vec4(0.0)) {
return best;
}
vec2 edge = unpack(tex);
vec2 diff = (edge - variable_vertex.xy);
float dist_sq = dot(diff, diff);
if (dist_sq < best.z) {
best = vec3(edge.x, edge.y, dist_sq);
}
return best;
}
void postProcess(inout PostProcessInputs postProcess) {
vec2 size = vec2(textureSize(materialParams_source, 0));
vec2 dist = materialParams.step / size;
vec3 best = vec3(-1.0, -1.0, 1e20);
best = jump(best, dist * vec2(+1, +1));
best = jump(best, dist * vec2(+1, 0));
best = jump(best, dist * vec2(+1, -1));
best = jump(best, dist * vec2( 0, +1));
best = jump(best, dist * vec2( 0, 0));
best = jump(best, dist * vec2( 0, -1));
best = jump(best, dist * vec2(-1, +1));
best = jump(best, dist * vec2(-1, 0));
best = jump(best, dist * vec2(-1, -1));
if (best.x >= 0.0) {
postProcess.color = pack(best.xy);
} else {
postProcess.color = vec4(0.0);
}
}
}
@@ -185,6 +185,12 @@ static void UpdateGeomMaterial(mjrfRenderable* renderable, const mjvGeom& geom,
material.segmentation_id = geom.segid;
// Assume an emissive object is a selected object.
if (geom.emission > 0 && geom.emission == model->vis.global.glow) {
material.selected = true;
material.emissive = 0.0f;
}
// UvScale only applies to objects that don't have explicit UV coordinates
// in their vertex buffer. Instead, we set the UV coordinate to be the same
// as the vertex position.
@@ -193,7 +193,9 @@ MaterialManager::MaterialKey MaterialManager::PrepareMaterialInstance(
const mjrfMaterial& material, mjrDrawMode draw_mode, mjtGeom geom_type,
const Mesh* mesh) {
ObjectManager::MaterialType type;
if (draw_mode == mjDRAW_MODE_DEPTH) {
if (material.selected) {
type = ObjectManager::kOutlineFlatten;
} else if (draw_mode == mjDRAW_MODE_DEPTH) {
type = ObjectManager::kUnlitDepth;
} else if (draw_mode == mjDRAW_MODE_SEGMENTATION_BY_ID) {
type = ObjectManager::kUnlitSegmentation;
@@ -14,10 +14,8 @@
#include "experimental/filament/filament/object_manager.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <utility>
@@ -27,6 +25,7 @@
#include <filament/Material.h>
#include <filament/Skybox.h>
#include <filament/Texture.h>
#include <math/vec2.h>
#include <mujoco/mujoco.h>
#include "experimental/filament/filament/builtins.h"
#include "user/user_resource.h"
@@ -73,6 +72,9 @@ ObjectManager::ObjectManager(filament::Engine* engine)
materials_[kUnlitDecor] = LoadMaterial(engine, "unlit_decor.filamat");
materials_[kUnlitDepth] = LoadMaterial(engine, "unlit_depth.filamat");
materials_[kUnlitUi] = LoadMaterial(engine, "unlit_ui.filamat");
materials_[kOutlineComposite] = LoadMaterial(engine, "outline_composite.filamat");
materials_[kOutlineFlatten] = LoadMaterial(engine, "outline_flatten.filamat");
materials_[kOutlineJumpFlood] = LoadMaterial(engine, "outline_jumpflood.filamat");
static uint8_t black_rgb[3] = {0, 0, 0};
static uint8_t white_rgb[3] = {255, 255, 255};
@@ -106,9 +108,13 @@ ObjectManager::ObjectManager(filament::Engine* engine)
fallback_textures_[mjTEXROLE_NORMAL] = fallback_normal_;
fallback_textures_[mjTEXROLE_EMISSIVE] = fallback_black_;
fallback_textures_[mjTEXROLE_ORM] = fallback_orm_;
CreateQuadBuffers();
}
ObjectManager::~ObjectManager() {
engine_->destroy(quad_vb_);
engine_->destroy(quad_ib_);
engine_->destroy(fallback_black_);
engine_->destroy(fallback_white_);
engine_->destroy(fallback_normal_);
@@ -148,4 +154,33 @@ const filament::Texture* ObjectManager::GetFallbackTexture(
}
return fallback_textures_[role];
}
void ObjectManager::CreateQuadBuffers() {
// Define a single triangle that completely covers the viewport. (A single
// large triangle is more efficient than a two-triangle quad because it
// avoids the diagonal edge where pixels might be rasterized twice.)
static const filament::math::float2 kVertices[3] = {
{-1.0f, -1.0f},
{ 3.0f, -1.0f},
{-1.0f, 3.0f}
};
static const uint16_t kIndices[3] = {0, 1, 2};
filament::VertexBuffer::Builder vb_builder;
vb_builder.bufferCount(1);
vb_builder.vertexCount(3);
vb_builder.attribute(filament::VertexAttribute::POSITION, 0,
filament::VertexBuffer::AttributeType::FLOAT2, 0,
sizeof(filament::math::float2));
quad_vb_ = vb_builder.build(*engine_);
quad_vb_->setBufferAt(*engine_, 0, {kVertices, sizeof(kVertices)});
filament::IndexBuffer::Builder ib_builder;
ib_builder.indexCount(3);
ib_builder.bufferType(filament::IndexBuffer::IndexType::USHORT);
quad_ib_ = ib_builder.build(*engine_);
quad_ib_->setBuffer(*engine_, {kIndices, sizeof(kIndices)});
}
} // namespace mujoco
@@ -19,7 +19,6 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
#include <string_view>
#include <unordered_map>
@@ -61,6 +60,9 @@ class ObjectManager {
kUnlitDecor,
kUnlitDepth,
kUnlitUi,
kOutlineComposite,
kOutlineFlatten,
kOutlineJumpFlood,
kNumMaterials,
};
@@ -70,6 +72,10 @@ class ObjectManager {
// Returns the fallback Texture with the given role.
const filament::Texture* GetFallbackTexture(mjtTextureRole role) const;
// Returns the buffers for creating a full-screen quad.
filament::VertexBuffer* GetQuadVertexBuffer() const { return quad_vb_; }
filament::IndexBuffer* GetQuadIndexBuffer() const { return quad_ib_; }
// Returns the built-in mesh collection with the given dimensions. For
// performance reasons, you should consider always using the same dimensions
// in order to reuse the same meshes.
@@ -82,6 +88,8 @@ class ObjectManager {
ObjectManager& operator=(const ObjectManager&) = delete;
private:
void CreateQuadBuffers();
filament::Engine* engine_ = nullptr;
std::array<filament::Material*, kNumMaterials> materials_;
std::array<filament::Texture*, mjNTEXROLE> fallback_textures_;
@@ -90,6 +98,8 @@ class ObjectManager {
filament::Texture* fallback_black_ = nullptr;
filament::Texture* fallback_normal_ = nullptr;
filament::Texture* fallback_orm_ = nullptr;
filament::VertexBuffer* quad_vb_ = nullptr;
filament::IndexBuffer* quad_ib_ = nullptr;
};
} // namespace mujoco
@@ -0,0 +1,246 @@
// 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 "experimental/filament/filament/outliner.h"
#include <cstdint>
#include <memory>
#include <filament/Camera.h>
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include <filament/RenderableManager.h>
#include <filament/RenderTarget.h>
#include <filament/Scene.h>
#include <filament/TextureSampler.h>
#include <filament/View.h>
#include <filament/Viewport.h>
#include <math/vec4.h>
#include <utils/EntityManager.h>
#include <mujoco/mujoco.h>
#include "experimental/filament/filament/object_manager.h"
#include "experimental/filament/filament/render_target.h"
#include "experimental/filament/filament/texture.h"
#include "experimental/filament/render_context_filament.h"
namespace mujoco {
Outliner::Outliner(ObjectManager* object_mgr, uint8_t layer_mask,
filament::math::float4 color, float thickness)
: object_mgr_(object_mgr),
engine_(object_mgr->GetEngine()),
layer_mask_(layer_mask),
color_(color),
thickness_(thickness) {}
Outliner::~Outliner() {
Reset();
}
void Outliner::Prepare(int width, int height) {
if (width == width_ && height == height_) {
return;
}
Reset();
width_ = width;
height_ = height;
if (width_ <= 0 || height_ <= 0) {
return;
}
auto& em = utils::EntityManager::get();
// Sets up a pass with a full-screen quad that renders with a given material.
auto setup_fullscreen = [&](int pass, ObjectManager::MaterialType type) {
filament::Material* material = object_mgr_->GetMaterial(type);
filament::VertexBuffer* quad_vb = object_mgr_->GetQuadVertexBuffer();
filament::IndexBuffer* quad_ib = object_mgr_->GetQuadIndexBuffer();
const filament::RenderableManager::PrimitiveType primitive_type =
filament::RenderableManager::PrimitiveType::TRIANGLES;
material_instances_[pass] = material->createInstance();
quads_[pass] = em.create();
filament::RenderableManager::Builder(1)
.geometry(0, primitive_type, quad_vb, quad_ib)
.material(0, material_instances_[pass])
.culling(false)
.receiveShadows(false)
.castShadows(false)
.build(*engine_, quads_[pass]);
scenes_[pass] = engine_->createScene();
scenes_[pass]->addEntity(quads_[pass]);
views_[pass]->setScene(scenes_[pass]);
};
// Sets up a pass to use the given render target as the source and/or output.
auto bind = [&](int pass, int src, int out) {
// Set the `source` texture as the color texture from a render target.
if (src >= 0) {
const filament::TextureSampler sampler(
filament::TextureSampler::MinFilter::NEAREST,
filament::TextureSampler::MagFilter::NEAREST);
material_instances_[pass]->setParameter(
"source", targets_[src]->GetColorTexture()->GetFilamentTexture(),
sampler);
}
// Set the output of the view to the given render target.
if (out >= 0) {
views_[pass]->setRenderTarget(targets_[out]->GetFilamentRenderTarget());
}
};
// Set up two render targets. We will alternate between the two targets to
// allow for chaining passes together.
for (int i = 0; i < 2; ++i) {
mjrfRenderTargetConfig config;
mjrf_defaultRenderTargetConfig(&config);
config.color_format = mjPIXEL_FORMAT_RGBA8;
config.depth_format = mjPIXEL_FORMAT_DEPTH32F;
config.width = width;
config.height = height;
targets_[i] = std::make_unique<RenderTarget>(engine_, config);
targets_[i]->Prepare(width, height);
}
// Setup orthographic camera for full-screen quad rendering.
camera_ = engine_->createCamera(em.create());
camera_->setProjection(filament::Camera::Projection::ORTHO, -1.0, 1.0, -1.0,
1.0, -1.0, 1.0);
camera_->lookAt({0, 0, 1}, {0, 0, 0}, {0, 1, 0});
// Setup all the views.
for (auto& view : views_) {
view = engine_->createView();
view->setCamera(camera_);
view->setViewport({0, 0, (uint32_t)width, (uint32_t)height});
view->setPostProcessingEnabled(false);
view->setShadowingEnabled(false);
view->setMultiSampleAntiAliasingOptions({.enabled = false});
}
// In the first pass, we will render a given scene, but only render the
// objects marked as outlines. We assume that the objects have already been
// assigned the kOutlineFlatten material.
views_[kPassFlatten]->setVisibleLayers(0xff, layer_mask_);
// All subsequent passes are full-screen post-processing passes.
setup_fullscreen(kPassJumpFlood1, ObjectManager::kOutlineJumpFlood);
setup_fullscreen(kPassJumpFlood2, ObjectManager::kOutlineJumpFlood);
setup_fullscreen(kPassJumpFlood3, ObjectManager::kOutlineJumpFlood);
setup_fullscreen(kPassJumpFlood4, ObjectManager::kOutlineJumpFlood);
setup_fullscreen(kPassJumpFlood5, ObjectManager::kOutlineJumpFlood);
setup_fullscreen(kPassDrawOutline, ObjectManager::kOutlineComposite);
// Chain the passes together such that the output of a pass is the input to
// the next pass. The first pass has no input (we are just rendering the
// selected objects) and the last pass has no output (we are just rendering
// the outline to the externally provided target).
bind(kPassFlatten, -1, 0);
bind(kPassJumpFlood1, 0, 1);
bind(kPassJumpFlood2, 1, 0);
bind(kPassJumpFlood3, 0, 1);
bind(kPassJumpFlood4, 1, 0);
bind(kPassJumpFlood5, 0, 1);
bind(kPassDrawOutline, 1, -1);
// Bind the parameters for each pass. For the jump flood passes, the step
// parameter determines how far to propagate the outline in each pass.
material_instances_[kPassJumpFlood1]->setParameter("step", 16.0f);
material_instances_[kPassJumpFlood2]->setParameter("step", 8.0f);
material_instances_[kPassJumpFlood3]->setParameter("step", 4.0f);
material_instances_[kPassJumpFlood4]->setParameter("step", 2.0f);
material_instances_[kPassJumpFlood5]->setParameter("step", 1.0f);
// The final pass renders the actual outline onto a render target.
material_instances_[kPassDrawOutline]->setParameter("color", color_);
material_instances_[kPassDrawOutline]->setParameter("width", thickness_);
// Commit all the material instances to the engine.
for (auto& material_instance : material_instances_) {
if (material_instance) {
material_instance->commit(*engine_);
}
}
}
void Outliner::Reset() {
auto& em = utils::EntityManager::get();
for (auto& scene : scenes_) {
if (scene) {
engine_->destroy(scene);
scene = nullptr;
}
}
for (auto& quad : quads_) {
if (quad) {
engine_->destroy(quad);
em.destroy(quad);
quad = utils::Entity();
}
}
for (auto& material_instance : material_instances_) {
if (material_instance) {
engine_->destroy(material_instance);
material_instance = nullptr;
}
}
for (auto& view : views_) {
if (view) {
engine_->destroy(view);
view = nullptr;
}
}
if (camera_) {
utils::Entity entity = camera_->getEntity();
engine_->destroyCameraComponent(entity);
em.destroy(entity);
camera_ = nullptr;
}
for (auto& target : targets_) {
target.reset();
}
width_ = 0;
height_ = 0;
}
void Outliner::Render(filament::Renderer* renderer,
filament::View* view,
filament::RenderTarget* render_target) {
filament::Viewport viewport = view->getViewport();
Prepare(viewport.width, viewport.height);
for (auto& view : views_) {
view->setViewport(viewport);
}
// Re-render the view's scene to create the flattened selection mask.
auto prev_clear_opts = renderer->getClearOptions();
renderer->setClearOptions({.clearColor = {0, 0, 0, 0}, .clear = true});
views_[kPassFlatten]->setScene(view->getScene());
views_[kPassFlatten]->setCamera(&view->getCamera());
renderer->render(views_[kPassFlatten]);
renderer->setClearOptions(prev_clear_opts);
// Run the jump flood steps to expand the selection mask
for (int i = 0; i < kNumJumpFloodPasses; ++i) {
renderer->render(views_[kPassJumpFlood1 + i]);
}
// Render the final pass as an outline onto the provided render target.
views_[kPassDrawOutline]->setRenderTarget(render_target);
renderer->render(views_[kPassDrawOutline]);
views_[kPassDrawOutline]->setRenderTarget(nullptr);
}
} // namespace mujoco
@@ -0,0 +1,87 @@
// 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_EXPERIMENTAL_FILAMENT_FILAMENT_OUTLINER_H_
#define MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_OUTLINER_H_
#include <cstdint>
#include <memory>
#include <filament/Camera.h>
#include <filament/Renderer.h>
#include <filament/View.h>
#include <math/vec4.h>
#include <utils/Entity.h>
#include "experimental/filament/filament/object_manager.h"
#include "experimental/filament/filament/render_target.h"
namespace mujoco {
// Renders an outline of selected objects.
//
// This class uses the "jump flood" algorithm to create an outline of selected
// objects.
class Outliner {
public:
Outliner(ObjectManager* object_mgr, uint8_t layer_mask,
filament::math::float4 color, float thickness);
~Outliner();
Outliner(const Outliner&) = delete;
Outliner& operator=(const Outliner&) = delete;
// Renders the outline of the selected objects in the given view. The outline
// will be rendered on top of the provided render target. This assumes that
// any objects to be outlined have been assigned an `OutlineFlatten` material
// instance.
void Render(filament::Renderer* renderer, filament::View* view,
filament::RenderTarget* render_target);
private:
void Prepare(int width, int height);
void Reset();
enum Pass {
kPassFlatten,
kPassJumpFlood1,
kPassJumpFlood2,
kPassJumpFlood3,
kPassJumpFlood4,
kPassJumpFlood5,
kPassDrawOutline,
kNumPasses,
kNumJumpFloodPasses = kPassJumpFlood5 - kPassJumpFlood1 + 1,
};
ObjectManager* object_mgr_ = nullptr;
filament::Engine* engine_ = nullptr;
uint8_t layer_mask_ = 0xff;
filament::math::float4 color_ = {1.0f, 1.0f, 1.0f, 1.0f};
float thickness_ = 2.5f;
int width_ = 0;
int height_ = 0;
filament::Camera* camera_ = nullptr;
std::unique_ptr<RenderTarget> targets_[2];
filament::View* views_[kNumPasses] = {};
filament::Scene* scenes_[kNumPasses] = {};
utils::Entity quads_[kNumPasses] = {};
filament::MaterialInstance* material_instances_[kNumPasses] = {};
};
} // namespace mujoco
#endif // MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_OUTLINER_H_
@@ -225,6 +225,9 @@ void Renderable::UpdateMaterial(const mjrfMaterial& material) {
if (material.decor_ux) {
layer_mask_ = kLayerMask_Decor;
}
if (material.selected) {
layer_mask_ |= kLayerMask_Outline;
}
SetLayerMask(layer_mask_);
material_ = material;
}
@@ -244,6 +247,7 @@ void Renderable::Prepare(std::span<const mjrfRenderRequest*> requests,
for (const mjrfRenderRequest* request : requests) {
DrawState draw_state;
mjrfMaterial material = material_;
material.selected = false;
draw_state.wireframe = (request->draw_mode == mjDRAW_MODE_WIREFRAME);
if (material.decor_ux) {
@@ -374,23 +378,23 @@ void Renderable::BindMaterialInstance(const mjrfRenderRequest& request) {
SetCastShadows(state.cast_shadows);
SetReceiveShadows(state.receive_shadows);
SetWireframe(state.wireframe);
SetMaterialInstance(state.material_key);
curr_state_ = state;
draw_queue_.pop_front();
}
if (state.material_key != curr_state_.material_key) {
MaterialManager::MaterialKey Renderable::SetMaterialInstance(MaterialManager::MaterialKey key) {
MaterialManager::MaterialKey prev = curr_state_.material_key;
if (key != curr_state_.material_key) {
filament::MaterialInstance* instance =
material_mgr_->GetInstance(state.material_key);
if (!instance) {
mju_error("Failed to get material instance.");
return;
}
material_mgr_->GetInstance(key);
filament::RenderableManager& rm = GetEngine()->getRenderableManager();
for (Part& part : parts_) {
filament::RenderableManager::Instance ri = rm.getInstance(part.entity);
rm.setMaterialInstanceAt(ri, 0, instance);
}
}
curr_state_ = state;
draw_queue_.pop_front();
return prev;
}
std::uint8_t Renderable::SetLayerMask(std::uint8_t mask) {
@@ -38,6 +38,7 @@ namespace mujoco {
enum LayerMask : uint8_t {
kLayerMask_Object = 0x01 << 1,
kLayerMask_Decor = 0x01 << 2,
kLayerMask_Outline = 0x01 << 3,
kLayerMask_All = 0xff,
kLayerMask_None = 0x00,
};
@@ -113,6 +114,8 @@ class Renderable : public mjrfRenderable {
// Binds the material instance for the given render request.
void BindMaterialInstance(const mjrfRenderRequest& request);
MaterialManager::MaterialKey SetMaterialInstance(MaterialManager::MaterialKey key);
static Renderable* downcast(mjrfRenderable* renderable) {
return static_cast<Renderable*>(renderable);
}
@@ -18,6 +18,7 @@
#include <memory>
#include <span>
#include <string_view>
#include <vector>
#include <filament/ColorGrading.h>
#include <filament/LightManager.h>
@@ -38,10 +39,13 @@
#include <math/vec4.h>
#include <utils/EntityManager.h>
#include <mujoco/mujoco.h>
#include "experimental/filament/filament/object_manager.h"
#include "experimental/filament/filament/outliner.h"
#include "experimental/filament/filament/reflection_manager.h"
#include "experimental/filament/filament_util.h"
#include "experimental/filament/filament/color_grading_options.h"
#include "experimental/filament/filament/light.h"
#include "experimental/filament/filament/material_manager.h"
#include "experimental/filament/filament/render_target.h"
#include "experimental/filament/filament/renderable.h"
#include "experimental/filament/filament/texture.h"
@@ -120,9 +124,11 @@ static void SetupReflectionCamera(const mat4& surface_xform,
reflection_camera->setCustomProjection(oblique, near, far);
}
SceneView::SceneView(filament::Engine* engine, const mjrfSceneParams& params)
: engine_(engine) {
reflection_mgr_ = std::make_unique<ReflectionManager>(engine_);
SceneView::SceneView(ObjectManager* object_mgr, MaterialManager* material_mgr,
const mjrfSceneParams& params)
: object_mgr_(object_mgr), material_mgr_(material_mgr) {
filament::Engine* engine = object_mgr_->GetEngine();
reflection_mgr_ = std::make_unique<ReflectionManager>(engine);
scene_ = engine->createScene();
camera_ = engine->createCamera(utils::EntityManager::get().create());
@@ -157,9 +163,12 @@ SceneView::SceneView(filament::Engine* engine, const mjrfSceneParams& params)
}
SceneView::~SceneView() {
filament::Engine* engine = object_mgr_->GetEngine();
outliner_.reset();
if (skybox_) {
scene_->setSkybox(nullptr);
engine_->destroy(skybox_);
engine->destroy(skybox_);
}
for (auto& light : lights_) {
light->RemoveFromScene(scene_);
@@ -169,15 +178,15 @@ SceneView::~SceneView() {
}
lights_.clear();
renderables_.clear();
engine_->destroyCameraComponent(reflect_camera_->getEntity());
engine_->destroy(reflect_view_);
engine_->destroyCameraComponent(camera_->getEntity());
engine->destroyCameraComponent(reflect_camera_->getEntity());
engine->destroy(reflect_view_);
engine->destroyCameraComponent(camera_->getEntity());
if (color_grading_) {
engine_->destroy(color_grading_);
engine->destroy(color_grading_);
}
engine_->destroy(scene_);
engine_->destroy(depth_segment_view_);
engine_->destroy(main_view_);
engine->destroy(scene_);
engine->destroy(depth_segment_view_);
engine->destroy(main_view_);
}
void SceneView::AddToScene(Light* light) {
@@ -225,9 +234,21 @@ void SceneView::PrepareToRender(std::span<const mjrfRenderRequest*> requests) {
}
}
bool has_selected = false;
reflection_mgr_->ClearRenderables();
for (Renderable* renderable : renderables_) {
renderable->Prepare(requests, reflection_mgr_.get());
if (renderable->GetMaterial().selected) {
has_selected = true;
}
}
if (has_selected) {
mjrfMaterial material;
mjrf_defaultMaterial(&material);
material.selected = true;
outline_material_key_ = material_mgr_->PrepareMaterialInstance(
material, mjDRAW_MODE_DEFAULT, mjGEOM_NONE, nullptr);
}
}
@@ -259,8 +280,13 @@ void SceneView::Render(filament::Renderer* renderer, const mjrfRenderRequest& re
SetupCamera(request.camera, viewport, camera_);
std::vector<Renderable*> selected_renderables;
for (auto& iter : renderables_) {
iter->BindMaterialInstance(request);
if (iter->GetMaterial().selected &&
request.draw_mode == mjDRAW_MODE_DEFAULT) {
selected_renderables.push_back(iter);
}
}
for (int i = 0; i < reflection_mgr_->GetNumRenderables(); ++i) {
@@ -285,11 +311,25 @@ void SceneView::Render(filament::Renderer* renderer, const mjrfRenderRequest& re
renderable->SetLayerMask(prev_layer_mask);
}
view->setRenderTarget(render_target ? render_target->GetFilamentRenderTarget()
: nullptr);
filament::RenderTarget* filament_render_target =
render_target ? render_target->GetFilamentRenderTarget() : nullptr;
view->setRenderTarget(filament_render_target);
renderer->render(view);
view->setRenderTarget(nullptr);
if (!selected_renderables.empty()) {
if (!outliner_) {
outliner_ =
std::make_unique<Outliner>(object_mgr_, kLayerMask_Outline,
float4{0.9f, 0.9f, 0.2f, 1.0f}, 3.5f);
}
for (Renderable* renderable : selected_renderables) {
renderable->SetMaterialInstance(outline_material_key_);
}
outliner_->Render(renderer, view, filament_render_target);
}
if (request.target) {
view->setMultiSampleAntiAliasingOptions(options);
}
@@ -27,6 +27,9 @@
#include <mujoco/mujoco.h>
#include "experimental/filament/filament/color_grading_options.h"
#include "experimental/filament/filament/light.h"
#include "experimental/filament/filament/material_manager.h"
#include "experimental/filament/filament/object_manager.h"
#include "experimental/filament/filament/outliner.h"
#include "experimental/filament/filament/renderable.h"
#include "experimental/filament/filament/reflection_manager.h"
#include "experimental/filament/filament/texture.h"
@@ -41,7 +44,8 @@ namespace mujoco {
// (e.g. normal, depth, segmentation, etc.) as well as reflective surfaces.
class SceneView : public mjrfScene {
public:
SceneView(filament::Engine* engine, const mjrfSceneParams& params);
SceneView(ObjectManager* object_mgr, MaterialManager* material_mgr,
const mjrfSceneParams& params);
~SceneView();
SceneView(const SceneView&) = delete;
@@ -63,7 +67,7 @@ class SceneView : public mjrfScene {
void Render(filament::Renderer* renderer, const mjrfRenderRequest& request);
// Returns the filament Engine managing the scene.
filament::Engine* GetEngine() const { return engine_; }
filament::Engine* GetEngine() const { return object_mgr_->GetEngine(); }
// Returns the underlying filament View that is used for normal rendering.
// Callers can update rendering settings (e.g. post processing) directly.
@@ -85,7 +89,8 @@ class SceneView : public mjrfScene {
}
private:
filament::Engine* engine_ = nullptr;
ObjectManager* object_mgr_ = nullptr;
MaterialManager* material_mgr_ = nullptr;
filament::Scene* scene_ = nullptr;
filament::Camera* camera_ = nullptr;
filament::ColorGrading* color_grading_ = nullptr;
@@ -101,6 +106,9 @@ class SceneView : public mjrfScene {
// Custom view and camera for reflective surfaces.
filament::View* reflect_view_ = nullptr;
filament::Camera* reflect_camera_ = nullptr;
MaterialManager::MaterialKey outline_material_key_;
std::unique_ptr<Outliner> outliner_;
std::unique_ptr<ReflectionManager> reflection_mgr_;
};
} // namespace mujoco
@@ -143,7 +143,8 @@ void mjrf_destroyMesh(mjrfMesh* mesh) { delete mujoco::Mesh::downcast(mesh); }
mjrfScene* mjrf_createScene(mjrfContext* ctx, const mjrfSceneParams* params) {
return new mujoco::SceneView(
mujoco::FilamentContext::downcast(ctx)->GetEngine(), *params);
mujoco::FilamentContext::downcast(ctx)->GetObjectManager(),
mujoco::FilamentContext::downcast(ctx)->GetMaterialManager(), *params);
}
void mjrf_destroyScene(mjrfScene* scene) {
+3
View File
@@ -159,6 +159,9 @@ var Module = {
"assets/phong_cube_fade.filamat",
"assets/phong_cube.filamat",
"assets/phong_cube_reflect.filamat",
"assets/outline_composite.filamat",
"assets/outline_flatten.filamat",
"assets/outline_jumpflood.filamat",
"assets/unlit_decor.filamat",
"assets/unlit_depth.filamat",
"assets/unlit_segmentation.filamat",