Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fixtures/html/images.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
gap: 40px;
}

#svg-image:hover {
Expand Down
108 changes: 65 additions & 43 deletions src/client/builtin_scene/client_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,44 +88,81 @@ namespace builtin_scene
}
}

void RenderSystem::renderPass(vector<EntityId> &roots,
RenderPass renderPass,
optional<XRRenderTarget> renderTarget)
struct RenderableMeshEntity
{
EntityId id;
shared_ptr<Mesh3d> meshComponent;
shared_ptr<MeshMaterial3d> materialComponent;
};

void RenderSystem::renderPass(vector<EntityId> &roots, RenderPass renderPass, optional<XRRenderTarget> renderTarget)
{
assert(roots.size() > 0 && "The roots must not be empty.");

// TODO(yorkie): support other render passes like particles, post-processing, etc.
if (renderPass != RenderPass::kOpaques)
return;
vector<RenderableMeshEntity> entities; // TODO(yorkie): cache this?
auto addRenderableItem = [this, &renderPass, &entities](EntityId entity) -> bool
{
// Render the mesh if it exists
auto mesh = getComponent<Mesh3d>(entity);
if (mesh != nullptr)
{
// If the mesh exists but rendering is disabled, we need to skip its rendering and its children.
if (mesh->isRenderingDisabled())
return false;

auto material = getComponent<MeshMaterial3d>(entity);
assert(material != nullptr &&
"The material component must be valid on renderable mesh");

if (material->matchesPass(renderPass))
entities.push_back({entity, mesh, material});
}

// TODO: support other renderable components (e.g., particles, etc.)
return true;
};

renderVolumeMask(renderPass, renderTarget);
onBeforeRender(renderTarget);
// Search for renderable items in the hierarchy
for (const auto &root : roots)
traverseAndRender(root, renderPass, renderTarget);
onAfterRender(renderTarget);
traverse(root, addRenderableItem);

// Render items
if (entities.size() > 0)
{
renderVolumeMask(renderPass, renderTarget);
onBeforeRender(renderTarget);
for (const auto &entity : entities)
{
renderMesh(entity.id,
entity.meshComponent,
entity.materialComponent,
renderPass,
renderTarget);
}
onAfterRender(renderTarget);
}
}

void RenderSystem::renderVolumeMask(RenderPass renderPass, optional<XRRenderTarget> renderTarget)
{
if (renderer_->isVolumeMaskEnabled())
{
renderer_->addVolumeMask([this, &renderPass, &renderTarget](ecs::EntityId entity, SceneRenderer &renderer)
{ renderMesh(entity, getComponent<Mesh3d>(entity), renderPass, renderTarget); });
auto renderVolume = [this, &renderPass, &renderTarget](ecs::EntityId entity, SceneRenderer &renderer)
{
auto meshComponent = getComponent<Mesh3d>(entity);
auto materialComponent = getComponent<MeshMaterial3d>(entity);
renderMesh(entity, meshComponent, materialComponent, renderPass, renderTarget);
};
renderer_->addVolumeMask(renderVolume);
}
}

void RenderSystem::renderMesh(EntityId &entity,
void RenderSystem::renderMesh(const EntityId &entity,
shared_ptr<Mesh3d> meshComponent,
shared_ptr<MeshMaterial3d> materialComponent,
RenderPass renderPass,
optional<XRRenderTarget> renderTarget)
{
auto materialComponent = getComponent<MeshMaterial3d>(entity);
if (TR_UNLIKELY(materialComponent == nullptr))
{
assert(false && "The material component must be valid.");
return;
}

if (!meshComponent->initialized())
renderer_->initializeMesh3d(meshComponent);
if (!materialComponent->initialized())
Expand Down Expand Up @@ -174,29 +211,10 @@ namespace builtin_scene
}
}

