From d99e93527ad6ce1f1260fda344bd0623a1b3cf42 Mon Sep 17 00:00:00 2001
From: Fabio Pellacini <fabio.pellacini@gmail.com>
Date: Wed, 31 Jan 2024 09:09:30 +0100
Subject: [PATCH] updated

---
 libs/yocto/yocto_math.h     | 214 ++++++++++
 libs/yocto/yocto_modeling.h | 121 ------
 libs/yocto/yocto_sampling.h |  53 ---
 libs/yocto/yocto_scene.cpp  | 619 +++++++++++++++++++++++++++
 libs/yocto/yocto_scene.h    | 145 +++++++
 libs/yocto/yocto_shape.h    | 805 ------------------------------------
 6 files changed, 978 insertions(+), 979 deletions(-)

diff --git a/libs/yocto/yocto_math.h b/libs/yocto/yocto_math.h
index 77c78bfd9..31184f64f 100644
--- a/libs/yocto/yocto_math.h
+++ b/libs/yocto/yocto_math.h
@@ -1188,6 +1188,76 @@ inline bbox3f capsule_bounds(vec3f p0, vec3f p1, float r0, float r1);
 
 }  // namespace yocto
 
+// -----------------------------------------------------------------------------
+// GEOMETRY UTILITIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Line properties.
+inline vec3f line_tangent(vec3f p0, vec3f p1);
+inline float line_length(vec3f p0, vec3f p1);
+
+// Triangle properties.
+inline vec3f triangle_normal(vec3f p0, vec3f p1, vec3f p2);
+inline float triangle_area(vec3f p0, vec3f p1, vec3f p2);
+
+// Quad properties.
+inline vec3f quad_normal(vec3f p0, vec3f p1, vec3f p2, vec3f p3);
+inline float quad_area(vec3f p0, vec3f p1, vec3f p2, vec3f p3);
+
+// Interpolates values over a line parameterized from a to b by u. Same as lerp.
+template <typename T>
+inline T interpolate_line(const T& p0, const T& p1, float u);
+
+// Interpolates values over a triangle parameterized by u and v along the
+// (p1-p0) and (p2-p0) directions. Same as barycentric interpolation.
+template <typename T>
+inline T interpolate_triangle(const T& p0, const T& p1, const T& p2, vec2f uv);
+
+// Interpolates values over a quad parameterized by u and v along the
+// (p1-p0) and (p2-p1) directions. Same as bilinear interpolation.
+template <typename T>
+inline T interpolate_quad(
+    const T& p0, const T& p1, const T& p2, const T& p3, vec2f uv);
+
+// Interpolates values along a cubic Bezier segment parametrized by u.
+template <typename T>
+inline T interpolate_bezier(
+    const T& p0, const T& p1, const T& p2, const T& p3, float u);
+
+// Computes the derivative of a cubic Bezier segment parametrized by u.
+template <typename T>
+inline T interpolate_bezier_derivative(
+    const T& p0, const T& p1, const T& p2, const T& p3, float u);
+
+// Interpolated line properties.
+inline vec3f line_point(vec3f p0, vec3f p1, float u);
+inline vec3f line_tangent(vec3f t0, vec3f t1, float u);
+
+// Interpolated triangle properties.
+inline vec3f triangle_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv);
+inline vec3f triangle_normal(vec3f n0, vec3f n1, vec3f n2, vec2f uv);
+
+// Interpolated quad properties.
+inline vec3f quad_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv);
+inline vec3f quad_normal(vec3f n0, vec3f n1, vec3f n2, vec3f n3, vec2f uv);
+
+// Interpolated sphere properties.
+inline vec3f sphere_point(const vec3f p, float r, vec2f uv);
+inline vec3f sphere_normal(const vec3f p, float r, vec2f uv);
+
+// Triangle tangent and bitangent from uv
+inline pair<vec3f, vec3f> triangle_tangents_fromuv(
+    vec3f p0, vec3f p1, vec3f p2, vec2f uv0, vec2f uv1, vec2f uv2);
+
+// Quad tangent and bitangent from uv. Note that we pass a current_uv since
+// internally we may want to split the quad in two and we need to known where
+// to do it. If not interested in the split, just pass vec2f{0,0} here.
+inline pair<vec3f, vec3f> quad_tangents_fromuv(vec3f p0, vec3f p1, vec3f p2,
+    vec3f p3, vec2f uv0, vec2f uv1, vec2f uv2, vec2f uv3, vec2f current_uv);
+
+}  // namespace yocto
+
 // -----------------------------------------------------------------------------
 // USER INTERFACE UTILITIES
 // -----------------------------------------------------------------------------
@@ -2799,6 +2869,150 @@ inline bbox3f capsule_bounds(vec3f p0, vec3f p1, float r0, float r1) {
 
 }  // namespace yocto
 
+// -----------------------------------------------------------------------------
+// GEOMETRY UTILITIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Line properties.
+inline vec3f line_tangent(vec3f p0, vec3f p1) { return normalize(p1 - p0); }
+inline float line_length(vec3f p0, vec3f p1) { return length(p1 - p0); }
+
+// Triangle properties.
+inline vec3f triangle_normal(vec3f p0, vec3f p1, vec3f p2) {
+  return normalize(cross(p1 - p0, p2 - p0));
+}
+inline float triangle_area(vec3f p0, vec3f p1, vec3f p2) {
+  return length(cross(p1 - p0, p2 - p0)) / 2;
+}
+
+// Quad propeties.
+inline vec3f quad_normal(vec3f p0, vec3f p1, vec3f p2, vec3f p3) {
+  return normalize(triangle_normal(p0, p1, p3) + triangle_normal(p2, p3, p1));
+}
+inline float quad_area(vec3f p0, vec3f p1, vec3f p2, vec3f p3) {
+  return triangle_area(p0, p1, p3) + triangle_area(p2, p3, p1);
+}
+
+// Interpolates values over a line parameterized from a to b by u. Same as lerp.
+template <typename T>
+inline T interpolate_line(const T& p0, const T& p1, float u) {
+  return p0 * (1 - u) + p1 * u;
+}
+
+// Interpolates values over a triangle parameterized by u and v along the
+// (p1-p0) and (p2-p0) directions. Same as barycentric interpolation.
+template <typename T>
+inline T interpolate_triangle(const T& p0, const T& p1, const T& p2, vec2f uv) {
+  return p0 * (1 - uv.x - uv.y) + p1 * uv.x + p2 * uv.y;
+}
+
+// Interpolates values over a quad parameterized by u and v along the
+// (p1-p0) and (p2-p1) directions. Same as bilinear interpolation.
+template <typename T>
+inline T interpolate_quad(
+    const T& p0, const T& p1, const T& p2, const T& p3, vec2f uv) {
+  if (uv.x + uv.y <= 1) {
+    return interpolate_triangle(p0, p1, p3, uv);
+  } else {
+    return interpolate_triangle(p2, p3, p1, 1 - uv);
+  }
+}
+
+// Interpolates values along a cubic Bezier segment parametrized by u.
+template <typename T>
+inline T interpolate_bezier(
+    const T& p0, const T& p1, const T& p2, const T& p3, float u) {
+  return p0 * (1 - u) * (1 - u) * (1 - u) + p1 * 3 * u * (1 - u) * (1 - u) +
+         p2 * 3 * u * u * (1 - u) + p3 * u * u * u;
+}
+// Computes the derivative of a cubic Bezier segment parametrized by u.
+template <typename T>
+inline T interpolate_bezier_derivative(
+    const T& p0, const T& p1, const T& p2, const T& p3, float u) {
+  return (p1 - p0) * 3 * (1 - u) * (1 - u) + (p2 - p1) * 6 * u * (1 - u) +
+         (p3 - p2) * 3 * u * u;
+}
+
+// Interpolated line properties.
+inline vec3f line_point(vec3f p0, vec3f p1, float u) {
+  return p0 * (1 - u) + p1 * u;
+}
+inline vec3f line_tangent(vec3f t0, vec3f t1, float u) {
+  return normalize(t0 * (1 - u) + t1 * u);
+}
+
+// Interpolated triangle properties.
+inline vec3f triangle_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv) {
+  return p0 * (1 - uv.x - uv.y) + p1 * uv.x + p2 * uv.y;
+}
+inline vec3f triangle_normal(vec3f n0, vec3f n1, vec3f n2, vec2f uv) {
+  return normalize(n0 * (1 - uv.x - uv.y) + n1 * uv.x + n2 * uv.y);
+}
+
+// Interpolated quad properties.
+inline vec3f quad_point(vec3f p0, vec3f p1, vec3f p2, vec3f p3, vec2f uv) {
+  if (uv.x + uv.y <= 1) {
+    return triangle_point(p0, p1, p3, uv);
+  } else {
+    return triangle_point(p2, p3, p1, 1 - uv);
+  }
+}
+inline vec3f quad_normal(vec3f n0, vec3f n1, vec3f n2, vec3f n3, vec2f uv) {
+  if (uv.x + uv.y <= 1) {
+    return triangle_normal(n0, n1, n3, uv);
+  } else {
+    return triangle_normal(n2, n3, n1, 1 - uv);
+  }
+}
+
+// Interpolated sphere properties.
+inline vec3f sphere_point(const vec3f p, float r, vec2f uv) {
+  return p + r * vec3f{cos(uv.x * 2 * pif) * sin(uv.y * pif),
+                     sin(uv.x * 2 * pif) * sin(uv.y * pif), cos(uv.y * pif)};
+}
+inline vec3f sphere_normal(const vec3f p, float r, vec2f uv) {
+  return normalize(vec3f{cos(uv.x * 2 * pif) * sin(uv.y * pif),
+      sin(uv.x * 2 * pif) * sin(uv.y * pif), cos(uv.y * pif)});
+}
+
+// Triangle tangent and bitangent from uv
+inline pair<vec3f, vec3f> triangle_tangents_fromuv(
+    vec3f p0, vec3f p1, vec3f p2, vec2f uv0, vec2f uv1, vec2f uv2) {
+  // Follows the definition in http://www.terathon.com/code/tangent.html and
+  // https://gist.github.com/aras-p/2843984
+  // normal points up from texture space
+  auto p   = p1 - p0;
+  auto q   = p2 - p0;
+  auto s   = vec2f{uv1.x - uv0.x, uv2.x - uv0.x};
+  auto t   = vec2f{uv1.y - uv0.y, uv2.y - uv0.y};
+  auto div = s.x * t.y - s.y * t.x;
+
+  if (div != 0) {
+    auto tu = vec3f{t.y * p.x - t.x * q.x, t.y * p.y - t.x * q.y,
+                  t.y * p.z - t.x * q.z} /
+              div;
+    auto tv = vec3f{s.x * q.x - s.y * p.x, s.x * q.y - s.y * p.y,
+                  s.x * q.z - s.y * p.z} /
+              div;
+    return {tu, tv};
+  } else {
+    return {{1, 0, 0}, {0, 1, 0}};
+  }
+}
+
+// Quad tangent and bitangent from uv.
+inline pair<vec3f, vec3f> quad_tangents_fromuv(vec3f p0, vec3f p1, vec3f p2,
+    vec3f p3, vec2f uv0, vec2f uv1, vec2f uv2, vec2f uv3, vec2f current_uv) {
+  if (current_uv.x + current_uv.y <= 1) {
+    return triangle_tangents_fromuv(p0, p1, p3, uv0, uv1, uv3);
+  } else {
+    return triangle_tangents_fromuv(p2, p3, p1, uv2, uv3, uv1);
+  }
+}
+
+}  // namespace yocto
+
 // -----------------------------------------------------------------------------
 // USER INTERFACE UTILITIES
 // -----------------------------------------------------------------------------
