Lagrange
Loading...
Searching...
No Matches
occluded_sampler_common.h
1/*
2 * Copyright 2026 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12#pragma once
13
14#include <lagrange/Logger.h>
15#include <lagrange/internal/constants.h>
16#include <lagrange/raycasting/RayCaster.h>
17#include <lagrange/scene/SimpleScene.h>
18#include <lagrange/utils/AdjacencyList.h>
19#include <lagrange/utils/geometry3d.h>
20#include <lagrange/utils/range.h>
21#include <lagrange/views.h>
22
23#include <Eigen/Core>
24#include <Eigen/Geometry>
25
26#include <atomic>
27#include <chrono>
28#include <cmath>
29#include <cstddef>
30#include <cstdint>
31#include <numeric>
32#include <optional>
33#include <vector>
34
35namespace lagrange::raycasting::detail {
36
37enum class SamplingMode {
39 Normal,
42 Adaptive,
44 BruteForce,
45};
46
47// See https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
48template <typename Scalar>
49Eigen::Array4<Scalar> generate_vec4(const size_t i)
50{
51 constexpr double phi_1 = 1.1673039782614186842560459;
52 constexpr double phi_2 = phi_1 * phi_1;
53 constexpr double phi_3 = phi_2 * phi_1;
54 constexpr double phi_4 = phi_2 * phi_2;
55 // +0.5 avoids the degenerate (0,0,0,0) sample at i=0.
56 const Eigen::Array4<Scalar> shifted =
57 (static_cast<double>(i) / Eigen::Array4d(phi_1, phi_2, phi_3, phi_4)).cast<Scalar>() +
58 Scalar(0.5);
59 return shifted - shifted.floor();
60}
61
62template <typename Scalar>
63struct RaySampler
64{
65 struct Sample
66 {
67 Eigen::RowVector2<Scalar> bary;
68 Eigen::RowVector3<Scalar> direction;
69 };
70
71 explicit RaySampler(const Eigen::RowVector3<Scalar>& normal)
72 : m_normal(normal)
73 {
74 Eigen::Vector3<Scalar> x_col, y_col;
75 lagrange::orthogonal_frame(Eigen::Vector3<Scalar>(normal.transpose()), x_col, y_col);
76 m_x_axis = x_col.transpose();
77 m_y_axis = y_col.transpose();
78 }
79
80 // Atomic member blocks the implicit move ctor; load explicitly so RaySampler can live in
81 // a std::vector.
82 RaySampler(RaySampler&& other) noexcept
83 : m_normal(other.m_normal)
84 , m_x_axis(other.m_x_axis)
85 , m_y_axis(other.m_y_axis)
86 , m_sample_index(other.m_sample_index.load(std::memory_order_relaxed))
87 {}
88 RaySampler(const RaySampler&) = delete;
89 RaySampler& operator=(const RaySampler&) = delete;
90 RaySampler& operator=(RaySampler&&) = delete;
91
92 const Eigen::RowVector3<Scalar>& get_normal() const { return m_normal; }
93
97 {
98 const auto vec4 =
99 generate_vec4<Scalar>(m_sample_index.fetch_add(1, std::memory_order_relaxed));
100 const Scalar phi = 2 * static_cast<Scalar>(lagrange::internal::pi) * vec4[0];
101 const Scalar r2 = vec4[1];
102 const Scalar r = std::sqrt(r2);
103 const Scalar x = r * std::cos(phi);
104 const Scalar y = r * std::sin(phi);
105 const Scalar z = std::sqrt(Scalar(1) - r2);
106 Sample result;
107 result.direction = x * m_x_axis + y * m_y_axis + z * m_normal;
108 result.bary = vec4.template tail<2>();
109 if (result.bary[0] + result.bary[1] > 1) {
110 // Intentional: direction flip pairs with the bary fold for bi-hemisphere coverage.
111 result.direction = -result.direction;
112 result.bary = Eigen::RowVector2<Scalar>::Ones() - result.bary;
113 }
114 return result;
115 }
116
117 Sample jitter(Scalar jitter_sigma)
118 {
119 auto vec4 = generate_vec4<Scalar>(m_sample_index.fetch_add(1, std::memory_order_relaxed));
120 // Box-Muller via (1 - u1) so that u1 == 0 (at index 0) maps to r == 0, not log(0).
121 const Scalar r = jitter_sigma * std::sqrt(-2 * std::log(1 - vec4[0]));
122 const Scalar phi = 2 * static_cast<Scalar>(lagrange::internal::pi) * vec4[1];
123 Sample result;
124 result.direction = r * (std::cos(phi) * m_x_axis + std::sin(phi) * m_y_axis);
125 result.bary = vec4.template tail<2>();
126 if (result.bary[0] + result.bary[1] > 1) {
127 // Plain bary fold — direction is a relative offset here, no hemisphere to flip.
128 result.bary = Eigen::RowVector2<Scalar>::Ones() - result.bary;
129 }
130 return result;
131 }
132
133private:
134 const Eigen::RowVector3<Scalar> m_normal;
135 Eigen::RowVector3<Scalar> m_x_axis;
136 Eigen::RowVector3<Scalar> m_y_axis;
137 std::atomic<size_t> m_sample_index{0};
138};
139
142template <typename Scalar, typename Vertices, typename Facets, typename Index>
143Eigen::RowVector3<Scalar> barycentric_position(
144 const Eigen::RowVector2<Scalar>& bary,
145 const Vertices& vertices,
146 const Facets& facets,
147 Index f)
148{
149 const Scalar b2 = Scalar(1) - bary(0) - bary(1);
150 return (bary(0) * vertices.col(facets(f, 0)) + bary(1) * vertices.col(facets(f, 1)) +
151 b2 * vertices.col(facets(f, 2)))
152 .transpose();
153}
154
157{
158 static constexpr size_t capacity = 16;
159
160 RayCaster::Point16f origins;
161 RayCaster::Direction16f directions;
162 RayCaster::Float16 tmins;
163 size_t count = 0;
164
165 template <typename Origin, typename Direction>
166 void push(const Origin& origin, const Direction& direction, float tmin = 1e-6f)
167 {
168 origins.row(count) = origin.template cast<float>();
169 directions.row(count) = direction.template cast<float>();
170 tmins[count] = tmin;
171 ++count;
172 }
173
174 bool full() const { return count == capacity; }
175 bool empty() const { return count == 0; }
176 void clear() { count = 0; }
177
178 uint32_t cast(const RayCaster& caster) const
179 {
180 return caster.occluded16(origins, directions, count, tmins);
181 }
182
184 uint32_t occupied_mask() const { return (uint32_t{1} << count) - 1; }
185};
186
188template <typename Scalar, typename Index>
190{
191 Index mesh_index;
192 Eigen::Transform<Scalar, 3, Eigen::Affine> transform;
193 std::vector<Scalar> facet_areas; // world-space
194 std::vector<RaySampler<Scalar>> ray_samplers; // world-space normals
195};
196
198template <typename Scalar, typename Index>
199struct FacetInstanceData : InstanceData<Scalar, Index>
200{
203 std::vector<Scalar> facet_weights;
204 std::vector<Index> active_local_facets;
205
208 std::optional<AdjacencyList<Index>> facet_neighbors;
209
213 std::vector<std::optional<Eigen::Vector3<Scalar>>> facet_escape_directions;
216 std::vector<std::optional<Eigen::Vector3<Scalar>>> facet_escape_snapshot;
217};
218
219template <typename Derived, typename Scalar, typename Index>
221{
223 RayCaster m_ray_caster;
224
225 Derived& derived() { return static_cast<Derived&>(*this); }
226 const Derived& derived() const { return static_cast<const Derived&>(*this); }
227
228 size_t num_instances() const { return derived().m_instances.size(); }
229 Index mesh_index(size_t i) const { return derived().m_instances[i].mesh_index; }
230 const auto& instance_transform(size_t i) const { return derived().m_instances[i].transform; }
231
232 Scalar compute_total_active_weight() const
233 {
234 const auto r = lagrange::range(num_instances());
235 return std::accumulate(r.begin(), r.end(), Scalar(0), [&](Scalar s, size_t i) {
236 return s + derived().instance_active_weight(i);
237 });
238 }
239
240 void run_batch(uint64_t num_rays)
241 {
242 const Scalar total = compute_total_active_weight();
243 if (total <= 0) return;
244
245 const auto t_start = std::chrono::steady_clock::now();
246
247 for (auto i : lagrange::range(num_instances())) {
248 const Scalar inst_weight = derived().instance_active_weight(i);
249 if (inst_weight <= 0) continue;
250
251 const uint64_t inst_rays = static_cast<uint64_t>(
252 std::round(static_cast<double>(num_rays) * inst_weight / total));
253 if (inst_rays == 0) continue;
254
255 const auto& mesh = m_scene.get_mesh(mesh_index(i));
256 const auto vertices =
257 (instance_transform(i) * vertex_view(mesh).transpose().template topRows<3>())
258 .eval();
259 const auto facets = facet_view(mesh);
260
261 derived().process_instance(i, inst_rays, inst_weight, vertices, facets);
262 }
263
264 derived().end_batch();
265
266 const auto t_end = std::chrono::steady_clock::now();
267 const auto batch_ms =
268 std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count();
269 lagrange::logger().info("Batch: {} ms", batch_ms);
270 }
271};
272
274template <typename Sampler, typename Index>
275std::pair<uint64_t, uint64_t> sum_progress(const Sampler& sampler, Index count)
276{
277 uint64_t num_visible = 0;
278 uint64_t total_rays = 0;
279 for (Index i = 0; i < count; ++i) {
280 if (sampler.is_visible(i)) ++num_visible;
281 total_rays += sampler.num_rays_cast(i);
282 }
283 return {num_visible, total_rays};
284}
285
286} // namespace lagrange::raycasting::detail
A ray caster built on top of Embree that operates directly on SurfaceMesh and SimpleScene objects.
Definition RayCaster.h:196
uint32_t occluded16(const Point16f &origins, const Direction16f &directions, const std::variant< Mask16, size_t > &active=size_t(16), const Float16 &tmin=Float16::Zero(), const Float16 &tmax=Float16::Constant(std::numeric_limits< float >::infinity())) const
Test a packet of up to 16 rays for occlusion.
Definition RayCaster.cpp:1640
Simple scene container for instanced meshes.
Definition SimpleScene.h:62
LA_CORE_API spdlog::logger & logger()
Retrieves the current logger.
Definition Logger.cpp:40
@ Scalar
Mesh attribute must have exactly 1 channel.
Definition AttributeFwd.h:56
SurfaceMesh< ToScalar, ToIndex > cast(const SurfaceMesh< FromScalar, FromIndex > &source_mesh, const AttributeFilter &convertible_attributes={}, std::vector< std::string > *converted_attributes_names=nullptr)
Cast a mesh to a mesh of different scalar and/or index type.
ConstRowMatrixView< Scalar > vertex_view(const SurfaceMesh< Scalar, Index > &mesh)
Returns a read-only view of the mesh vertices in the form of an Eigen matrix.
Definition views.cpp:156
ConstRowMatrixView< Index > facet_view(const SurfaceMesh< Scalar, Index > &mesh)
Returns a read-only view of a mesh facets in the form of an Eigen matrix.
Definition views.cpp:170
internal::Range< Index > range(Index end)
Returns an iterable object representing the range [0, end).
Definition range.h:176
void orthogonal_frame(const Eigen::Matrix< Scalar, 3, 1 > &x, Eigen::Matrix< Scalar, 3, 1 > &y, Eigen::Matrix< Scalar, 3, 1 > &z)
Build an orthogonal frame given a single vector.
Definition geometry3d.h:124
Per-instance data for OccludedFacetSampler; gates each facet individually.
Definition occluded_sampler_common.h:200
std::vector< Scalar > facet_weights
Per-facet ray-distribution weight = cbrt(area).
Definition occluded_sampler_common.h:203
std::vector< std::optional< Eigen::Vector3< Scalar > > > facet_escape_snapshot
Pre-batch snapshot of facet_escape_directions.
Definition occluded_sampler_common.h:216
std::vector< std::optional< Eigen::Vector3< Scalar > > > facet_escape_directions
World-space unit-length escape direction recorded the first time each facet becomes visible; std::nul...
Definition occluded_sampler_common.h:213
std::optional< AdjacencyList< Index > > facet_neighbors
1-ring edge-neighbors (mesh dual graph).
Definition occluded_sampler_common.h:208
Definition occluded_sampler_common.h:221
Per-instance precomputation shared by both samplers.
Definition occluded_sampler_common.h:190
16-ray SIMD packet for RayCaster::occluded16. push widens to float on entry.
Definition occluded_sampler_common.h:157
uint32_t occupied_mask() const
Low count bits set — masks off undefined slots in a partial packet's cast mask.
Definition occluded_sampler_common.h:184
Definition occluded_sampler_common.h:66
Definition occluded_sampler_common.h:64
Sample operator()()
Bi-hemisphere sample via Malley's method: when bary reflection fires (u+v > 1), the direction is also...
Definition occluded_sampler_common.h:96