Cycles: Adaptive subdivision triangular patches

There is a corner case where one side of a quad needs splitting and the other
side has only one segment. Previously this would produce either gaps or after
recent changes to stitch together geometry, uninitialized memory.

Now solve this by splitting into triangular patches, as suggested in the
DiagSplit paper. These triangular patches can be further subdivided themselves.
Dicing has special cases for 1 or 2 segments on edges. For more segments it
works the same as: quad dicing: A regular inner triangle grid stitched to the
outer edges.

Fix #136973: Inconsistent results with adaptive subdivision

Pull Request: https://projects.blender.org/blender/blender/pulls/139062
This commit is contained in:
Brecht Van Lommel
2025-04-15 17:18:50 +02:00
parent 98104d63ca
commit 59b4842117
10 changed files with 552 additions and 32 deletions

View File

@@ -333,7 +333,7 @@ void EdgeDice::add_triangle_strip(const SubPatch &sub, const int left_edge, cons
}
}
void EdgeDice::set_sides(const SubPatch &sub)
void EdgeDice::quad_set_sides(const SubPatch &sub)
{
for (int edge = 0; edge < 4; edge++) {
const int t = sub.edges[edge].edge->T;
@@ -367,7 +367,7 @@ void EdgeDice::set_sides(const SubPatch &sub)
}
}
void EdgeDice::dice(const SubPatch &sub)
void EdgeDice::quad_dice(const SubPatch &sub)
{
/* Compute inner grid size with scale factor. */
const int Mu = max(sub.edges[0].edge->T, sub.edges[2].edge->T);
@@ -394,6 +394,272 @@ void EdgeDice::dice(const SubPatch &sub)
}
}
void EdgeDice::tri_set_sides(const SubPatch &sub)
{
for (int edge = 0; edge < 3; edge++) {
const int t = sub.edges[edge].edge->T;
const int i_start = (sub.edges[edge].own_vertex) ? 0 : 1;
const int i_end = (sub.edges[edge].own_edge) ? t : 1;
/* set verts on the edge of the patch */
for (int i = i_start; i < i_end; i++) {
const float f = i / (float)t;
float2 uv;
switch (edge) {
case 0:
uv = make_float2(f, 0.0f);
break;
case 1:
uv = make_float2(1.0f - f, f);
break;
case 2:
default:
uv = make_float2(0.0f, 1.0f - f);
break;
}
const int vert_index = sub.get_vert_along_edge(edge, i);
set_vertex(sub, vert_index, sub.map_uv(uv));
}
}
}
void EdgeDice::tri_dice(const SubPatch &sub)
{
const int M = max(max(sub.edges[0].edge->T, sub.edges[1].edge->T), sub.edges[2].edge->T);
const float d = 1.0f / (float)(M + 1);
int triangle_index = sub.triangles_offset;
if (M == 1) {
/* Single triangle. */
set_triangle(sub,
triangle_index++,
sub.edges[0].start_vert_index(),
sub.edges[1].start_vert_index(),
sub.edges[2].start_vert_index(),
sub.map_uv(make_float2(0.0f, 0.0f)),
sub.map_uv(make_float2(1.0f, 0.0f)),
sub.map_uv(make_float2(0.0f, 1.0f)));
assert(triangle_index == sub.triangles_offset + sub.calc_num_triangles());
return;
}
if (M == 2) {
/* Edges have 2 segments or less. */
int num_split = 0;
int split_0 = -1;
for (int i = 0; i < 3; i++) {
if (sub.edges[i].edge->T == 2) {
num_split++;
if (split_0 == -1) {
split_0 = i;
}
}
}
/* When two edges have 2 segments, we assume split_0 is the first of two consecutive edges. */
if (split_0 == 0 && sub.edges[2].edge->T == 2) {
split_0 = 2;
}
const int split_1 = (split_0 + 1) % 3;
const int split_2 = (split_0 + 2) % 3;
const int v[3] = {sub.edges[0].start_vert_index(),
sub.edges[1].start_vert_index(),
sub.edges[2].start_vert_index()};
const int mid_v[3] = {sub.get_vert_along_edge(0, 1),
sub.get_vert_along_edge(1, 1),
sub.get_vert_along_edge(2, 1)};
const float2 uv[3] = {sub.map_uv(make_float2(0.0f, 0.0f)),
sub.map_uv(make_float2(1.0f, 0.0f)),
sub.map_uv(make_float2(0.0f, 1.0f))};
const float2 mid_uv[3] = {sub.map_uv(make_float2(0.5f, 0.0f)),
sub.map_uv(make_float2(0.5f, 0.5f)),
sub.map_uv(make_float2(0.0f, 0.5f))};
if (num_split == 3) {
/* All edges have two segments
* /\
* /--\
* / \/ \
* ------- */
set_triangle(sub, triangle_index++, v[0], mid_v[0], mid_v[2], uv[0], mid_uv[0], mid_uv[2]);
set_triangle(sub, triangle_index++, v[1], mid_v[1], mid_v[0], uv[1], mid_uv[1], mid_uv[0]);
set_triangle(sub, triangle_index++, v[2], mid_v[2], mid_v[1], uv[2], mid_uv[2], mid_uv[1]);
set_triangle(
sub, triangle_index++, mid_v[0], mid_v[1], mid_v[2], mid_uv[0], mid_uv[1], mid_uv[2]);
}
else {
/* One edge has two segments.
* / \
* / | \
* / | \
* ------- */
set_triangle(sub,
triangle_index++,
v[split_0],
mid_v[split_0],
v[split_2],
uv[split_0],
mid_uv[split_0],
uv[split_2]);
if (num_split == 1) {
set_triangle(sub,
triangle_index++,
mid_v[split_0],
v[split_1],
v[split_2],
mid_uv[split_0],
uv[split_1],
uv[split_2]);
}
else {
/* Two edges have two segments.
* /|\
* / | \
* / |/ \
* ------- */
set_triangle(sub,
triangle_index++,
mid_v[split_0],
v[split_1],
mid_v[split_1],
mid_uv[split_0],
uv[split_1],
mid_uv[split_1]);
set_triangle(sub,
triangle_index++,
mid_v[split_0],
mid_v[split_1],
v[split_2],
mid_uv[split_0],
mid_uv[split_1],
uv[split_2]);
}
}
assert(triangle_index == sub.triangles_offset + sub.calc_num_triangles());
return;
}
const int inner_M = M - 2;
for (int j = 0; j < inner_M; j++) {
for (int i = 0; i < j + 1; i++) {
const int i_next = i + 1;
const int j_next = j + 1;
const float2 inner_uv = make_float2(d, d);
const int v0 = sub.get_inner_grid_vert_triangle(i, j);
const int v1 = sub.get_inner_grid_vert_triangle(i_next, j_next);
const int v2 = sub.get_inner_grid_vert_triangle(i, j_next);
const float2 uv0 = sub.map_uv(inner_uv + make_float2(i, j - i) * d);
const float2 uv1 = sub.map_uv(inner_uv + make_float2(i_next, j - i) * d);
const float2 uv2 = sub.map_uv(inner_uv + make_float2(i, j_next - i) * d);
set_vertex(sub, v0, uv0);
if (j == inner_M - 1) {
set_vertex(sub, v1, uv1);
set_vertex(sub, v2, uv2);
}
set_triangle(sub, triangle_index++, v0, v1, v2, uv0, uv1, uv2);
if (i < j) {
const int v3 = sub.get_inner_grid_vert_triangle(i_next, j);
const float2 uv3 = sub.map_uv(inner_uv + make_float2(i_next, j - i_next) * d);
set_vertex(sub, v3, uv3);
set_triangle(sub, triangle_index++, v0, v3, v1, uv0, uv3, uv1);
}
}
}
assert(triangle_index == sub.triangles_offset + inner_M * inner_M);
/* Stitch inner grid to edges. */
for (int edge = 0; edge < 3; edge++) {
const int outer_T = sub.edges[edge].edge->T;
const int inner_T = inner_M;
float2 inner_uv, outer_uv, inner_uv_step, outer_uv_step;
switch (edge) {
case 0:
inner_uv = make_float2(d, d);
outer_uv = make_float2(0.0f, 0.0f);
inner_uv_step = make_float2(d, 0.0f);
outer_uv_step = make_float2(1.0f / (float)outer_T, 0.0f);
break;
case 1:
inner_uv = make_float2(1.0f - 2.0f * d, d);
outer_uv = make_float2(1.0f, 0.0f);
inner_uv_step = make_float2(-d, d);
outer_uv_step = make_float2(-1.0f / (float)outer_T, 1.0f / (float)outer_T);
break;
case 2:
default:
inner_uv = make_float2(d, 1.0f - 2.0f * d);
outer_uv = make_float2(0.0f, 1.0f);
inner_uv_step = make_float2(0.0f, -d);
outer_uv_step = make_float2(0.0f, -1.0f / (float)outer_T);
break;
}
/* Stitch together two arrays of verts with triangles. At each step, we compare using
* the next verts on both sides, to find the split direction with the smallest
* diagonal, and use that in order to keep the triangle shape reasonable. */
for (size_t i = 0, j = 0; i < inner_T || j < outer_T;) {
const int v0 = sub.get_vert_along_grid_edge(edge, i);
const int v1 = sub.get_vert_along_edge(edge, j);
int v2;
const float2 uv0 = sub.map_uv(inner_uv);
const float2 uv1 = sub.map_uv(outer_uv);
float2 uv2;
if (j == outer_T) {
v2 = sub.get_vert_along_grid_edge(edge, ++i);
inner_uv += inner_uv_step;
uv2 = sub.map_uv(inner_uv);
}
else if (i == inner_T) {
v2 = sub.get_vert_along_edge(edge, ++j);
outer_uv += outer_uv_step;
uv2 = sub.map_uv(outer_uv);
}
else {
/* Length of diagonals. */
const int v2_a = sub.get_vert_along_edge(edge, j + 1);
const int v2_b = sub.get_vert_along_grid_edge(edge, i + 1);
const float len_a = len_squared(mesh_P[v0] - mesh_P[v2_a]);
const float len_b = len_squared(mesh_P[v1] - mesh_P[v2_b]);
/* Use smallest diagonal. */
if (len_a < len_b) {
v2 = v2_a;
outer_uv += outer_uv_step;
uv2 = sub.map_uv(outer_uv);
j++;
}
else {
v2 = v2_b;
inner_uv += inner_uv_step;
uv2 = sub.map_uv(inner_uv);
i++;
}
}
set_triangle(sub, triangle_index++, v0, v1, v2, uv0, uv1, uv2);
}
}
assert(triangle_index == sub.triangles_offset + sub.calc_num_triangles());
}
void EdgeDice::dice(const DiagSplit &split)
{
const size_t num_subpatches = split.get_num_subpatches();
@@ -402,14 +668,26 @@ void EdgeDice::dice(const DiagSplit &split)
* on these coordinates and they are unique assigned to a subpatch for determinism. */
parallel_for(blocked_range<size_t>(0, num_subpatches, 8), [&](const blocked_range<size_t> &r) {
for (size_t i = r.begin(); i != r.end(); i++) {
set_sides(split.get_subpatch(i));
const SubPatch &subpatch = split.get_subpatch(i);
if (subpatch.shape == SubPatch::TRIANGLE) {
tri_set_sides(subpatch);
}
else {
quad_set_sides(subpatch);
}
}
});
/* Inner vertex coordinates and triangles. */
parallel_for(blocked_range<size_t>(0, num_subpatches, 8), [&](const blocked_range<size_t> &r) {
for (size_t i = r.begin(); i != r.end(); i++) {
dice(split.get_subpatch(i));
const SubPatch &subpatch = split.get_subpatch(i);
if (subpatch.shape == SubPatch::TRIANGLE) {
tri_dice(subpatch);
}
else {
quad_dice(subpatch);
}
}
});
}