diff --git a/libs/yocto/yocto_modeling.h b/libs/yocto/yocto_modeling.h
index 60def9244..4fcf7a90a 100644
--- a/libs/yocto/yocto_modeling.h
+++ b/libs/yocto/yocto_modeling.h
@@ -332,13 +332,6 @@ static pair<vector<vec4i>, vector<T>> subdivide_catmullclark_creased(
     const vector<vec2i>& creases, const vector<int>& corners,
     int subdivisions = 1, bool lock_boundary = false);
 
-// Shape subdivision
-inline shape_data subdivide_shape(
-    const shape_data& shape, int subdivisions, bool catmullclark);
-
-inline fvshape_data subdivide_fvshape(
-    const fvshape_data& shape, int subdivisions, bool catmullclark);
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
@@ -356,12 +349,6 @@ inline vector<vec3f> displace_vertices(const vector<vec3f>& positions,
     const image_t<vec4f>& displacement, float scale = 1.0f,
     float offset = 0.5f);
 
-// Shape displacement
-inline shape_data displace_shape(const shape_data& shape,
-    const image_t<float>& displacement, float height = 1, float offset = 0.5f);
-inline shape_data displace_shape(const shape_data& shape,
-    const image_t<vec4f>& displacement, float height = 1, float offset = 0.5f);
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
@@ -2250,90 +2237,6 @@ static pair<vector<vec4i>, vector<T>> subdivide_catmullclark_illustration(
   return {quads, vertices};
 }
 
-// Shape subdivision
-inline shape_data subdivide_shape(
-    const shape_data& shape, int subdivisions, bool catmullclark) {
-  // This should probably be re-implemented in a faster fashion,
-  // but how it is not obvious
-  if (subdivisions == 0) return shape;
-  auto subdivided = shape_data{};
-  if (!subdivided.points.empty()) {
-    subdivided = shape;
-  } else if (!subdivided.lines.empty()) {
-    std::tie(std::ignore, subdivided.normals) = subdivide_lines(
-        shape.lines, shape.normals, subdivisions);
-    std::tie(std::ignore, subdivided.texcoords) = subdivide_lines(
-        shape.lines, shape.texcoords, subdivisions);
-    std::tie(std::ignore, subdivided.colors) = subdivide_lines(
-        shape.lines, shape.colors, subdivisions);
-    std::tie(std::ignore, subdivided.radius) = subdivide_lines(
-        subdivided.lines, shape.radius, subdivisions);
-    std::tie(subdivided.lines, subdivided.positions) = subdivide_lines(
-        shape.lines, shape.positions, subdivisions);
-  } else if (!subdivided.triangles.empty()) {
-    std::tie(std::ignore, subdivided.normals) = subdivide_triangles(
-        shape.triangles, shape.normals, subdivisions);
-    std::tie(std::ignore, subdivided.texcoords) = subdivide_triangles(
-        shape.triangles, shape.texcoords, subdivisions);
-    std::tie(std::ignore, subdivided.colors) = subdivide_triangles(
-        shape.triangles, shape.colors, subdivisions);
-    std::tie(std::ignore, subdivided.radius) = subdivide_triangles(
-        shape.triangles, shape.radius, subdivisions);
-    std::tie(subdivided.triangles, subdivided.positions) = subdivide_triangles(
-        shape.triangles, shape.positions, subdivisions);
-  } else if (!subdivided.quads.empty() && !catmullclark) {
-    std::tie(std::ignore, subdivided.normals) = subdivide_quads(
-        shape.quads, shape.normals, subdivisions);
-    std::tie(std::ignore, subdivided.texcoords) = subdivide_quads(
-        shape.quads, shape.texcoords, subdivisions);
-    std::tie(std::ignore, subdivided.colors) = subdivide_quads(
-        shape.quads, shape.colors, subdivisions);
-    std::tie(std::ignore, subdivided.radius) = subdivide_quads(
-        shape.quads, shape.radius, subdivisions);
-    std::tie(subdivided.quads, subdivided.positions) = subdivide_quads(
-        shape.quads, shape.positions, subdivisions);
-  } else if (!subdivided.quads.empty() && catmullclark) {
-    std::tie(std::ignore, subdivided.normals) = subdivide_catmullclark(
-        shape.quads, shape.normals, subdivisions);
-    std::tie(std::ignore, subdivided.texcoords) = subdivide_catmullclark(
-        shape.quads, shape.texcoords, subdivisions);
-    std::tie(std::ignore, subdivided.colors) = subdivide_catmullclark(
-        shape.quads, shape.colors, subdivisions);
-    std::tie(std::ignore, subdivided.radius) = subdivide_catmullclark(
-        shape.quads, shape.radius, subdivisions);
-    std::tie(subdivided.quads, subdivided.positions) = subdivide_catmullclark(
-        shape.quads, shape.positions, subdivisions);
-  } else {
-    // empty shape
-  }
-  return subdivided;
-}
-
-// Subdivision
-inline fvshape_data subdivide_fvshape(
-    const fvshape_data& shape, int subdivisions, bool catmullclark) {
-  // This should be probably re-implemeneted in a faster fashion.
-  if (subdivisions == 0) return shape;
-  auto subdivided = fvshape_data{};
-  if (!catmullclark) {
-    std::tie(subdivided.quadspos, subdivided.positions) = subdivide_quads(
-        shape.quadspos, shape.positions, subdivisions);
-    std::tie(subdivided.quadsnorm, subdivided.normals) = subdivide_quads(
-        shape.quadsnorm, shape.normals, subdivisions);
-    std::tie(subdivided.quadstexcoord, subdivided.texcoords) = subdivide_quads(
-        shape.quadstexcoord, shape.texcoords, subdivisions);
-  } else {
-    std::tie(subdivided.quadspos, subdivided.positions) =
-        subdivide_catmullclark(shape.quadspos, shape.positions, subdivisions);
-    std::tie(subdivided.quadsnorm, subdivided.normals) = subdivide_catmullclark(
-        shape.quadsnorm, shape.normals, subdivisions);
-    std::tie(subdivided.quadstexcoord, subdivided.texcoords) =
-        subdivide_catmullclark(
-            shape.quadstexcoord, shape.texcoords, subdivisions, true);
-  }
-  return subdivided;
-}
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
@@ -2367,30 +2270,6 @@ inline vector<vec3f> displace_vertices(const vector<vec3f>& positions,
   return displaced;
 }
 
-// Displacement
-inline shape_data displace_shape(const shape_data& shape,
-    const image_t<float>& displacement, float height, float offset) {
-  if (displacement.empty() || shape.texcoords.empty() ||
-      shape.normals.empty() || (shape.triangles.empty() && shape.quads.empty()))
-    return shape;
-  auto displaced      = shape;
-  displaced.positions = displace_vertices(displaced.positions,
-      displaced.normals, displaced.texcoords, displacement, height, offset);
-  displaced.normals   = compute_normals(displaced);
-  return displaced;
-}
-inline shape_data displace_shape(const shape_data& shape,
-    const image_t<vec4f>& displacement, float height, float offset) {
-  if (displacement.empty() || shape.texcoords.empty() ||
-      shape.normals.empty() || (shape.triangles.empty() && shape.quads.empty()))
-    return shape;
-  auto displaced      = shape;
-  displaced.positions = displace_vertices(displaced.positions,
-      displaced.normals, displaced.texcoords, displacement, height, offset);
-  displaced.normals   = compute_normals(displaced);
-  return displaced;
-}
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
diff --git a/libs/yocto/yocto_sampling.h b/libs/yocto/yocto_sampling.h
index 22d388eb1..831076739 100644
--- a/libs/yocto/yocto_sampling.h
+++ b/libs/yocto/yocto_sampling.h
@@ -165,13 +165,6 @@ inline pair<int, vec2f> sample_quads(
 inline vector<float> sample_quads_cdf(
     const vector<vec4i>& quads, const vector<vec3f>& positions);
 
-// Shape sampling
-vector<float>    sample_shape_cdf(const shape_data& shape);
-pair<int, vec2f> sample_shape(
-    const shape_data& shape, const vector<float>& cdf, float rn, vec2f ruv);
-vector<pair<int, vec2f>> sample_shape(
-    const shape_data& shape, int num_samples, uint64_t seed = 98729387);
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
@@ -409,52 +402,6 @@ inline vector<float> sample_quads_cdf(
   return cdf;
 }
 
-// Shape sampling
-inline vector<float> sample_shape_cdf(const shape_data& shape) {
-  if (!shape.points.empty()) {
-    return sample_points_cdf((int)shape.points.size());
-  } else if (!shape.lines.empty()) {
-    return sample_lines_cdf(shape.lines, shape.positions);
-  } else if (!shape.triangles.empty()) {
-    return sample_triangles_cdf(shape.triangles, shape.positions);
-  } else if (!shape.quads.empty()) {
-    return sample_quads_cdf(shape.quads, shape.positions);
-  } else {
-    return sample_points_cdf((int)shape.positions.size());
-  }
-}
-
-inline pair<int, vec2f> sample_shape(
-    const shape_data& shape, const vector<float>& cdf, float rn, vec2f ruv) {
-  if (!shape.points.empty()) {
-    auto element = sample_points(cdf, rn);
-    return {element, {0, 0}};
-  } else if (!shape.lines.empty()) {
-    auto [element, u] = sample_lines(cdf, rn, ruv.x);
-    return {element, {u, 0}};
-  } else if (!shape.triangles.empty()) {
-    auto [element, uv] = sample_triangles(cdf, rn, ruv);
-    return {element, uv};
-  } else if (!shape.quads.empty()) {
-    auto [element, uv] = sample_quads(cdf, rn, ruv);
-    return {element, uv};
-  } else {
-    auto element = sample_points(cdf, rn);
-    return {element, {0, 0}};
-  }
-}
-
-inline vector<pair<int, vec2f>> sample_shape(
-    const shape_data& shape, int num_samples, uint64_t seed) {
-  auto cdf    = sample_shape_cdf(shape);
-  auto points = vector<pair<int, vec2f>>(num_samples);
-  auto rng    = make_rng(seed);
-  for (auto& point : points) {
-    point = sample_shape(shape, cdf, rand1f(rng), rand2f(rng));
-  }
-  return points;
-}
-
 }  // namespace yocto
 
 // -----------------------------------------------------------------------------
