Cycles: new Microfacet-based Hair BSDF with elliptical cross-section support #105600

Merged
Weizhen Huang merged 114 commits from weizhen/blender:microfacet_hair into main 2023-08-18 12:46:20 +02:00
14 changed files with 297 additions and 30 deletions
Showing only changes of commit 93e0559f54 - Show all commits

@ -1 +1 @@
Subproject commit e398d3c4969a37ae2ecff388344dd780bc1cfe82
Subproject commit 7084c4ecd97d93459d9d23fd90f81589b09be5df

@ -1 +1 @@
Subproject commit 90c87dd771e027e0ffa157a0e294399bfd605d99
Subproject commit 042c799b7aef9634a33d24e78d4922706aca9a2b

@ -1 +1 @@
Subproject commit 96143b1a8b037ea3c81f065f557025db9fe1ace3
Subproject commit bdcfdd47ec3451822b21d1cff2ea2db751093c9a

View File

@ -520,6 +520,13 @@ void calculate_tangents(Span<float3> positions, bool is_cyclic, MutableSpan<floa
*/
void calculate_normals_minimum(Span<float3> tangents, bool cyclic, MutableSpan<float3> normals);
/**
* Calculate curvature vectors. TODO: more description. */
void calculate_curvature_vectors(Span<float3> positions,
Span<float3> tangents,
bool cyclic,
MutableSpan<float3> normals);
/**
* Calculate a vector perpendicular to every tangent on the X-Y plane (unless the tangent is
* vertical, in that case use the X direction).
@ -696,6 +703,16 @@ void interpolate_to_evaluated(GSpan src, Span<int> evaluated_offsets, GMutableSp
namespace catmull_rom {
void calculate_tangents(Span<float3> positions,
bool is_cyclic,
int resolution,
MutableSpan<float3> tangents);
void calculate_normals(Span<float3> positions,
bool is_cyclic,
int resolution,
Span<float3> tangents,
MutableSpan<float3> normals);
/**
* Calculate the number of evaluated points that #interpolate_to_evaluated is expected to produce.
* \param points_num: The number of points in the curve.
@ -719,6 +736,8 @@ void interpolate_to_evaluated(const GSpan src,
GMutableSpan dst);
void calculate_basis(const float parameter, float4 &r_weights);
void calculate_basis_derivative(const float parameter, float4 &r_weights);
void calculate_basis_derivative2(const float parameter, float4 &r_weights);
/**
* Interpolate the control point values for the given parameter on the piecewise segment.
@ -727,11 +746,24 @@ void calculate_basis(const float parameter, float4 &r_weights);
* \param parameter: Parameter in range [0, 1] to compute the interpolation for.
*/
template<typename T>
T interpolate(const T &a, const T &b, const T &c, const T &d, const float parameter)
T interpolate(
const int order, const T &a, const T &b, const T &c, const T &d, const float parameter)
{
BLI_assert(0.0f <= parameter && parameter <= 1.0f);
float4 n;
calculate_basis(parameter, n);
switch (order) {
case 1:
calculate_basis_derivative(parameter, n);
break;
case 2:
calculate_basis_derivative2(parameter, n);
break;
default:
calculate_basis(parameter, n);
break;
}
if constexpr (is_same_any_v<T, float, float2, float3>) {
/* Save multiplications by adjusting weights after mix. */
return 0.5f * attribute_math::mix4<T>(n, a, b, c, d);

View File

@ -31,13 +31,37 @@ void calculate_basis(const float parameter, float4 &r_weights)
r_weights[3] = -s * t * t;
}
/* Adapted from Cycles #catmull_rom_basis_derivative function. */
void calculate_basis_derivative(const float parameter, float4 &r_weights)
{
const float t = parameter;
const float s = 1.0f - parameter;
r_weights[0] = s * (2.0f * t - s);
r_weights[1] = t * (9.0f * t - 10.0f);
r_weights[2] = -s * (9.0f * s - 10.0f);
r_weights[3] = t * (t - 2.0f * s);
}
/* Adapted from Cycles #catmull_rom_basis_derivative2 function. */
void calculate_basis_derivative2(const float parameter, float4 &r_weights)
{
const float t = parameter;
const float s = 1.0f - parameter;
r_weights[0] = -6.0f * t + 4.0f;
r_weights[1] = 18.0f * t - 10.0f;
r_weights[2] = 18.0f * s - 10.0f;
r_weights[3] = 6.0f * t - 2.0f;
}
template<typename T>
static void evaluate_segment(const T &a, const T &b, const T &c, const T &d, MutableSpan<T> dst)
static void evaluate_segment(
const int order, const T &a, const T &b, const T &c, const T &d, MutableSpan<T> dst)
{
const float step = 1.0f / dst.size();
dst.first() = b;
for (const int i : dst.index_range().drop_front(1)) {
dst[i] = interpolate<T>(a, b, c, d, i * step);
IndexRange index_range = (order == 0) ? dst.index_range().drop_front(1) : dst.index_range();
for (const int i : index_range) {
dst[i] = interpolate(order, a, b, c, d, i * step);
}
}
@ -45,9 +69,11 @@ static void evaluate_segment(const T &a, const T &b, const T &c, const T &d, Mut
* \param range_fn: Returns an index range describing where in the #dst span each segment should be
* evaluated to, and how many points to add to it. This is used to avoid the need to allocate an
* actual offsets array in typical evaluation use cases where the resolution is per-curve.
* \param order: The order of the derivative. order == 0 is the default.
*/
template<typename T, typename RangeForSegmentFn>
static void interpolate_to_evaluated(const Span<T> src,
static void interpolate_to_evaluated(const int order,
const Span<T> src,
const bool cyclic,
const RangeForSegmentFn &range_fn,
MutableSpan<T> dst)
@ -59,20 +85,30 @@ static void interpolate_to_evaluated(const Span<T> src,
* - Finally evaluate all of the segments in the middle in parallel. */
if (src.size() == 1) {
dst.first() = src.first();
switch (order) {
case 1:
dst.first() = float3(0.0f, 0.0f, 1.0f);
break;
case 2:
dst.first() = float3(1.0f, 0.0f, 0.0f);
break;
default:
dst.first() = src.first();
break;
}
return;
}
const IndexRange first = range_fn(0);
if (src.size() == 2) {
evaluate_segment(src.first(), src.first(), src.last(), src.last(), dst.slice(first));
evaluate_segment(order, src.first(), src.first(), src.last(), src.last(), dst.slice(first));
if (cyclic) {
const IndexRange last = range_fn(1);
evaluate_segment(src.last(), src.last(), src.first(), src.first(), dst.slice(last));
evaluate_segment(order, src.last(), src.last(), src.first(), src.first(), dst.slice(last));
}
else {
dst.last() = src.last();
dst.last() = interpolate(order, src.first(), src.first(), src.last(), src.last(), 1);
}
return;
}
@ -80,17 +116,19 @@ static void interpolate_to_evaluated(const Span<T> src,
const IndexRange second_to_last = range_fn(src.index_range().last(1));
const IndexRange last = range_fn(src.index_range().last());
if (cyclic) {
evaluate_segment(src.last(), src[0], src[1], src[2], dst.slice(first));
evaluate_segment(src.last(2), src.last(1), src.last(), src.first(), dst.slice(second_to_last));
evaluate_segment(src.last(1), src.last(), src[0], src[1], dst.slice(last));
evaluate_segment(order, src.last(), src[0], src[1], src[2], dst.slice(first));
evaluate_segment(
order, src.last(2), src.last(1), src.last(), src.first(), dst.slice(second_to_last));
evaluate_segment(order, src.last(1), src.last(), src[0], src[1], dst.slice(last));
}
else {
evaluate_segment(src[0], src[0], src[1], src[2], dst.slice(first));
evaluate_segment(src.last(2), src.last(1), src.last(), src.last(), dst.slice(second_to_last));
evaluate_segment(order, src[0], src[0], src[1], src[2], dst.slice(first));
evaluate_segment(
order, src.last(2), src.last(1), src.last(), src.last(), dst.slice(second_to_last));
/* For non-cyclic curves, the last segment should always just have a single point. We could
* assert that the size of the provided range is 1 here, but that would require specializing
* the #range_fn implementation for the last point, which may have a performance cost. */
dst.last() = src.last();
dst.last() = interpolate(order, src.last(2), src.last(1), src.last(), src.last(), 1);
}
/* Evaluate every segment that isn't the first or last. */
@ -98,7 +136,7 @@ static void interpolate_to_evaluated(const Span<T> src,
threading::parallel_for(inner_range, 512, [&](IndexRange range) {
for (const int i : range) {
const IndexRange segment = range_fn(i);
evaluate_segment(src[i - 1], src[i], src[i + 1], src[i + 2], dst.slice(segment));
evaluate_segment(order, src[i - 1], src[i], src[i + 1], src[i + 2], dst.slice(segment));
}
});
}
@ -112,6 +150,7 @@ static void interpolate_to_evaluated(const Span<T> src,
{
BLI_assert(dst.size() == calculate_evaluated_num(src.size(), cyclic, resolution));
interpolate_to_evaluated(
0,
src,
cyclic,
[resolution](const int segment_i) -> IndexRange {
@ -128,6 +167,7 @@ static void interpolate_to_evaluated(const Span<T> src,
{
interpolate_to_evaluated(
0,
src,
cyclic,
[evaluated_offsets](const int segment_i) -> IndexRange {
@ -158,4 +198,76 @@ void interpolate_to_evaluated(const GSpan src,
});
}
/* Calculate analytical tangents from the first derivative. */
void calculate_tangents(const Span<float3> positions,
const bool is_cyclic,
const int resolution,
MutableSpan<float3> evaluated_tangents)
{
interpolate_to_evaluated(
1,
positions,
is_cyclic,
[resolution](const int segment_i) -> IndexRange {
return {segment_i * resolution, resolution};
},
evaluated_tangents);
for (const int i : evaluated_tangents.index_range()) {
evaluated_tangents[i] = math::normalize(evaluated_tangents[i]);
}
}
/* Calculate analytical normals from the first and the second derivative. */
void calculate_normals(const Span<float3> positions,
const bool is_cyclic,
const int resolution,
const Span<float3> evaluated_tangents,
MutableSpan<float3> evaluated_normals)
{
/* Compute the second derivative (r'') of the control points, evaluated at t = 0. */
interpolate_to_evaluated(
2,
positions,
is_cyclic,
[resolution](const int segment_i) -> IndexRange {
return {segment_i * resolution, 1};
},
evaluated_normals);
/* The normals along the segment is interpolated between the control points. */
const float step = 1.0f / resolution;
for (const int i : positions.index_range()) {
if (i == positions.size() - 1 && !is_cyclic) {
break;
}
const float3 last_normal = evaluated_normals[i * resolution];
float3 next_normal = (i == positions.size() - 1) ? evaluated_normals[0] :
evaluated_normals[(i + 1) * resolution];
if (!is_cyclic && math::dot(last_normal, next_normal) < 0) {
/* Prevent sudden normal changes. */
next_normal = -next_normal;
evaluated_normals[(i + 1) * resolution] = next_normal;
}
for (int j = 1; j < resolution; j++) {
const float t = j * step;
evaluated_normals[i * resolution + j] = (1.0f - t) * last_normal + t * next_normal;
}
}
MutableSpan<float3> binormals(evaluated_normals);
/* Compute evaluated_normals of the control points. */
for (const int i : evaluated_normals.index_range()) {
/* B = normalize(r' x r'') */
binormals[i] = math::cross(evaluated_tangents[i], evaluated_normals[i]);
/* N = B x T */
evaluated_normals[i] = math::normalize(math::cross(binormals[i], evaluated_tangents[i]));
}
if (positions.size() == 1) {
return;
}
}
} // namespace blender::bke::curves::catmull_rom

View File

@ -50,6 +50,7 @@ static HandleType handle_type_from_legacy(const uint8_t handle_type_legacy)
static NormalMode normal_mode_from_legacy(const short twist_mode)
{
/* TODO: add curvature vector mode. */
switch (twist_mode) {
case CU_TWIST_Z_UP:
case CU_TWIST_TANGENT:

View File

@ -199,4 +199,77 @@ void calculate_normals_minimum(const Span<float3> tangents,
}
}
void calculate_curvature_vectors(const Span<float3> positions,
const Span<float3> tangents,
const bool cyclic,
MutableSpan<float3> normals)
{
const int size = positions.size();
BLI_assert(normals.size() == size);
BLI_assert(tangents.size() == size);
if (normals.is_empty()) {
return;
}
const float epsilon = 1e-4f;
/* Fill in the first normal, in case the following computations are invalid. */
const float3 &first_tangent = tangents.first();
if (fabs(first_tangent.x) + fabs(first_tangent.y) < epsilon) {
normals.first() = {1.0f, 0.0f, 0.0f};
}
else {
normals.first() = math::normalize(float3(first_tangent.y, -first_tangent.x, 0.0f));
}
/* Computing normals from the second point to the second to last point, or from the first point
* to the last point for cyclic curve. */
/* TODO: maybe better to compute only for control points. */
int first_valid_normal_index = -1;
const int start_index = cyclic ? 0 : 1;
const int end_index = cyclic ? size - 1 : size - 2;
for (int i = start_index; i < end_index; i++) {
const float3 previous_segment = (i == 0) ? positions.last() - positions.first() :
positions[i - 1] - positions[i];
const float3 next_segment = (i == size - 1) ? positions.first() - positions.last() :
positions[i + 1] - positions[i];
const float3 binormal = math::cross(math::normalize(next_segment),
math::normalize(previous_segment));
float normal_len;
float3 normal = math::normalize_and_get_length(math::cross(tangents[i], binormal), normal_len);
if (normal_len > epsilon) {
if (first_valid_normal_index != -1 && math::dot(normal, normals[i - 1]) < 0) {
/* Prevent sudden normal changes. */
normal = -normal;
}
if (first_valid_normal_index == -1) {
first_valid_normal_index = i;
}
normals[i] = normal;
}
else {
if (first_valid_normal_index != -1) {
normals[i] = normals[i - 1];
}
}
}
if (first_valid_normal_index == -1) {
normals.fill(normals.first());
}
else {
const float3 &first_valid_normal = normals[first_valid_normal_index];
/* If the first few normals are invalid, use the normal from the first point with a valid
* normal. */
normals.take_front(first_valid_normal_index).fill(first_valid_normal);
if (!cyclic) {
/* The last normal is the same as the second to last normal. The normal is already filled if
* the curve is cyclic. */
normals.last() = normals[size - 2];
}
}
}
} // namespace blender::bke::curves::poly

View File

@ -673,17 +673,31 @@ Span<float3> CurvesGeometry::evaluated_tangents() const
{
this->runtime->tangent_cache_mutex.ensure([&]() {
const Span<float3> evaluated_positions = this->evaluated_positions();
Span<float3> positions = this->positions();
VArray<int> resolution = this->resolution();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> types = this->curve_types();
this->runtime->evaluated_tangent_cache.resize(this->evaluated_points_num());
MutableSpan<float3> tangents = this->runtime->evaluated_tangent_cache;
threading::parallel_for(this->curves_range(), 128, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const IndexRange points = this->points_for_curve(curve_index);
const IndexRange evaluated_points = this->evaluated_points_for_curve(curve_index);
curves::poly::calculate_tangents(evaluated_positions.slice(evaluated_points),
cyclic[curve_index],
tangents.slice(evaluated_points));
switch (types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
curves::catmull_rom::calculate_tangents(positions.slice(points),
cyclic[curve_index],
resolution[curve_index],
tangents.slice(evaluated_points));
break;
default:
curves::poly::calculate_tangents(evaluated_positions.slice(evaluated_points),
cyclic[curve_index],
tangents.slice(evaluated_points));
break;
}
}
});
@ -720,6 +734,7 @@ Span<float3> CurvesGeometry::evaluated_tangents() const
});
}
});
return this->runtime->evaluated_tangent_cache;
}
@ -737,6 +752,8 @@ Span<float3> CurvesGeometry::evaluated_normals() const
this->runtime->normal_cache_mutex.ensure([&]() {
const Span<float3> evaluated_tangents = this->evaluated_tangents();
const VArray<bool> cyclic = this->cyclic();
Span<float3> positions = this->positions();
VArray<int> resolution = this->resolution();
const VArray<int8_t> normal_mode = this->normal_mode();
const VArray<int8_t> types = this->curve_types();
const VArray<float> tilt = this->tilt();
@ -749,6 +766,7 @@ Span<float3> CurvesGeometry::evaluated_normals() const
Vector<float> evaluated_tilts;
for (const int curve_index : curves_range) {
const IndexRange points = this->points_for_curve(curve_index);
const IndexRange evaluated_points = this->evaluated_points_for_curve(curve_index);
switch (normal_mode[curve_index]) {
case NORMAL_MODE_Z_UP:
@ -760,6 +778,25 @@ Span<float3> CurvesGeometry::evaluated_normals() const
cyclic[curve_index],
evaluated_normals.slice(evaluated_points));
break;
case NORMAL_MODE_CURVATURE_VECTOR:
switch (types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
curves::catmull_rom::calculate_normals(positions.slice(points),
cyclic[curve_index],
resolution[curve_index],
evaluated_tangents.slice(evaluated_points),
evaluated_normals.slice(evaluated_points));
break;
default:
const Span<float3> evaluated_positions = this->evaluated_positions();
curves::poly::calculate_curvature_vectors(
evaluated_positions.slice(evaluated_points),
evaluated_tangents.slice(evaluated_points),
cyclic[curve_index],
evaluated_normals.slice(evaluated_points));
break;
}
break;
}
/* If the "tilt" attribute exists, rotate the normals around the tangents by the

View File

@ -199,6 +199,10 @@ static Array<float3> curve_normal_point_domain(const bke::CurvesGeometry &curves
case NORMAL_MODE_MINIMUM_TWIST:
bke::curves::poly::calculate_normals_minimum(nurbs_tangents, cyclic, curve_normals);
break;
case NORMAL_MODE_CURVATURE_VECTOR:
bke::curves::poly::calculate_curvature_vectors(
curve_positions, nurbs_tangents, cyclic, curve_normals);
break;
}
break;
}
@ -504,7 +508,7 @@ static ComponentAttributeProviders create_attribute_providers_for_curve()
static const fn::CustomMF_SI_SO<int8_t, int8_t> normal_mode_clamp{
"Normal Mode Validate",
[](int8_t value) {
return std::clamp<int8_t>(value, NORMAL_MODE_MINIMUM_TWIST, NORMAL_MODE_Z_UP);
return std::clamp<int8_t>(value, NORMAL_MODE_MINIMUM_TWIST, NORMAL_MODE_CURVATURE_VECTOR);
},
fn::CustomMF_presets::AllSpanOrSingle()};
static BuiltinCustomDataLayerProvider normal_mode("normal_mode",

View File

@ -373,6 +373,7 @@ AddCurvesOnMeshOutputs add_curves_on_mesh(CurvesGeometry &curves,
}
curves.fill_curve_types(new_curves_range, CURVE_TYPE_CATMULL_ROM);
curves.normal_mode_for_write().fill(NORMAL_MODE_CURVATURE_VECTOR);
/* Explicitly set all other attributes besides those processed above to default values. */
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();

View File

@ -262,11 +262,12 @@ static T interpolate_catmull_rom(const Span<T> src_data,
if (i3 == src_data.size()) {
i3 = src_cyclic ? 0 : insertion_point.next_index;
}
return bke::curves::catmull_rom::interpolate<T>(src_data[i0],
src_data[insertion_point.index],
src_data[insertion_point.next_index],
src_data[i3],
insertion_point.parameter);
return bke::curves::catmull_rom::interpolate(0,
src_data[i0],
src_data[insertion_point.index],
src_data[insertion_point.next_index],
src_data[i3],
insertion_point.parameter);
}
static bke::curves::bezier::Insertion knot_insert_bezier(

View File

@ -80,6 +80,7 @@ typedef enum KnotsMode {
typedef enum NormalMode {
NORMAL_MODE_MINIMUM_TWIST = 0,
NORMAL_MODE_Z_UP = 1,
NORMAL_MODE_CURVATURE_VECTOR = 2,
} NormalMode;
/**

View File

@ -38,6 +38,11 @@ const EnumPropertyItem rna_enum_curve_normal_modes[] = {
"Z Up",
"Calculate normals perpendicular to the Z axis and the curve tangent. If a series of points "
"is vertical, the X axis is used"},
{NORMAL_MODE_CURVATURE_VECTOR,
"CURVATURE_VECTOR",
ICON_NONE,
"Curvature Vector",
"Calculate normals aligned with the local curvature vectors."},
{0, NULL, 0, NULL, NULL},
};

@ -1 +1 @@
Subproject commit fdfa2fcb9495d87571f2dfe2ae9fa0e032536600
Subproject commit e1744b9bd82527cf7e8af63362b61bd309b5711b