View File

@@ -56,7 +56,8 @@ class EdgeDice {
void dice(const DiagSplit &split);
protected:
void dice(const SubPatch &sub);
void tri_dice(const SubPatch &sub);
void quad_dice(const SubPatch &sub);
void set_vertex(const SubPatch &sub, const int index, const float2 uv);
void set_triangle(const SubPatch &sub,
@@ -73,7 +74,8 @@ class EdgeDice {
float3 eval_projected(const SubPatch &sub, const float2 uv);
void set_sides(const SubPatch &sub);
void tri_set_sides(const SubPatch &sub);
void quad_set_sides(const SubPatch &sub);
float quad_area(const float3 &a, const float3 &b, const float3 &c, const float3 &d);
float scale_factor(const SubPatch &sub, const int Mu, const int Mv);

View File

@@ -70,7 +70,9 @@ void DiagSplit::alloc_subpatch(SubPatch &&sub)
assert(sub.edges[0].edge->T >= 1);
assert(sub.edges[1].edge->T >= 1);
assert(sub.edges[2].edge->T >= 1);
assert(sub.edges[3].edge->T >= 1);
if (sub.shape == SubPatch::QUAD) {
assert(sub.edges[3].edge->T >= 1);
}
sub.inner_grid_vert_offset = alloc_verts(sub.calc_num_inner_verts());
sub.triangles_offset = num_triangles;
@@ -205,25 +207,32 @@ void DiagSplit::assign_edge_factor(SubEdge *edge,
void DiagSplit::resolve_edge_factors(const SubPatch &sub)
{
/* Compute edge factor if not already set. Or if DSPLIT_NON_UNIFORM and splitting is
* no longer possible because the opposite side can't be split. */
SubEdge *edge0 = sub.edges[0].edge;
SubEdge *edge1 = sub.edges[1].edge;
SubEdge *edge2 = sub.edges[2].edge;
SubEdge *edge3 = sub.edges[3].edge;
/* Compute edge factor if not already set. */
if (edge0->T == 0 || (edge0->must_split() && edge2->T == 1)) {
if (edge0->T == 0) {
assign_edge_factor(edge0, sub.patch, sub.uvs[0], sub.uvs[1], true);
}
if (edge1->T == 0 || (edge0->must_split() && edge3->T == 1)) {
if (edge1->T == 0) {
assign_edge_factor(edge1, sub.patch, sub.uvs[1], sub.uvs[2], true);
}
if (edge2->T == 0 || (edge0->must_split() && edge0->T == 1)) {
assign_edge_factor(edge2, sub.patch, sub.uvs[2], sub.uvs[3], true);
if (sub.shape == SubPatch::TRIANGLE) {
if (edge2->T == 0) {
assign_edge_factor(edge2, sub.patch, sub.uvs[2], sub.uvs[0], true);
}
}
if (edge3->T == 0 || (edge0->must_split() && edge1->T == 1)) {
assign_edge_factor(edge3, sub.patch, sub.uvs[3], sub.uvs[0], true);
else {
SubEdge *edge3 = sub.edges[3].edge;
if (edge2->T == 0) {
assign_edge_factor(edge2, sub.patch, sub.uvs[2], sub.uvs[3], true);
}
if (edge3->T == 0) {
assign_edge_factor(edge3, sub.patch, sub.uvs[3], sub.uvs[0], true);
}
}
}
@@ -369,6 +378,15 @@ void DiagSplit::split_quad(SubPatch &&sub)
return;
}
/* Split into triangles if one side must the split, and the opposite side has
* only a single segment. Then we can't do an even split across the quad. */
if ((split_u && (sub.edges[0].edge->T == 1 || sub.edges[2].edge->T == 1)) ||
(!split_u && (sub.edges[1].edge->T == 1 || sub.edges[3].edge->T == 1)))
{
split_quad_into_triangles(std::move(sub));
return;
}
/* Copy into new subpatches. */
SubPatch sub_a(sub);
SubPatch sub_b(sub);
@@ -501,6 +519,152 @@ void DiagSplit::split_quad(SubPatch &&sub)
split_quad(std::move(sub_b));
}
void DiagSplit::split_quad_into_triangles(SubPatch &&sub)
{
assert(sub.shape == SubPatch::QUAD);
/* Copy into new subpatches. */
SubPatch sub_a(sub);
SubPatch sub_b(sub);
sub_a.shape = SubPatch::TRIANGLE;
sub_b.shape = SubPatch::TRIANGLE;
for (int i = 0; i < 4; i++) {
sub_a.edges[i].own_edge = false;
sub_a.edges[i].own_vertex = false;
sub_b.edges[i].own_edge = false;
sub_b.edges[i].own_vertex = false;
}
const int split_edge_depth = std::max({sub.edges[0].edge->depth,
sub.edges[1].edge->depth,
sub.edges[2].edge->depth,
sub.edges[3].edge->depth});
sub_a.edges[0] = sub.edges[0];
sub_a.edges[1] = sub.edges[1];
sub_a.uvs[0] = sub.uvs[0];
sub_a.uvs[1] = sub.uvs[1];
sub_a.uvs[2] = sub.uvs[2];
alloc_edge(&sub_a.edges[2],
sub.edges[2].start_vert_index(),
sub.edges[0].start_vert_index(),
split_edge_depth,
true,
false);
sub_b.edges[1] = sub.edges[2];
sub_b.edges[2] = sub.edges[3];
sub_b.uvs[0] = sub.uvs[0];
sub_b.uvs[1] = sub.uvs[2];
sub_b.uvs[2] = sub.uvs[3];
alloc_edge(&sub_b.edges[0],
sub.edges[0].start_vert_index(),
sub.edges[2].start_vert_index(),
split_edge_depth,
true,
false);
/* Set T for new edge. */
assign_edge_factor(sub_b.edges[0].edge, sub.patch, sub.uvs[0], sub.uvs[2]);
/* Recurse */
split_triangle(std::move(sub_a));
split_triangle(std::move(sub_b));
}
void DiagSplit::split_triangle(SubPatch &&sub)
{
assert(sub.shape == SubPatch::TRIANGLE);
/* Set edge factors if we haven't already. */
resolve_edge_factors(sub);
const bool do_split = sub.edges[0].edge->must_split() || sub.edges[1].edge->must_split() ||
sub.edges[2].edge->must_split();
if (!do_split) {
/* Add the unsplit subpatch. */
alloc_subpatch(std::move(sub));
return;
}
/* Slight bias so that for equal length edges, we get consistent results across
* platforms rather than choice being decided by precision. */
const float bias = 1.00012345f;
/* Pick longest edge that must be split. */
float max_length = 0;
int split_index_0 = 0;
for (int i = 0; i < 3; i++) {
if (sub.edges[i].edge->must_split() && sub.edges[i].edge->length > max_length) {
split_index_0 = i;
max_length = sub.edges[i].edge->length * bias;
}
}
/* Copy into new subpatches. */
SubPatch sub_a(sub);
SubPatch sub_b(sub);
for (int i = 0; i < 4; i++) {
sub_a.edges[i].own_edge = false;
sub_a.edges[i].own_vertex = false;
sub_b.edges[i].own_edge = false;
sub_b.edges[i].own_vertex = false;
}
const int split_index_1 = (split_index_0 + 1) % 3;
const int split_index_2 = (split_index_0 + 2) % 3;
sub_a.edges[2] = sub.edges[split_index_2];
sub_b.edges[1] = sub.edges[split_index_1];
/*
* uv_opposite
* 2 2
* / | | \
* / | | \
* / A | | B \
* / | | \
* 0 --- 1 0 --- 1
* uv_split
*/
/* Allocate new edges and vertices. */
const float2 uv_split = split_edge(sub.patch,
&sub.edges[split_index_0],
&sub_a.edges[0],
&sub_b.edges[0],
sub.uvs[split_index_0],
sub.uvs[split_index_1]);
/* Set UVs. */
sub_a.uvs[0] = sub.uvs[split_index_0];
sub_a.uvs[1] = uv_split;
sub_a.uvs[2] = sub.uvs[split_index_2];
sub_b.uvs[0] = uv_split;
sub_b.uvs[1] = sub.uvs[split_index_1];
sub_b.uvs[2] = sub.uvs[split_index_2];
/* Create new edge */
const int vsplit = sub.edges[split_index_0].mid_vert_index();
const int vopposite = sub.edges[split_index_2].start_vert_index();
const int split_edge_depth = sub.edges[split_index_0].edge->depth + 1;
alloc_edge(&sub_a.edges[1], vsplit, vopposite, split_edge_depth, true, false);
alloc_edge(&sub_b.edges[2], vopposite, vsplit, split_edge_depth, true, false);
/* Set T for split edge. */
const float2 uv_opposite = sub.uvs[split_index_2];
assign_edge_factor(sub_a.edges[1].edge, sub.patch, uv_split, uv_opposite);
/* Recurse */
split_triangle(std::move(sub_a));
split_triangle(std::move(sub_b));
}
void DiagSplit::split_quad(const Mesh::SubdFace &face, const int face_index, const Patch *patch)
{
const int *subd_face_corners = params.mesh->get_subd_face_corners().data();

View File

@@ -70,6 +70,8 @@ class DiagSplit {
float2 uv_start,
float2 uv_end);
void split_quad(SubPatch &&sub);
void split_triangle(SubPatch &&sub);
void split_quad_into_triangles(SubPatch &&sub);
void split_quad(const Mesh::SubdFace &face, const int face_index, const Patch *patch);
void split_ngon(const Mesh::SubdFace &face,
const int face_index,

View File

@@ -95,6 +95,8 @@ class SubPatch {
/* Face and corner. */
int face_index = 0;
int corner = 0;
/* Is a triangular patch instead of a quad patch? */
enum { TRIANGLE, QUAD } shape = QUAD;
/* Vertex indices for inner grid start at this index. */
int inner_grid_vert_offset = 0;
/* Triangle indices. */
@@ -138,13 +140,22 @@ class SubPatch {
};
/*
* edges[2]
* uv3 ←------------ uv2
* |
* edges[3] | | edges[1]
* |
* uv0 ------------→ uv1
* edges[0]
* edge2
* uv3 ←------------ uv2
* |
* edge3 | | edge1
* |
* uv0 ------------→ uv1
* edge0
*
* uv2
* | \
* | \
* edge2 | \ edge1
* | \
* ↓ \
* uv0 --→ uv1
* edge0
*/
/* UV within patch, counter-clockwise starting from uv (0, 0) towards (1, 0) etc. */
@@ -160,6 +171,16 @@ class SubPatch {
int calc_num_inner_verts() const
{
if (shape == TRIANGLE) {
const int M = max(max(edges[0].edge->T, edges[1].edge->T), edges[2].edge->T);
if (M <= 2) {
/* No inner grid. */
return 0;
}
/* 1 + 2 + .. + M-1 */
return M * (M - 1) / 2;
}
const int Mu = max(edges[0].edge->T, edges[2].edge->T);
const int Mv = max(edges[3].edge->T, edges[1].edge->T);
return (Mu - 1) * (Mv - 1);
@@ -167,6 +188,22 @@ class SubPatch {
int calc_num_triangles() const
{
if (shape == TRIANGLE) {
const int M = max(max(edges[0].edge->T, edges[1].edge->T), edges[2].edge->T);
if (M == 1) {
return 1;
}
if (M == 2) {
return edges[0].edge->T + edges[1].edge->T + edges[2].edge->T - 2;
}
const int inner_M = M - 2;
const int inner_triangles = inner_M * inner_M;
const int edge_triangles = edges[0].edge->T + edges[1].edge->T + edges[2].edge->T +
inner_M * 3;
return inner_triangles + edge_triangles;
}
const int Mu = max(edges[0].edge->T, edges[2].edge->T);
const int Mv = max(edges[3].edge->T, edges[1].edge->T);
@@ -194,8 +231,39 @@ class SubPatch {
return get_vert_along_edge(edge, edges[edge].edge->T - n);
}
int get_inner_grid_vert_triangle(int i, int j) const
{
/* Rowows (1 + 2 + .. + j), and column i. */
const int offset = j * (j + 1) / 2 + i;
assert(offset < calc_num_inner_verts());
return inner_grid_vert_offset + offset;
}
int get_vert_along_grid_edge(const int edge, const int n) const
{
if (shape == TRIANGLE) {
const int M = max(max(edges[0].edge->T, edges[1].edge->T), edges[2].edge->T);
const int inner_M = M - 2;
assert(M >= 2);
switch (edge) {
case 0: {
return get_inner_grid_vert_triangle(n, n);
}
case 1: {
return get_inner_grid_vert_triangle(inner_M - n, inner_M);
}
case 2: {
return get_inner_grid_vert_triangle(0, inner_M - n);
}
default:
assert(0);
break;
}
return -1;
}
const int Mu = max(edges[0].edge->T, edges[2].edge->T);
const int Mv = max(edges[3].edge->T, edges[1].edge->T);
@@ -227,6 +295,12 @@ class SubPatch {
float2 map_uv(float2 uv) const
{
/* Map UV from subpatch to patch parametric coordinates. */
if (shape == TRIANGLE) {
return clamp((1.0f - uv.x - uv.y) * uvs[0] + uv.x * uvs[1] + uv.y * uvs[2],
zero_float2(),
one_float2());
}
const float2 d0 = interp(uvs[0], uvs[3], uv.y);
const float2 d1 = interp(uvs[1], uvs[2], uv.y);
return clamp(interp(d0, d1, uv.x), zero_float2(), one_float2());

View File

@@ -4,7 +4,7 @@
#pragma once
#include <algorithm>
#include <algorithm> // IWYU pragma: export
CCL_NAMESPACE_BEGIN

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.