diff --git a/libs/yocto/yocto_scene.cpp b/libs/yocto/yocto_scene.cpp
index f678e17f7..582c42aa2 100644
--- a/libs/yocto/yocto_scene.cpp
+++ b/libs/yocto/yocto_scene.cpp
@@ -107,6 +107,625 @@ ray3f eval_camera(const camera_data& camera, vec2f image_uv, vec2f lens_uv) {
 
 }  // namespace yocto
 
+// -----------------------------------------------------------------------------
+// SHAPE FUNCTIONS
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Shape creation
+template <typename PFunc, typename TFunc>
+inline shape_data make_lines(int steps, PFunc&& position, TFunc&& tangent) {
+  auto shape      = shape_data{};
+  shape.positions = vector<vec3f>(steps + 1);
+  shape.normals   = vector<vec3f>(steps + 1);
+  shape.texcoords = vector<vec2f>(steps + 1);
+  for (auto idx : range(steps + 1)) {
+    auto u               = (float)idx / (float)steps;
+    shape.positions[idx] = position(u);
+    shape.normals[idx]   = tangent(u);
+    shape.texcoords[idx] = {u, 0};
+  }
+  shape.lines = vector<vec2i>(steps);
+  for (auto idx : range(steps)) shape.lines[idx] = {idx, idx + 1};
+  return shape;
+}
+template <typename PFunc, typename NFunc>
+inline shape_data make_quads(vec2i steps, PFunc&& position, NFunc&& normal) {
+  auto shape      = shape_data{};
+  shape.positions = vector<vec3f>((steps.x + 1) * (steps.y + 1));
+  shape.normals   = vector<vec3f>((steps.x + 1) * (steps.y + 1));
+  shape.texcoords = vector<vec2f>((steps.x + 1) * (steps.y + 1));
+  for (auto j : range(steps.y + 1)) {
+    for (auto i : range(steps.x + 1)) {
+      auto uv              = vec2f{i / (float)steps.x, j / (float)steps.y};
+      auto idx             = j * (steps.x + 1) + i;
+      shape.positions[idx] = position(uv);
+      shape.normals[idx]   = normal(uv);
+      shape.texcoords[idx] = uv;
+    }
+  }
+  shape.quads = vector<vec4i>(steps.x * steps.y);
+  for (auto j : range(steps.y)) {
+    for (auto i : range(steps.x)) {
+      auto idx         = j * steps.x + i;
+      shape.quads[idx] = {j * (steps.x + 1) + i, j * (steps.x + 1) + i + 1,
+          (j + 1) * (steps.x + 1) + i + 1, (j + 1) * (steps.x + 1) + i};
+    }
+  }
+  return shape;
+}
+
+}  // namespace yocto
+
+// -----------------------------------------------------------------------------
+// IMPLEMENTATION FO SHAPE PROPERTIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Interpolate vertex data
+vec3f eval_position(const shape_data& shape, int element, vec2f uv) {
+  if (!shape.points.empty()) {
+    auto& point = shape.points[element];
+    return shape.positions[point];
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return interpolate_line(
+        shape.positions[line.x], shape.positions[line.y], uv.x);
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return interpolate_triangle(shape.positions[triangle.x],
+        shape.positions[triangle.y], shape.positions[triangle.z], uv);
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return interpolate_quad(shape.positions[quad.x], shape.positions[quad.y],
+        shape.positions[quad.z], shape.positions[quad.w], uv);
+  } else {
+    return {0, 0, 0};
+  }
+}
+
+vec3f eval_normal(const shape_data& shape, int element, vec2f uv) {
+  if (shape.normals.empty()) return eval_element_normal(shape, element);
+  if (!shape.points.empty()) {
+    auto& point = shape.points[element];
+    return normalize(shape.normals[point]);
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return normalize(
+        interpolate_line(shape.normals[line.x], shape.normals[line.y], uv.x));
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return normalize(interpolate_triangle(shape.normals[triangle.x],
+        shape.normals[triangle.y], shape.normals[triangle.z], uv));
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return normalize(
+        interpolate_quad(shape.normals[quad.x], shape.normals[quad.y],
+            shape.normals[quad.z], shape.normals[quad.w], uv));
+  } else {
+    return {0, 0, 1};
+  }
+}
+
+vec3f eval_tangent(const shape_data& shape, int element, vec2f uv) {
+  return eval_normal(shape, element, uv);
+}
+
+vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv) {
+  if (shape.texcoords.empty()) return uv;
+  if (!shape.points.empty()) {
+    auto& point = shape.points[element];
+    return shape.texcoords[point];
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return interpolate_line(
+        shape.texcoords[line.x], shape.texcoords[line.y], uv.x);
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return interpolate_triangle(shape.texcoords[triangle.x],
+        shape.texcoords[triangle.y], shape.texcoords[triangle.z], uv);
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return interpolate_quad(shape.texcoords[quad.x], shape.texcoords[quad.y],
+        shape.texcoords[quad.z], shape.texcoords[quad.w], uv);
+  } else {
+    return uv;
+  }
+}
+
+vec4f eval_color(const shape_data& shape, int element, vec2f uv) {
+  if (shape.colors.empty()) return {1, 1, 1, 1};
+  if (!shape.points.empty()) {
+    auto& point = shape.points[element];
+    return shape.colors[point];
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return interpolate_line(shape.colors[line.x], shape.colors[line.y], uv.x);
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return interpolate_triangle(shape.colors[triangle.x],
+        shape.colors[triangle.y], shape.colors[triangle.z], uv);
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return interpolate_quad(shape.colors[quad.x], shape.colors[quad.y],
+        shape.colors[quad.z], shape.colors[quad.w], uv);
+  } else {
+    return {1, 1, 1, 1};
+  }
+}
+
+float eval_radius(const shape_data& shape, int element, vec2f uv) {
+  if (shape.radius.empty()) return 0;
+  if (!shape.points.empty()) {
+    auto& point = shape.points[element];
+    return shape.radius[point];
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return interpolate_line(shape.radius[line.x], shape.radius[line.y], uv.x);
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return interpolate_triangle(shape.radius[triangle.x],
+        shape.radius[triangle.y], shape.radius[triangle.z], uv);
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return interpolate_quad(shape.radius[quad.x], shape.radius[quad.y],
+        shape.radius[quad.z], shape.radius[quad.w], uv);
+  } else {
+    return 0;
+  }
+}
+
+// Evaluate element normals
+vec3f eval_element_normal(const shape_data& shape, int element) {
+  if (!shape.points.empty()) {
+    return {0, 0, 1};
+  } else if (!shape.lines.empty()) {
+    auto& line = shape.lines[element];
+    return line_tangent(shape.positions[line.x], shape.positions[line.y]);
+  } else if (!shape.triangles.empty()) {
+    auto& triangle = shape.triangles[element];
+    return triangle_normal(shape.positions[triangle.x],
+        shape.positions[triangle.y], shape.positions[triangle.z]);
+  } else if (!shape.quads.empty()) {
+    auto& quad = shape.quads[element];
+    return quad_normal(shape.positions[quad.x], shape.positions[quad.y],
+        shape.positions[quad.z], shape.positions[quad.w]);
+  } else {
+    return {0, 0, 0};
+  }
+}
+
+// Compute per-vertex normals/tangents for lines/triangles/quads.
+vector<vec3f> compute_normals(const shape_data& shape) {
+  if (!shape.points.empty()) {
+    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
+  } else if (!shape.lines.empty()) {
+    return lines_tangents(shape.lines, shape.positions);
+  } else if (!shape.triangles.empty()) {
+    return triangles_normals(shape.triangles, shape.positions);
+  } else if (!shape.quads.empty()) {
+    return quads_normals(shape.quads, shape.positions);
+  } else {
+    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
+  }
+}
+void compute_normals(vector<vec3f>& normals, const shape_data& shape) {
+  if (!shape.points.empty()) {
+    normals.assign(shape.positions.size(), {0, 0, 1});
+  } else if (!shape.lines.empty()) {
+    lines_tangents(normals, shape.lines, shape.positions);
+  } else if (!shape.triangles.empty()) {
+    triangles_normals(normals, shape.triangles, shape.positions);
+  } else if (!shape.quads.empty()) {
+    quads_normals(normals, shape.quads, shape.positions);
+  } else {
+    normals.assign(shape.positions.size(), {0, 0, 1});
+  }
+}
+
+// Conversions
+shape_data quads_to_triangles(const shape_data& shape) {
+  auto result = shape;
+  if (!shape.quads.empty()) {
+    result.triangles = quads_to_triangles(shape.quads);
+    result.quads     = {};
+  }
+  return result;
+}
+void quads_to_triangles_inplace(shape_data& shape) {
+  if (shape.quads.empty()) return;
+  shape.triangles = quads_to_triangles(shape.quads);
+  shape.quads     = {};
+}
+
+// Transform shape
+shape_data transform_shape(
+    const shape_data& shape, const frame3f& frame, bool non_rigid) {
+  return transform_shape(shape, frame, 1, non_rigid);
+}
+shape_data transform_shape(const shape_data& shape, const frame3f& frame,
+    float radius_scale, bool non_rigid) {
+  auto transformed = shape;
+  for (auto& position : transformed.positions)
+    position = transform_point(frame, position);
+  for (auto& normal : transformed.normals)
+    normal = transform_normal(frame, normal, non_rigid);
+  for (auto& radius : transformed.radius) radius *= radius_scale;
+  return transformed;
+}
+shape_data scale_shape(const shape_data& shape, float scale, float uvscale) {
+  if (scale == 1 && uvscale == 1) return shape;
+  auto transformed = shape;
+  for (auto& position : transformed.positions) position *= scale;
+  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
+  return transformed;
+}
+shape_data scale_shape(shape_data&& shape, float scale, float uvscale) {
+  if (scale == 1 && uvscale == 1) return std::move(shape);
+  auto transformed = std::move(shape);
+  for (auto& position : transformed.positions) position *= scale;
+  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
+  return transformed;
+}
+
+// Manipulate vertex data
+shape_data remove_normals(const shape_data& shape) {
+  auto transformed    = shape;
+  transformed.normals = {};
+  return transformed;
+}
+shape_data add_normals(const shape_data& shape) {
+  auto transformed    = shape;
+  transformed.normals = compute_normals(shape);
+  return transformed;
+}
+
+// Shape subdivision
+shape_data subdivide_shape(
+    const shape_data& shape, int subdivisions, bool catmullclark) {
+  // This should probably be re-implemented in a faster fashion,
+  // but how it is not obvious
+  if (subdivisions == 0) return shape;
+  auto subdivided = shape_data{};
+  if (!subdivided.points.empty()) {
+    subdivided = shape;
+  } else if (!subdivided.lines.empty()) {
+    std::tie(std::ignore, subdivided.normals) = subdivide_lines(
+        shape.lines, shape.normals, subdivisions);
+    std::tie(std::ignore, subdivided.texcoords) = subdivide_lines(
+        shape.lines, shape.texcoords, subdivisions);
+    std::tie(std::ignore, subdivided.colors) = subdivide_lines(
+        shape.lines, shape.colors, subdivisions);
+    std::tie(std::ignore, subdivided.radius) = subdivide_lines(
+        subdivided.lines, shape.radius, subdivisions);
+    std::tie(subdivided.lines, subdivided.positions) = subdivide_lines(
+        shape.lines, shape.positions, subdivisions);
+  } else if (!subdivided.triangles.empty()) {
+    std::tie(std::ignore, subdivided.normals) = subdivide_triangles(
+        shape.triangles, shape.normals, subdivisions);
+    std::tie(std::ignore, subdivided.texcoords) = subdivide_triangles(
+        shape.triangles, shape.texcoords, subdivisions);
+    std::tie(std::ignore, subdivided.colors) = subdivide_triangles(
+        shape.triangles, shape.colors, subdivisions);
+    std::tie(std::ignore, subdivided.radius) = subdivide_triangles(
+        shape.triangles, shape.radius, subdivisions);
+    std::tie(subdivided.triangles, subdivided.positions) = subdivide_triangles(
+        shape.triangles, shape.positions, subdivisions);
+  } else if (!subdivided.quads.empty() && !catmullclark) {
+    std::tie(std::ignore, subdivided.normals) = subdivide_quads(
+        shape.quads, shape.normals, subdivisions);
+    std::tie(std::ignore, subdivided.texcoords) = subdivide_quads(
+        shape.quads, shape.texcoords, subdivisions);
+    std::tie(std::ignore, subdivided.colors) = subdivide_quads(
+        shape.quads, shape.colors, subdivisions);
+    std::tie(std::ignore, subdivided.radius) = subdivide_quads(
+        shape.quads, shape.radius, subdivisions);
+    std::tie(subdivided.quads, subdivided.positions) = subdivide_quads(
+        shape.quads, shape.positions, subdivisions);
+  } else if (!subdivided.quads.empty() && catmullclark) {
+    std::tie(std::ignore, subdivided.normals) = subdivide_catmullclark(
+        shape.quads, shape.normals, subdivisions);
+    std::tie(std::ignore, subdivided.texcoords) = subdivide_catmullclark(
+        shape.quads, shape.texcoords, subdivisions);
+    std::tie(std::ignore, subdivided.colors) = subdivide_catmullclark(
+        shape.quads, shape.colors, subdivisions);
+    std::tie(std::ignore, subdivided.radius) = subdivide_catmullclark(
+        shape.quads, shape.radius, subdivisions);
+    std::tie(subdivided.quads, subdivided.positions) = subdivide_catmullclark(
+        shape.quads, shape.positions, subdivisions);
+  } else {
+    // empty shape
+  }
+  return subdivided;
+}
+
+// Displacement
+shape_data displace_shape(const shape_data& shape,
+    const image_t<float>& displacement, float height, float offset) {
+  if (displacement.empty() || shape.texcoords.empty() ||
+      shape.normals.empty() || (shape.triangles.empty() && shape.quads.empty()))
+    return shape;
+  auto displaced      = shape;
+  displaced.positions = displace_vertices(displaced.positions,
+      displaced.normals, displaced.texcoords, displacement, height, offset);
+  displaced.normals   = compute_normals(displaced);
+  return displaced;
+}
+shape_data displace_shape(const shape_data& shape,
+    const image_t<vec4f>& displacement, float height, float offset) {
+  if (displacement.empty() || shape.texcoords.empty() ||
+      shape.normals.empty() || (shape.triangles.empty() && shape.quads.empty()))
+    return shape;
+  auto displaced      = shape;
+  displaced.positions = displace_vertices(displaced.positions,
+      displaced.normals, displaced.texcoords, displacement, height, offset);
+  displaced.normals   = compute_normals(displaced);
+  return displaced;
+}
+
+// Shape sampling
+vector<float> sample_shape_cdf(const shape_data& shape) {
+  if (!shape.points.empty()) {
+    return sample_points_cdf((int)shape.points.size());
+  } else if (!shape.lines.empty()) {
+    return sample_lines_cdf(shape.lines, shape.positions);
+  } else if (!shape.triangles.empty()) {
+    return sample_triangles_cdf(shape.triangles, shape.positions);
+  } else if (!shape.quads.empty()) {
+    return sample_quads_cdf(shape.quads, shape.positions);
+  } else {
+    return sample_points_cdf((int)shape.positions.size());
+  }
+}
+
+pair<int, vec2f> sample_shape(
+    const shape_data& shape, const vector<float>& cdf, float rn, vec2f ruv) {
+  if (!shape.points.empty()) {
+    auto element = sample_points(cdf, rn);
+    return {element, {0, 0}};
+  } else if (!shape.lines.empty()) {
+    auto [element, u] = sample_lines(cdf, rn, ruv.x);
+    return {element, {u, 0}};
+  } else if (!shape.triangles.empty()) {
+    auto [element, uv] = sample_triangles(cdf, rn, ruv);
+    return {element, uv};
+  } else if (!shape.quads.empty()) {
+    auto [element, uv] = sample_quads(cdf, rn, ruv);
+    return {element, uv};
+  } else {
+    auto element = sample_points(cdf, rn);
+    return {element, {0, 0}};
+  }
+}
+
+vector<pair<int, vec2f>> sample_shape(
+    const shape_data& shape, int num_samples, uint64_t seed) {
+  auto cdf    = sample_shape_cdf(shape);
+  auto points = vector<pair<int, vec2f>>(num_samples);
+  auto rng    = make_rng(seed);
+  for (auto& point : points) {
+    point = sample_shape(shape, cdf, rand1f(rng), rand2f(rng));
+  }
+  return points;
+}
+
+}  // namespace yocto
+
+// -----------------------------------------------------------------------------
+// IMPLEMENTATION FOR FVSHAPE PROPERTIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Interpolate vertex data
+vec3f eval_position(const fvshape_data& shape, int element, vec2f uv) {
+  if (!shape.quadspos.empty()) {
+    auto& quad = shape.quadspos[element];
+    return interpolate_quad(shape.positions[quad.x], shape.positions[quad.y],
+        shape.positions[quad.z], shape.positions[quad.w], uv);
+  } else {
+    return {0, 0, 0};
+  }
+}
+
+vec3f eval_normal(const fvshape_data& shape, int element, vec2f uv) {
+  if (shape.normals.empty()) return eval_element_normal(shape, element);
+  if (!shape.quadspos.empty()) {
+    auto& quad = shape.quadsnorm[element];
+    return normalize(
+        interpolate_quad(shape.normals[quad.x], shape.normals[quad.y],
+            shape.normals[quad.z], shape.normals[quad.w], uv));
+  } else {
+    return {0, 0, 1};
+  }
+}
+
+vec2f eval_texcoord(const fvshape_data& shape, int element, vec2f uv) {
+  if (shape.texcoords.empty()) return uv;
+  if (!shape.quadspos.empty()) {
+    auto& quad = shape.quadstexcoord[element];
+    return interpolate_quad(shape.texcoords[quad.x], shape.texcoords[quad.y],
+        shape.texcoords[quad.z], shape.texcoords[quad.w], uv);
+  } else {
+    return uv;
+  }
+}
+
+// Evaluate element normals
+vec3f eval_element_normal(const fvshape_data& shape, int element) {
+  if (!shape.quadspos.empty()) {
+    auto& quad = shape.quadspos[element];
+    return quad_normal(shape.positions[quad.x], shape.positions[quad.y],
+        shape.positions[quad.z], shape.positions[quad.w]);
+  } else {
+    return {0, 0, 0};
+  }
+}
+
+// Compute per-vertex normals/tangents for lines/triangles/quads.
+vector<vec3f> compute_normals(const fvshape_data& shape) {
+  if (!shape.quadspos.empty()) {
+    return quads_normals(shape.quadspos, shape.positions);
+  } else {
+    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
+  }
+}
+void compute_normals(vector<vec3f>& normals, const fvshape_data& shape) {
+  if (!shape.quadspos.empty()) {
+    quads_normals(normals, shape.quadspos, shape.positions);
+  } else {
+    normals.assign(shape.positions.size(), {0, 0, 1});
+  }
+}
+
+// Convert face varying data to single primitives. Returns the quads indices
+// and filled vectors for pos, norm and texcoord.
+static void split_facevarying(vector<vec4i>& split_quads,
+    vector<vec3f>& split_positions, vector<vec3f>& split_normals,
+    vector<vec2f>& split_texcoords, const vector<vec4i>& quadspos,
+    const vector<vec4i>& quadsnorm, const vector<vec4i>& quadstexcoord,
+    const vector<vec3f>& positions, const vector<vec3f>& normals,
+    const vector<vec2f>& texcoords) {
+  // make vertices
+  auto vertices = vector<vec3i>{};
+  vertices.reserve(quadspos.size() * 4);
+  for (auto fid : range(quadspos.size())) {
+    for (auto c : range(4)) {
+      auto vertex = vec3i{
+          (&quadspos[fid].x)[c],
+          (!quadsnorm.empty()) ? (&quadsnorm[fid].x)[c] : -1,
+          (!quadstexcoord.empty()) ? (&quadstexcoord[fid].x)[c] : -1,
+      };
+      vertices.push_back(vertex);
+    }
+  }
+
+  // sort vertices and remove duplicates
+  auto compare_vertices = [](vec3i a, vec3i b) {
+    return a.x < b.x || (a.x == b.x && a.y < b.y) ||
+           (a.x == b.x && a.y == b.y && a.z < b.z);
+  };
+  std::sort(vertices.begin(), vertices.end(), compare_vertices);
+  vertices.erase(std::unique(vertices.begin(), vertices.end()), vertices.end());
+
+  // fill vert data
+  if (!positions.empty()) {
+    split_positions.resize(vertices.size());
+    for (auto&& [index, vertex] : enumerate(vertices)) {
+      split_positions[index] = positions[vertex.x];
+    }
+  } else {
+    split_positions.clear();
+  }
+  if (!normals.empty()) {
+    split_normals.resize(vertices.size());
+    for (auto&& [index, vertex] : enumerate(vertices)) {
+      split_normals[index] = normals[vertex.y];
+    }
+  } else {
+    split_normals.clear();
+  }
+  if (!texcoords.empty()) {
+    split_texcoords.resize(vertices.size());
+    for (auto&& [index, vertex] : enumerate(vertices)) {
+      split_texcoords[index] = texcoords[vertex.z];
+    }
+  } else {
+    split_texcoords.clear();
+  }
+}
+
+// Conversions
+shape_data fvshape_to_shape(const fvshape_data& fvshape, bool as_triangles) {
+  auto shape = shape_data{};
+  split_facevarying(shape.quads, shape.positions, shape.normals,
+      shape.texcoords, fvshape.quadspos, fvshape.quadsnorm,
+      fvshape.quadstexcoord, fvshape.positions, fvshape.normals,
+      fvshape.texcoords);
+  return shape;
+}
+fvshape_data shape_to_fvshape(const shape_data& shape) {
+  if (!shape.points.empty() || !shape.lines.empty())
+    throw std::invalid_argument{"cannot convert shape"};
+  auto fvshape          = fvshape_data{};
+  fvshape.positions     = shape.positions;
+  fvshape.normals       = shape.normals;
+  fvshape.texcoords     = shape.texcoords;
+  fvshape.quadspos      = !shape.quads.empty() ? shape.quads
+                                               : triangles_to_quads(shape.triangles);
+  fvshape.quadsnorm     = !shape.normals.empty() ? fvshape.quadspos
+                                                 : vector<vec4i>{};
+  fvshape.quadstexcoord = !shape.texcoords.empty() ? fvshape.quadspos
+                                                   : vector<vec4i>{};
+  return fvshape;
+}
+
+// Transform shape
+fvshape_data transform_fvshape(
+    const fvshape_data& shape, const frame3f& frame, bool non_rigid) {
+  auto transformed = shape;
+  for (auto& position : transformed.positions)
+    position = transform_point(frame, position);
+  for (auto& normal : transformed.normals)
+    normal = transform_normal(frame, normal, non_rigid);
+  return transformed;
+}
+fvshape_data scale_fvshape(
+    const fvshape_data& shape, float scale, float uvscale) {
+  if (scale == 1 && uvscale == 1) return shape;
+  auto transformed = shape;
+  for (auto& position : transformed.positions) position *= scale;
+  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
+  return transformed;
+}
+fvshape_data scale_fvshape(fvshape_data&& shape, float scale, float uvscale) {
+  if (scale == 1 && uvscale == 1) return std::move(shape);
+  auto transformed = std::move(shape);
+  for (auto& position : transformed.positions) position *= scale;
+  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
+  return transformed;
+}
+
+// Vertex properties
+fvshape_data remove_normals(const fvshape_data& shape) {
+  auto transformed      = shape;
+  transformed.quadsnorm = {};
+  transformed.normals   = {};
+  return transformed;
+}
+fvshape_data add_normals(const fvshape_data& shape) {
+  auto transformed      = shape;
+  transformed.quadsnorm = transformed.quadspos;
+  transformed.normals   = compute_normals(shape);
+  return transformed;
+}
+
+// Subdivision
+fvshape_data subdivide_fvshape(
+    const fvshape_data& shape, int subdivisions, bool catmullclark) {
+  // This should be probably re-implemeneted in a faster fashion.
+  if (subdivisions == 0) return shape;
+  auto subdivided = fvshape_data{};
+  if (!catmullclark) {
+    std::tie(subdivided.quadspos, subdivided.positions) = subdivide_quads(
+        shape.quadspos, shape.positions, subdivisions);
+    std::tie(subdivided.quadsnorm, subdivided.normals) = subdivide_quads(
+        shape.quadsnorm, shape.normals, subdivisions);
+    std::tie(subdivided.quadstexcoord, subdivided.texcoords) = subdivide_quads(
+        shape.quadstexcoord, shape.texcoords, subdivisions);
+  } else {
+    std::tie(subdivided.quadspos, subdivided.positions) =
+        subdivide_catmullclark(shape.quadspos, shape.positions, subdivisions);
+    std::tie(subdivided.quadsnorm, subdivided.normals) = subdivide_catmullclark(
+        shape.quadsnorm, shape.normals, subdivisions);
+    std::tie(subdivided.quadstexcoord, subdivided.texcoords) =
+        subdivide_catmullclark(
+            shape.quadstexcoord, shape.texcoords, subdivisions, true);
+  }
+  return subdivided;
+}
+
+}  // namespace yocto
+
 // -----------------------------------------------------------------------------
 // TEXTURE PROPERTIES
 // -----------------------------------------------------------------------------
