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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0715f26581e01a11c332cdae59f15616635339de74530e8a894dde1be2da60e0
size 30580
oid sha256:24824ddd837c2921921817dd30159217d31f905005e2a21b3d264a985787f0b5
size 30511

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd2d001fa967bac87f6e4aed835e83e6c3cc000a37a62d6e7661b9bb7fb5e658
size 26121
oid sha256:dafa90dd5c8474d7e505515aff6c2a6d6917abd40e216494d8d6e37e1c894000
size 26053

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:28059841afd02a3030197e250b652c6a3c28fb0485cd87f27a28b746c2208c1d
size 29405
oid sha256:63882d0296fe5ca23fd97af91bd936fd87c1640d91ed11984f2ab1282de56804
size 30618

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8c87444559c09146aee894185b479a771444d36ddf898988146f5884b249320
size 27961
oid sha256:8eea87256e9949a318373ffa984c08d9afa84bac1d17d3ac14242b46aa1e151d
size 28092