void RenderSystem::traverseAndRender(EntityId root, RenderPass renderPass, optional<XRRenderTarget> renderTarget)
{
auto renderEntity = [this, &renderPass, &renderTarget](EntityId entity) -> bool
{
// Render the mesh if it exists
auto mesh = getComponent<Mesh3d>(entity);
if (mesh != nullptr)
{
// If the mesh exists but rendering is disabled, we need to skip its rendering and its children.
if (mesh->isRenderingDisabled())
return false;
renderMesh(entity, mesh, renderPass, renderTarget);
}

// TODO: support other renderable components (e.g., particles, etc.)
return true;
};
return traverse(root, renderEntity);
}

void RenderSystem::traverseAndUpdate(ecs::EntityId root, std::optional<XRRenderTarget> renderTarget)
size_t RenderSystem::traverseAndUpdate(ecs::EntityId root, std::optional<XRRenderTarget> renderTarget)
{
auto updateEntity = [this, &renderTarget](EntityId entity) -> bool
size_t num = 0;
auto updateEntity = [this, &renderTarget, &num](EntityId entity) -> bool
{
auto mesh = getComponent<Mesh3d>(entity);
if (mesh != nullptr)
Expand All @@ -209,10 +227,14 @@ namespace builtin_scene
// TODO(yorkie): update transformation matrix here?
if (mesh->isInstancedMesh())
updateInstancedMeshData(*mesh, renderTarget);

num++;
}
return true; // Continue traversing children
};
return traverse(root, updateEntity);

traverse(root, updateEntity);
return num;
}

glm::mat4 RenderSystem::getTransformationMatrix(ecs::EntityId id)
Expand Down
21 changes: 8 additions & 13 deletions src/client/builtin_scene/client_renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,22 @@ namespace builtin_scene
*
* @param entity The entity to render.
* @param meshComponent The mesh component to render.
* @param materialComponent The mesh material component to use for rendering.
* @param renderPass The render pass to use, which can be used to filter the objects or instances to render.
* @param renderTarget The XR render target.
*/
void renderMesh(ecs::EntityId &, std::shared_ptr<Mesh3d>, RenderPass, std::optional<XRRenderTarget>);
/**
* Execute this before rendering the scene.
*
* @param renderTarget The XR render target.
*/
void renderMesh(const ecs::EntityId &,
std::shared_ptr<Mesh3d>,
std::shared_ptr<MeshMaterial3d>,
const RenderPass,
std::optional<XRRenderTarget>);

void onBeforeRender(std::optional<XRRenderTarget>);
/**
* Execute this after rendering the scene.
*
* @param renderTarget The XR render target.
*/
void onAfterRender(std::optional<XRRenderTarget>);

// Traverse the entity hierarchy in pre-order and execute the given function for each entity.
void traverse(ecs::EntityId root, std::function<bool(ecs::EntityId)> &&);
void traverseAndUpdate(ecs::EntityId root, std::optional<XRRenderTarget>);
void traverseAndRender(ecs::EntityId root, RenderPass, std::optional<XRRenderTarget>);
size_t traverseAndUpdate(ecs::EntityId root, std::optional<XRRenderTarget>);

glm::mat4 getTransformationMatrix(ecs::EntityId);
void updateInstancedMeshData(const Mesh3d &, std::optional<XRRenderTarget>);
Expand Down
2 changes: 1 addition & 1 deletion src/client/builtin_scene/instanced_mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ namespace builtin_scene
InstanceFilter::kTransparent, transparentVao, transparentInstanceVbo);
}

void InstancedMeshBase::updateRenderQueues(bool ignoreDirty)
void InstancedMeshBase::updateInstancesList(bool ignoreDirty)
{
if (!isDirty_ && !ignoreDirty)
return;
Expand Down
4 changes: 2 additions & 2 deletions src/client/builtin_scene/instanced_mesh.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,11 @@ namespace builtin_scene
std::shared_ptr<client_graphics::WebGLVertexArray> transparentVao,
std::shared_ptr<client_graphics::WebGLBuffer> transparentInstanceVao);
/**
* Update the internal `idToInstanceMap_` into the opaque and transparent `RenderableInstancesList` the queues.
* Update the internal `idToInstanceMap_` into the opaque and transparent `RenderableInstancesList`.
*
* @param ignoreDirty Whether to ignore the dirty flag, `true` means force update.
*/
void updateRenderQueues(bool ignoreDirty = false);
void updateInstancesList(bool ignoreDirty = false);