diff --git a/libs/yocto/yocto_scene.h b/libs/yocto/yocto_scene.h
index 805bb8ec0..c2353b3b1 100644
--- a/libs/yocto/yocto_scene.h
+++ b/libs/yocto/yocto_scene.h
@@ -90,6 +90,38 @@ struct camera_data {
   float   aperture     = 0;
 };
 
+// Shape data represented as indexed meshes of elements.
+// May contain either points, lines, triangles and quads.
+struct shape_data {
+  // element data
+  vector<int>   points    = {};
+  vector<vec2i> lines     = {};
+  vector<vec3i> triangles = {};
+  vector<vec4i> quads     = {};
+
+  // vertex data
+  vector<vec3f> positions = {};
+  vector<vec3f> normals   = {};
+  vector<vec2f> texcoords = {};
+  vector<vec4f> colors    = {};
+  vector<float> radius    = {};
+  vector<vec4f> tangents  = {};
+};
+
+// Shape data stored as a face-varying mesh.
+// Not included in scenes but used for loading and subdividing.
+struct fvshape_data {
+  // element data
+  vector<vec4i> quadspos      = {};
+  vector<vec4i> quadsnorm     = {};
+  vector<vec4i> quadstexcoord = {};
+
+  // vertex data
+  vector<vec3f> positions = {};
+  vector<vec3f> normals   = {};
+  vector<vec2f> texcoords = {};
+};
+
 // Texture data as array of float or byte pixels. Textures can be stored in
 // linear or non linear color space.
 struct texture_data {
@@ -236,6 +268,119 @@ ray3f eval_camera(const camera_data& camera, vec2f image_uv, vec2f lens_uv);
 
 }  // namespace yocto
 
+// -----------------------------------------------------------------------------
+// SHAPE PROPERTIES AND UTILITIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Shape creation
+template <typename PFunc, typename TFunc>
+inline shape_data make_lines(int steps, PFunc&& position, TFunc&& tangent);
+template <typename PFunc, typename NFunc>
+inline shape_data make_quads(int steps, PFunc&& position, NFunc&& normal);
+
+// Interpolate vertex data
+vec3f eval_position(const shape_data& shape, int element, vec2f uv);
+vec3f eval_normal(const shape_data& shape, int element, vec2f uv);
+vec3f eval_tangent(const shape_data& shape, int element, vec2f uv);
+vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv);
+vec4f eval_color(const shape_data& shape, int element, vec2f uv);
+float eval_radius(const shape_data& shape, int element, vec2f uv);
+
+// Evaluate element normals
+vec3f eval_element_normal(const shape_data& shape, int element);
+
+// Compute per-vertex normals/tangents for lines/triangles/quads.
+vector<vec3f> compute_normals(const shape_data& shape);
+void          compute_normals(vector<vec3f>& normals, const shape_data& shape);
+
+// Conversions
+shape_data quads_to_triangles(const shape_data& shape);
+void       quads_to_triangles_inplace(shape_data& shape);
+
+// Subdivision
+shape_data subdivide_shape(
+    const shape_data& shape, int subdivisions, bool catmullclark);
+
+// Transform shape
+shape_data transform_shape(
+    const shape_data& shape, const frame3f& frame, bool non_rigid = false);
+shape_data transform_shape(const shape_data& shape, const frame3f& frame,
+    float radius_scale, bool non_rigid = false);
+shape_data scale_shape(const shape_data& shape, float scale, float uvscale = 1);
+shape_data scale_shape(shape_data&& shape, float scale, float uvscale = 1);
+shape_data flipyz_shape(const shape_data& shape);
+
+// Manipulate vertex data
+shape_data remove_normals(const shape_data& shape);
+shape_data add_normals(const shape_data& shape);
+
+// Merge a shape into another
+void merge_shape_inplace(shape_data& shape, const shape_data& merge);
+
+// Shape subdivision
+shape_data subdivide_shape(
+    const shape_data& shape, int subdivisions, bool catmullclark);
+
+// Shape displacement
+shape_data displace_shape(const shape_data& shape,
+    const image_t<float>& displacement, float height, float offset);
+shape_data displace_shape(const shape_data& shape,
+    const image_t<vec4f>& displacement, float height, float offset);
+
+// Shape sampling
+vector<float>    sample_shape_cdf(const shape_data& shape);
+pair<int, vec2f> sample_shape(
+    const shape_data& shape, const vector<float>& cdf, float rn, vec2f ruv);
+vector<pair<int, vec2f>> sample_shape(
+    const shape_data& shape, int num_samples, uint64_t seed = 98729387);
+
+}  // namespace yocto
+
+// -----------------------------------------------------------------------------
+// FACE-VARYING SHAPE PROPERTIES AND UTILITIES
+// -----------------------------------------------------------------------------
+namespace yocto {
+
+// Interpolate vertex data
+vec3f eval_position(const fvshape_data& shape, int element, vec2f uv);
+vec3f eval_normal(const fvshape_data& shape, int element, vec2f uv);
+vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv);
+
+// Evaluate element normals
+vec3f eval_element_normal(const fvshape_data& shape, int element);
+
+// Compute per-vertex normals/tangents for lines/triangles/quads.
+vector<vec3f> compute_normals(const fvshape_data& shape);
+void compute_normals(vector<vec3f>& normals, const fvshape_data& shape);
+
+// Conversions
+shape_data fvshape_to_shape(
+    const fvshape_data& shape, bool as_triangles = false);
+fvshape_data shape_to_fvshape(const shape_data& shape);
+
+// Subdivision
+fvshape_data subdivide_fvshape(
+    const fvshape_data& shape, int subdivisions, bool catmullclark);
+
+// Transform shape
+fvshape_data transform_fvshape(
+    const fvshape_data& shape, const frame3f& frame, bool non_rigid = false);
+fvshape_data scale_fvshape(
+    const fvshape_data& shape, float scale, float uvscale = 1);
+fvshape_data scale_fvshape(
+    fvshape_data&& shape, float scale, float uvscale = 1);
+
+// Vertex properties
+fvshape_data remove_normals(const fvshape_data& shape);
+fvshape_data add_normals(const fvshape_data& shape);
+
+// Subdivision
+fvshape_data subdivide_fvshape(
+    const fvshape_data& shape, int subdivisions, bool catmullclark);
+
+}  // namespace yocto
+
 // -----------------------------------------------------------------------------
 // TEXTURE PROPERTIES
 // -----------------------------------------------------------------------------
diff --git a/libs/yocto/yocto_shape.h b/libs/yocto/yocto_shape.h
index 1d773f585..ad8939870 100644
--- a/libs/yocto/yocto_shape.h
+++ b/libs/yocto/yocto_shape.h
@@ -68,76 +68,6 @@ using std::vector;
 #define inline inline __device__ __forceinline__
 #endif
 