private:
inline void markAsDirty()
Expand Down
4 changes: 4 additions & 0 deletions src/client/builtin_scene/material_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ namespace builtin_scene
Material()
{
}
Material(bool isOpaque)
: isOpaque_(isOpaque)
{
}
virtual ~Material() = default;

public:
Expand Down
3 changes: 1 addition & 2 deletions src/client/builtin_scene/materials/web_content_instanced.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ namespace builtin_scene::materials
using namespace client_graphics;

WebContentInstancedMaterial::WebContentInstancedMaterial()
: Material()
: Material(false)
, width_(0.0f)
, height_(0.0f)
, textureAtlas_(nullptr)
, textureOffset_(0.0f, 0.0f)
, textureScale_(1.0f, 1.0f)
{
this->isOpaque_ = true;
}

bool WebContentInstancedMaterial::initialize(shared_ptr<WebGL2Context> glContext,
Expand Down
27 changes: 24 additions & 3 deletions src/client/builtin_scene/mesh_material.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#pragma once

#include <memory>
#include <cassert>
#include <concepts>
#include <client/graphics/webgl_context.hpp>

#include "./ecs.hpp"
#include "./material_base.hpp"
#include "./renderer/render_pass.hpp"

namespace builtin_scene
{
Expand Down Expand Up @@ -53,13 +55,32 @@ namespace builtin_scene
{
return program_;
}
/**
* @returns Whether the material is opaque.
*/

inline bool isOpaque() const
{
return material_->isOpaque();
}
inline bool isTransparent() const
{
return !isOpaque();
}

/**
* Check if the material matches the given render pass.
*
* @param pass The render pass to check against.
* @returns Whether the material matches the render pass.
*/
inline bool matchesPass(const RenderPass &pass) const
{
assert(pass == RenderPass::kOpaques || pass == RenderPass::kTransparents);
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion duplicates the logic in the if-else conditions below. Consider removing the assertion or simplifying the function to avoid redundant checks.

Suggested change
assert(pass == RenderPass::kOpaques || pass == RenderPass::kTransparents);

Copilot uses AI. Check for mistakes.
if (pass == RenderPass::kOpaques)
return isOpaque();
else if (pass == RenderPass::kTransparents)
return isTransparent();
else
return false;
Comment on lines +79 to +82
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final else clause will never execute due to the assertion above. If the assertion is removed, this should be unreachable code. Consider removing this clause or handling unexpected enum values appropriately.

Suggested change
else if (pass == RenderPass::kTransparents)
return isTransparent();
else
return false;
else // pass == RenderPass::kTransparents
return isTransparent();

Copilot uses AI. Check for mistakes.
}
/**
* Initialize the `MeshMaterial3d` instance with the given WebGL context and program.
*
Expand Down
18 changes: 18 additions & 0 deletions src/client/builtin_scene/renderer/render_pass.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once

#include <string>
#include <ostream>

namespace builtin_scene
{
enum class RenderPass
Expand All @@ -8,4 +11,19 @@ namespace builtin_scene
kTransparents,
kPostProcessing,
};

inline std::ostream &operator<<(std::ostream &os, RenderPass pass)
{
switch (pass)
{
case RenderPass::kOpaques:
return os << "RenderPass(Opaques)";
case RenderPass::kTransparents:
return os << "RenderPass(Transparents)";
case RenderPass::kPostProcessing:
return os << "RenderPass(PostProcessing)";
default:
return os << "RenderPass(Unknown)";
}
}
}
Loading