-// -----------------------------------------------------------------------------
-// GEOMETRY UTILITIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Line properties.
-inline vec3f line_tangent(vec3f p0, vec3f p1);
-inline float line_length(vec3f p0, vec3f p1);
-
-// Triangle properties.
-inline vec3f triangle_normal(vec3f p0, vec3f p1, vec3f p2);
-inline float triangle_area(vec3f p0, vec3f p1, vec3f p2);
-
-// Quad properties.
-inline vec3f quad_normal(vec3f p0, vec3f p1, vec3f p2, vec3f p3);
-inline float quad_area(vec3f p0, vec3f p1, vec3f p2, vec3f p3);
-
-// Interpolates values over a line parameterized from a to b by u. Same as lerp.
-template <typename T>
-inline T interpolate_line(const T& p0, const T& p1, float u);
-
-// Interpolates values over a triangle parameterized by u and v along the
-// (p1-p0) and (p2-p0) directions. Same as barycentric interpolation.
-template <typename T>
-inline T interpolate_triangle(const T& p0, const T& p1, const T& p2, vec2f uv);
-
-// Interpolates values over a quad parameterized by u and v along the
-// (p1-p0) and (p2-p1) directions. Same as bilinear interpolation.
-template <typename T>
-inline T interpolate_quad(
-    const T& p0, const T& p1, const T& p2, const T& p3, vec2f uv);
-
-// Interpolates values along a cubic Bezier segment parametrized by u.
-template <typename T>
-inline T interpolate_bezier(
-    const T& p0, const T& p1, const T& p2, const T& p3, float u);
-
-// Computes the derivative of a cubic Bezier segment parametrized by u.
-template <typename T>
-inline T interpolate_bezier_derivative(
-    const T& p0, const T& p1, const T& p2, const T& p3, float u);
-
-// Interpolated line properties.
-inline vec3f line_point(vec3f p0, vec3f p1, float u);
-inline vec3f line_tangent(vec3f t0, vec3f t1, float u);
-
-// Interpolated triangle properties.
-inline vec3f triangle_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv);
-inline vec3f triangle_normal(vec3f n0, vec3f n1, vec3f n2, vec2f uv);
-
-// Interpolated quad properties.
-inline vec3f quad_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv);
-inline vec3f quad_normal(vec3f n0, vec3f n1, vec3f n2, vec3f n3, vec2f uv);
-
-// Interpolated sphere properties.
-inline vec3f sphere_point(const vec3f p, float r, vec2f uv);
-inline vec3f sphere_normal(const vec3f p, float r, vec2f uv);
-
-// Triangle tangent and bitangent from uv
-inline pair<vec3f, vec3f> triangle_tangents_fromuv(
-    vec3f p0, vec3f p1, vec3f p2, vec2f uv0, vec2f uv1, vec2f uv2);
-
-// Quad tangent and bitangent from uv. Note that we pass a current_uv since
-// internally we may want to split the quad in two and we need to known where
-// to do it. If not interested in the split, just pass vec2f{0,0} here.
-inline pair<vec3f, vec3f> quad_tangents_fromuv(vec3f p0, vec3f p1, vec3f p2,
-    vec3f p3, vec2f uv0, vec2f uv1, vec2f uv2, vec2f uv3, vec2f current_uv);
-
-}  // namespace yocto
-
 // -----------------------------------------------------------------------------
 // COMPUTATION OF PER_VERTEX PROPERTIES
 // -----------------------------------------------------------------------------
@@ -199,129 +129,6 @@ inline vector<vec4i> bezier_to_lines(vector<vec2i>& lines);
 
 }  // namespace yocto
 
-// -----------------------------------------------------------------------------
-// SHAPE DATA AND UTILITIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Shape data represented as indexed meshes of elements.
-// May contain either points, lines, triangles and quads.
-struct shape_data {
-  // element data
-  vector<int>   points    = {};
-  vector<vec2i> lines     = {};
-  vector<vec3i> triangles = {};
-  vector<vec4i> quads     = {};
-
-  // vertex data
-  vector<vec3f> positions = {};
-  vector<vec3f> normals   = {};
-  vector<vec2f> texcoords = {};
-  vector<vec4f> colors    = {};
-  vector<float> radius    = {};
-  vector<vec4f> tangents  = {};
-};
-
-// Shape creation
-template <typename PFunc, typename TFunc>
-inline shape_data make_lines(int steps, PFunc&& position, TFunc&& tangent);
-template <typename PFunc, typename NFunc>
-inline shape_data make_quads(int steps, PFunc&& position, NFunc&& normal);
-
-// Interpolate vertex data
-vec3f eval_position(const shape_data& shape, int element, vec2f uv);
-vec3f eval_normal(const shape_data& shape, int element, vec2f uv);
-vec3f eval_tangent(const shape_data& shape, int element, vec2f uv);
-vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv);
-vec4f eval_color(const shape_data& shape, int element, vec2f uv);
-float eval_radius(const shape_data& shape, int element, vec2f uv);
-
-// Evaluate element normals
-vec3f eval_element_normal(const shape_data& shape, int element);
-
-// Compute per-vertex normals/tangents for lines/triangles/quads.
-vector<vec3f> compute_normals(const shape_data& shape);
-void          compute_normals(vector<vec3f>& normals, const shape_data& shape);
-
-// Conversions
-shape_data quads_to_triangles(const shape_data& shape);
-void       quads_to_triangles_inplace(shape_data& shape);
-
-// Subdivision
-shape_data subdivide_shape(
-    const shape_data& shape, int subdivisions, bool catmullclark);
-
-// Transform shape
-shape_data transform_shape(
-    const shape_data& shape, const frame3f& frame, bool non_rigid = false);
-shape_data transform_shape(const shape_data& shape, const frame3f& frame,
-    float radius_scale, bool non_rigid = false);
-shape_data scale_shape(const shape_data& shape, float scale, float uvscale = 1);
-shape_data scale_shape(shape_data&& shape, float scale, float uvscale = 1);
-shape_data flipyz_shape(const shape_data& shape);
-
-// Manipulate vertex data
-shape_data remove_normals(const shape_data& shape);
-shape_data add_normals(const shape_data& shape);
-
-// Merge a shape into another
-void merge_shape_inplace(shape_data& shape, const shape_data& merge);
-
-}  // namespace yocto
-
-// -----------------------------------------------------------------------------
-// FACE-VARYING SHAPE DATA AND UTILITIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Shape data stored as a face-varying mesh
-struct fvshape_data {
-  // element data
-  vector<vec4i> quadspos      = {};
-  vector<vec4i> quadsnorm     = {};
-  vector<vec4i> quadstexcoord = {};
-
-  // vertex data
-  vector<vec3f> positions = {};
-  vector<vec3f> normals   = {};
-  vector<vec2f> texcoords = {};
-};
-
-// Interpolate vertex data
-vec3f eval_position(const fvshape_data& shape, int element, vec2f uv);
-vec3f eval_normal(const fvshape_data& shape, int element, vec2f uv);
-vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv);
-
-// Evaluate element normals
-vec3f eval_element_normal(const fvshape_data& shape, int element);
-
-// Compute per-vertex normals/tangents for lines/triangles/quads.
-vector<vec3f> compute_normals(const fvshape_data& shape);
-void compute_normals(vector<vec3f>& normals, const fvshape_data& shape);
-
-// Conversions
-shape_data fvshape_to_shape(
-    const fvshape_data& shape, bool as_triangles = false);
-fvshape_data shape_to_fvshape(const shape_data& shape);
-
-// Subdivision
-fvshape_data subdivide_fvshape(
-    const fvshape_data& shape, int subdivisions, bool catmullclark);
-
-// Transform shape
-fvshape_data transform_fvshape(
-    const fvshape_data& shape, const frame3f& frame, bool non_rigid = false);
-fvshape_data scale_fvshape(
-    const fvshape_data& shape, float scale, float uvscale = 1);
-fvshape_data scale_fvshape(
-    fvshape_data&& shape, float scale, float uvscale = 1);
-
-// Vertex properties
-fvshape_data remove_normals(const fvshape_data& shape);
-fvshape_data add_normals(const fvshape_data& shape);
-
-}  // namespace yocto
-
 // -----------------------------------------------------------------------------
 //
 //
@@ -330,150 +137,6 @@ fvshape_data add_normals(const fvshape_data& shape);
 //
 // -----------------------------------------------------------------------------
 
-// -----------------------------------------------------------------------------
-// GEOMETRY UTILITIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Line properties.
-inline vec3f line_tangent(vec3f p0, vec3f p1) { return normalize(p1 - p0); }
-inline float line_length(vec3f p0, vec3f p1) { return length(p1 - p0); }
-
-// Triangle properties.
-inline vec3f triangle_normal(vec3f p0, vec3f p1, vec3f p2) {
-  return normalize(cross(p1 - p0, p2 - p0));
-}
-inline float triangle_area(vec3f p0, vec3f p1, vec3f p2) {
-  return length(cross(p1 - p0, p2 - p0)) / 2;
-}
-
-// Quad propeties.
-inline vec3f quad_normal(vec3f p0, vec3f p1, vec3f p2, vec3f p3) {
-  return normalize(triangle_normal(p0, p1, p3) + triangle_normal(p2, p3, p1));
-}
-inline float quad_area(vec3f p0, vec3f p1, vec3f p2, vec3f p3) {
-  return triangle_area(p0, p1, p3) + triangle_area(p2, p3, p1);
-}
-
-// Interpolates values over a line parameterized from a to b by u. Same as lerp.
-template <typename T>
-inline T interpolate_line(const T& p0, const T& p1, float u) {
-  return p0 * (1 - u) + p1 * u;
-}
-
-// Interpolates values over a triangle parameterized by u and v along the
-// (p1-p0) and (p2-p0) directions. Same as barycentric interpolation.
-template <typename T>
-inline T interpolate_triangle(const T& p0, const T& p1, const T& p2, vec2f uv) {
-  return p0 * (1 - uv.x - uv.y) + p1 * uv.x + p2 * uv.y;
-}
-
-// Interpolates values over a quad parameterized by u and v along the
-// (p1-p0) and (p2-p1) directions. Same as bilinear interpolation.
-template <typename T>
-inline T interpolate_quad(
-    const T& p0, const T& p1, const T& p2, const T& p3, vec2f uv) {
-  if (uv.x + uv.y <= 1) {
-    return interpolate_triangle(p0, p1, p3, uv);
-  } else {
-    return interpolate_triangle(p2, p3, p1, 1 - uv);
-  }
-}
-
-// Interpolates values along a cubic Bezier segment parametrized by u.
-template <typename T>
-inline T interpolate_bezier(
-    const T& p0, const T& p1, const T& p2, const T& p3, float u) {
-  return p0 * (1 - u) * (1 - u) * (1 - u) + p1 * 3 * u * (1 - u) * (1 - u) +
-         p2 * 3 * u * u * (1 - u) + p3 * u * u * u;
-}
-// Computes the derivative of a cubic Bezier segment parametrized by u.
-template <typename T>
-inline T interpolate_bezier_derivative(
-    const T& p0, const T& p1, const T& p2, const T& p3, float u) {
-  return (p1 - p0) * 3 * (1 - u) * (1 - u) + (p2 - p1) * 6 * u * (1 - u) +
-         (p3 - p2) * 3 * u * u;
-}
-
-// Interpolated line properties.
-inline vec3f line_point(vec3f p0, vec3f p1, float u) {
-  return p0 * (1 - u) + p1 * u;
-}
-inline vec3f line_tangent(vec3f t0, vec3f t1, float u) {
-  return normalize(t0 * (1 - u) + t1 * u);
-}
-
-// Interpolated triangle properties.
-inline vec3f triangle_point(vec3f p0, vec3f p1, vec3f p2, vec2f uv) {
-  return p0 * (1 - uv.x - uv.y) + p1 * uv.x + p2 * uv.y;
-}
-inline vec3f triangle_normal(vec3f n0, vec3f n1, vec3f n2, vec2f uv) {
-  return normalize(n0 * (1 - uv.x - uv.y) + n1 * uv.x + n2 * uv.y);
-}
-
-// Interpolated quad properties.
-inline vec3f quad_point(vec3f p0, vec3f p1, vec3f p2, vec3f p3, vec2f uv) {
-  if (uv.x + uv.y <= 1) {
-    return triangle_point(p0, p1, p3, uv);
-  } else {
-    return triangle_point(p2, p3, p1, 1 - uv);
-  }
-}
-inline vec3f quad_normal(vec3f n0, vec3f n1, vec3f n2, vec3f n3, vec2f uv) {
-  if (uv.x + uv.y <= 1) {
-    return triangle_normal(n0, n1, n3, uv);
-  } else {
-    return triangle_normal(n2, n3, n1, 1 - uv);
-  }
-}
-
-// Interpolated sphere properties.
-inline vec3f sphere_point(const vec3f p, float r, vec2f uv) {
-  return p + r * vec3f{cos(uv.x * 2 * pif) * sin(uv.y * pif),
-                     sin(uv.x * 2 * pif) * sin(uv.y * pif), cos(uv.y * pif)};
-}
-inline vec3f sphere_normal(const vec3f p, float r, vec2f uv) {
-  return normalize(vec3f{cos(uv.x * 2 * pif) * sin(uv.y * pif),
-      sin(uv.x * 2 * pif) * sin(uv.y * pif), cos(uv.y * pif)});
-}
-
-// Triangle tangent and bitangent from uv
-inline pair<vec3f, vec3f> triangle_tangents_fromuv(
-    vec3f p0, vec3f p1, vec3f p2, vec2f uv0, vec2f uv1, vec2f uv2) {
-  // Follows the definition in http://www.terathon.com/code/tangent.html and
-  // https://gist.github.com/aras-p/2843984
-  // normal points up from texture space
-  auto p   = p1 - p0;
-  auto q   = p2 - p0;
-  auto s   = vec2f{uv1.x - uv0.x, uv2.x - uv0.x};
-  auto t   = vec2f{uv1.y - uv0.y, uv2.y - uv0.y};
-  auto div = s.x * t.y - s.y * t.x;
-
-  if (div != 0) {
-    auto tu = vec3f{t.y * p.x - t.x * q.x, t.y * p.y - t.x * q.y,
-                  t.y * p.z - t.x * q.z} /
-              div;
-    auto tv = vec3f{s.x * q.x - s.y * p.x, s.x * q.y - s.y * p.y,
-                  s.x * q.z - s.y * p.z} /
-              div;
-    return {tu, tv};
-  } else {
-    return {{1, 0, 0}, {0, 1, 0}};
-  }
-}
-
-// Quad tangent and bitangent from uv.
-inline pair<vec3f, vec3f> quad_tangents_fromuv(vec3f p0, vec3f p1, vec3f p2,
-    vec3f p3, vec2f uv0, vec2f uv1, vec2f uv2, vec2f uv3, vec2f current_uv) {
-  if (current_uv.x + current_uv.y <= 1) {
-    return triangle_tangents_fromuv(p0, p1, p3, uv0, uv1, uv3);
-  } else {
-    return triangle_tangents_fromuv(p2, p3, p1, uv2, uv3, uv1);
-  }
-}
-
-}  // namespace yocto
-
 // -----------------------------------------------------------------------------
 // IMPLEMENTATION OF COMPUTATION OF PER-VERTEX PROPERTIES
 // -----------------------------------------------------------------------------
@@ -685,474 +348,6 @@ inline vector<vec2i> bezier_to_lines(const vector<vec4i>& beziers) {
 
 }  // namespace yocto
 
-// -----------------------------------------------------------------------------
-// SHAPE FUNCTIONS
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Shape creation
-template <typename PFunc, typename TFunc>
-inline shape_data make_lines(int steps, PFunc&& position, TFunc&& tangent) {
-  auto shape      = shape_data{};
-  shape.positions = vector<vec3f>(steps + 1);
-  shape.normals   = vector<vec3f>(steps + 1);
-  shape.texcoords = vector<vec2f>(steps + 1);
-  for (auto idx : range(steps + 1)) {
-    auto u               = (float)idx / (float)steps;
-    shape.positions[idx] = position(u);
-    shape.normals[idx]   = tangent(u);
-    shape.texcoords[idx] = {u, 0};
-  }
-  shape.lines = vector<vec2i>(steps);
-  for (auto idx : range(steps)) shape.lines[idx] = {idx, idx + 1};
-  return shape;
-}
-template <typename PFunc, typename NFunc>
-inline shape_data make_quads(vec2i steps, PFunc&& position, NFunc&& normal) {
-  auto shape      = shape_data{};
-  shape.positions = vector<vec3f>((steps.x + 1) * (steps.y + 1));
-  shape.normals   = vector<vec3f>((steps.x + 1) * (steps.y + 1));
-  shape.texcoords = vector<vec2f>((steps.x + 1) * (steps.y + 1));
-  for (auto j : range(steps.y + 1)) {
-    for (auto i : range(steps.x + 1)) {
-      auto uv              = vec2f{i / (float)steps.x, j / (float)steps.y};
-      auto idx             = j * (steps.x + 1) + i;
-      shape.positions[idx] = position(uv);
-      shape.normals[idx]   = normal(uv);
-      shape.texcoords[idx] = uv;
-    }
-  }
-  shape.quads = vector<vec4i>(steps.x * steps.y);
-  for (auto j : range(steps.y)) {
-    for (auto i : range(steps.x)) {
-      auto idx         = j * steps.x + i;
-      shape.quads[idx] = {j * (steps.x + 1) + i, j * (steps.x + 1) + i + 1,
-          (j + 1) * (steps.x + 1) + i + 1, (j + 1) * (steps.x + 1) + i};
-    }
-  }
-  return shape;
-}
-
-}  // namespace yocto
-
-// -----------------------------------------------------------------------------
-// IMPLEMENTATION FO SHAPE PROPERTIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Interpolate vertex data
-inline vec3f eval_position(const shape_data& shape, int element, vec2f uv) {
-  if (!shape.points.empty()) {
-    auto& point = shape.points[element];
-    return shape.positions[point];
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return interpolate_line(
-        shape.positions[line.x], shape.positions[line.y], uv.x);
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return interpolate_triangle(shape.positions[triangle.x],
-        shape.positions[triangle.y], shape.positions[triangle.z], uv);
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return interpolate_quad(shape.positions[quad.x], shape.positions[quad.y],
-        shape.positions[quad.z], shape.positions[quad.w], uv);
-  } else {
-    return {0, 0, 0};
-  }
-}
-
-inline vec3f eval_normal(const shape_data& shape, int element, vec2f uv) {
-  if (shape.normals.empty()) return eval_element_normal(shape, element);
-  if (!shape.points.empty()) {
-    auto& point = shape.points[element];
-    return normalize(shape.normals[point]);
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return normalize(
-        interpolate_line(shape.normals[line.x], shape.normals[line.y], uv.x));
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return normalize(interpolate_triangle(shape.normals[triangle.x],
-        shape.normals[triangle.y], shape.normals[triangle.z], uv));
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return normalize(
-        interpolate_quad(shape.normals[quad.x], shape.normals[quad.y],
-            shape.normals[quad.z], shape.normals[quad.w], uv));
-  } else {
-    return {0, 0, 1};
-  }
-}
-
-inline vec3f eval_tangent(const shape_data& shape, int element, vec2f uv) {
-  return eval_normal(shape, element, uv);
-}
-
-inline vec2f eval_texcoord(const shape_data& shape, int element, vec2f uv) {
-  if (shape.texcoords.empty()) return uv;
-  if (!shape.points.empty()) {
-    auto& point = shape.points[element];
-    return shape.texcoords[point];
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return interpolate_line(
-        shape.texcoords[line.x], shape.texcoords[line.y], uv.x);
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return interpolate_triangle(shape.texcoords[triangle.x],
-        shape.texcoords[triangle.y], shape.texcoords[triangle.z], uv);
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return interpolate_quad(shape.texcoords[quad.x], shape.texcoords[quad.y],
-        shape.texcoords[quad.z], shape.texcoords[quad.w], uv);
-  } else {
-    return uv;
-  }
-}
-
-inline vec4f eval_color(const shape_data& shape, int element, vec2f uv) {
-  if (shape.colors.empty()) return {1, 1, 1, 1};
-  if (!shape.points.empty()) {
-    auto& point = shape.points[element];
-    return shape.colors[point];
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return interpolate_line(shape.colors[line.x], shape.colors[line.y], uv.x);
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return interpolate_triangle(shape.colors[triangle.x],
-        shape.colors[triangle.y], shape.colors[triangle.z], uv);
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return interpolate_quad(shape.colors[quad.x], shape.colors[quad.y],
-        shape.colors[quad.z], shape.colors[quad.w], uv);
-  } else {
-    return {1, 1, 1, 1};
-  }
-}
-
-inline float eval_radius(const shape_data& shape, int element, vec2f uv) {
-  if (shape.radius.empty()) return 0;
-  if (!shape.points.empty()) {
-    auto& point = shape.points[element];
-    return shape.radius[point];
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return interpolate_line(shape.radius[line.x], shape.radius[line.y], uv.x);
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return interpolate_triangle(shape.radius[triangle.x],
-        shape.radius[triangle.y], shape.radius[triangle.z], uv);
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return interpolate_quad(shape.radius[quad.x], shape.radius[quad.y],
-        shape.radius[quad.z], shape.radius[quad.w], uv);
-  } else {
-    return 0;
-  }
-}
-
-// Evaluate element normals
-inline vec3f eval_element_normal(const shape_data& shape, int element) {
-  if (!shape.points.empty()) {
-    return {0, 0, 1};
-  } else if (!shape.lines.empty()) {
-    auto& line = shape.lines[element];
-    return line_tangent(shape.positions[line.x], shape.positions[line.y]);
-  } else if (!shape.triangles.empty()) {
-    auto& triangle = shape.triangles[element];
-    return triangle_normal(shape.positions[triangle.x],
-        shape.positions[triangle.y], shape.positions[triangle.z]);
-  } else if (!shape.quads.empty()) {
-    auto& quad = shape.quads[element];
-    return quad_normal(shape.positions[quad.x], shape.positions[quad.y],
-        shape.positions[quad.z], shape.positions[quad.w]);
-  } else {
-    return {0, 0, 0};
-  }
-}
-
-// Compute per-vertex normals/tangents for lines/triangles/quads.
-inline vector<vec3f> compute_normals(const shape_data& shape) {
-  if (!shape.points.empty()) {
-    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
-  } else if (!shape.lines.empty()) {
-    return lines_tangents(shape.lines, shape.positions);
-  } else if (!shape.triangles.empty()) {
-    return triangles_normals(shape.triangles, shape.positions);
-  } else if (!shape.quads.empty()) {
-    return quads_normals(shape.quads, shape.positions);
-  } else {
-    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
-  }
-}
-inline void compute_normals(vector<vec3f>& normals, const shape_data& shape) {
-  if (!shape.points.empty()) {
-    normals.assign(shape.positions.size(), {0, 0, 1});
-  } else if (!shape.lines.empty()) {
-    lines_tangents(normals, shape.lines, shape.positions);
-  } else if (!shape.triangles.empty()) {
-    triangles_normals(normals, shape.triangles, shape.positions);
-  } else if (!shape.quads.empty()) {
-    quads_normals(normals, shape.quads, shape.positions);
-  } else {
-    normals.assign(shape.positions.size(), {0, 0, 1});
-  }
-}
-
-// Conversions
-inline shape_data quads_to_triangles(const shape_data& shape) {
-  auto result = shape;
-  if (!shape.quads.empty()) {
-    result.triangles = quads_to_triangles(shape.quads);
-    result.quads     = {};
-  }
-  return result;
-}
-inline void quads_to_triangles_inplace(shape_data& shape) {
-  if (shape.quads.empty()) return;
-  shape.triangles = quads_to_triangles(shape.quads);
-  shape.quads     = {};
-}
-
-// Transform shape
-inline shape_data transform_shape(
-    const shape_data& shape, const frame3f& frame, bool non_rigid) {
-  return transform_shape(shape, frame, 1, non_rigid);
-}
-inline shape_data transform_shape(const shape_data& shape, const frame3f& frame,
-    float radius_scale, bool non_rigid) {
-  auto transformed = shape;
-  for (auto& position : transformed.positions)
-    position = transform_point(frame, position);
-  for (auto& normal : transformed.normals)
-    normal = transform_normal(frame, normal, non_rigid);
-  for (auto& radius : transformed.radius) radius *= radius_scale;
-  return transformed;
-}
-inline shape_data scale_shape(
-    const shape_data& shape, float scale, float uvscale) {
-  if (scale == 1 && uvscale == 1) return shape;
-  auto transformed = shape;
-  for (auto& position : transformed.positions) position *= scale;
-  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
-  return transformed;
-}
-inline shape_data scale_shape(shape_data&& shape, float scale, float uvscale) {
-  if (scale == 1 && uvscale == 1) return std::move(shape);
-  auto transformed = std::move(shape);
-  for (auto& position : transformed.positions) position *= scale;
-  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
-  return transformed;
-}
-
-// Manipulate vertex data
-inline shape_data remove_normals(const shape_data& shape) {
-  auto transformed    = shape;
-  transformed.normals = {};
-  return transformed;
-}
-inline shape_data add_normals(const shape_data& shape) {
-  auto transformed    = shape;
-  transformed.normals = compute_normals(shape);
-  return transformed;
-}
-
-}  // namespace yocto
-
-// -----------------------------------------------------------------------------
-// IMPLEMENTATION FOR FVSHAPE PROPERTIES
-// -----------------------------------------------------------------------------
-namespace yocto {
-
-// Interpolate vertex data
-inline vec3f eval_position(const fvshape_data& shape, int element, vec2f uv) {
-  if (!shape.quadspos.empty()) {
-    auto& quad = shape.quadspos[element];
-    return interpolate_quad(shape.positions[quad.x], shape.positions[quad.y],
-        shape.positions[quad.z], shape.positions[quad.w], uv);
-  } else {
-    return {0, 0, 0};
-  }
-}
-
-inline vec3f eval_normal(const fvshape_data& shape, int element, vec2f uv) {
-  if (shape.normals.empty()) return eval_element_normal(shape, element);
-  if (!shape.quadspos.empty()) {
-    auto& quad = shape.quadsnorm[element];
-    return normalize(
-        interpolate_quad(shape.normals[quad.x], shape.normals[quad.y],
-            shape.normals[quad.z], shape.normals[quad.w], uv));
-  } else {
-    return {0, 0, 1};
-  }
-}
-
-inline vec2f eval_texcoord(const fvshape_data& shape, int element, vec2f uv) {
-  if (shape.texcoords.empty()) return uv;
-  if (!shape.quadspos.empty()) {
-    auto& quad = shape.quadstexcoord[element];
-    return interpolate_quad(shape.texcoords[quad.x], shape.texcoords[quad.y],
-        shape.texcoords[quad.z], shape.texcoords[quad.w], uv);
-  } else {
-    return uv;
-  }
-}
-
-// Evaluate element normals
-inline vec3f eval_element_normal(const fvshape_data& shape, int element) {
-  if (!shape.quadspos.empty()) {
-    auto& quad = shape.quadspos[element];
-    return quad_normal(shape.positions[quad.x], shape.positions[quad.y],
-        shape.positions[quad.z], shape.positions[quad.w]);
-  } else {
-    return {0, 0, 0};
-  }
-}
-
-// Compute per-vertex normals/tangents for lines/triangles/quads.
-inline vector<vec3f> compute_normals(const fvshape_data& shape) {
-  if (!shape.quadspos.empty()) {
-    return quads_normals(shape.quadspos, shape.positions);
-  } else {
-    return vector<vec3f>(shape.positions.size(), {0, 0, 1});
-  }
-}
-inline void compute_normals(vector<vec3f>& normals, const fvshape_data& shape) {
-  if (!shape.quadspos.empty()) {
-    quads_normals(normals, shape.quadspos, shape.positions);
-  } else {
-    normals.assign(shape.positions.size(), {0, 0, 1});
-  }
-}
-
-// Convert face varying data to single primitives. Returns the quads indices
-// and filled vectors for pos, norm and texcoord.
-inline void split_facevarying(vector<vec4i>& split_quads,
-    vector<vec3f>& split_positions, vector<vec3f>& split_normals,
-    vector<vec2f>& split_texcoords, const vector<vec4i>& quadspos,
-    const vector<vec4i>& quadsnorm, const vector<vec4i>& quadstexcoord,
-    const vector<vec3f>& positions, const vector<vec3f>& normals,
-    const vector<vec2f>& texcoords) {
-  // make vertices
-  auto vertices = vector<vec3i>{};
-  vertices.reserve(quadspos.size() * 4);
-  for (auto fid : range(quadspos.size())) {
-    for (auto c : range(4)) {
-      auto vertex = vec3i{
-          (&quadspos[fid].x)[c],
-          (!quadsnorm.empty()) ? (&quadsnorm[fid].x)[c] : -1,
-          (!quadstexcoord.empty()) ? (&quadstexcoord[fid].x)[c] : -1,
-      };
-      vertices.push_back(vertex);
-    }
-  }
-
-  // sort vertices and remove duplicates
-  auto compare_vertices = [](vec3i a, vec3i b) {
-    return a.x < b.x || (a.x == b.x && a.y < b.y) ||
-           (a.x == b.x && a.y == b.y && a.z < b.z);
-  };
-  std::sort(vertices.begin(), vertices.end(), compare_vertices);
-  vertices.erase(std::unique(vertices.begin(), vertices.end()), vertices.end());
-
-  // fill vert data
-  if (!positions.empty()) {
-    split_positions.resize(vertices.size());
-    for (auto&& [index, vertex] : enumerate(vertices)) {
-      split_positions[index] = positions[vertex.x];
-    }
-  } else {
-    split_positions.clear();
-  }
-  if (!normals.empty()) {
-    split_normals.resize(vertices.size());
-    for (auto&& [index, vertex] : enumerate(vertices)) {
-      split_normals[index] = normals[vertex.y];
-    }
-  } else {
-    split_normals.clear();
-  }
-  if (!texcoords.empty()) {
-    split_texcoords.resize(vertices.size());
-    for (auto&& [index, vertex] : enumerate(vertices)) {
-      split_texcoords[index] = texcoords[vertex.z];
-    }
-  } else {
-    split_texcoords.clear();
-  }
-}
-
-// Conversions
-inline shape_data fvshape_to_shape(
-    const fvshape_data& fvshape, bool as_triangles) {
-  auto shape = shape_data{};
-  split_facevarying(shape.quads, shape.positions, shape.normals,
-      shape.texcoords, fvshape.quadspos, fvshape.quadsnorm,
-      fvshape.quadstexcoord, fvshape.positions, fvshape.normals,
-      fvshape.texcoords);
-  return shape;
-}
-inline fvshape_data shape_to_fvshape(const shape_data& shape) {
-  if (!shape.points.empty() || !shape.lines.empty())
-    throw std::invalid_argument{"cannot convert shape"};
-  auto fvshape          = fvshape_data{};
-  fvshape.positions     = shape.positions;
-  fvshape.normals       = shape.normals;
-  fvshape.texcoords     = shape.texcoords;
-  fvshape.quadspos      = !shape.quads.empty() ? shape.quads
-                                               : triangles_to_quads(shape.triangles);
-  fvshape.quadsnorm     = !shape.normals.empty() ? fvshape.quadspos
-                                                 : vector<vec4i>{};
-  fvshape.quadstexcoord = !shape.texcoords.empty() ? fvshape.quadspos
-                                                   : vector<vec4i>{};
-  return fvshape;
-}
-
-// Transform shape
-inline fvshape_data transform_fvshape(
-    const fvshape_data& shape, const frame3f& frame, bool non_rigid) {
-  auto transformed = shape;
-  for (auto& position : transformed.positions)
-    position = transform_point(frame, position);
-  for (auto& normal : transformed.normals)
-    normal = transform_normal(frame, normal, non_rigid);
-  return transformed;
-}
-inline fvshape_data scale_fvshape(
-    const fvshape_data& shape, float scale, float uvscale) {
-  if (scale == 1 && uvscale == 1) return shape;
-  auto transformed = shape;
-  for (auto& position : transformed.positions) position *= scale;
-  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
-  return transformed;
-}
-inline fvshape_data scale_fvshape(
-    fvshape_data&& shape, float scale, float uvscale) {
-  if (scale == 1 && uvscale == 1) return std::move(shape);
-  auto transformed = std::move(shape);
-  for (auto& position : transformed.positions) position *= scale;
-  for (auto& texcoord : transformed.texcoords) texcoord *= uvscale;
-  return transformed;
-}
-
-// Vertex properties
-inline fvshape_data remove_normals(const fvshape_data& shape) {
-  auto transformed      = shape;
-  transformed.quadsnorm = {};
-  transformed.normals   = {};
-  return transformed;
-}
-inline fvshape_data add_normals(const fvshape_data& shape) {
-  auto transformed      = shape;
-  transformed.quadsnorm = transformed.quadspos;
-  transformed.normals   = compute_normals(shape);
-  return transformed;
-}
-
-}  // namespace yocto
-
 // -----------------------------------------------------------------------------
 // CUDA SUPPORT
 // -----------------------------------------------------------------------------