455 lines
17 KiB
C
455 lines
17 KiB
C
#pragma once
|
|
|
|
#include "diffvg.h"
|
|
#include "shape.h"
|
|
#include "scene.h"
|
|
#include "vector.h"
|
|
#include "cdf.h"
|
|
|
|
struct PathBoundaryData {
|
|
int base_point_id;
|
|
int point_id;
|
|
float t;
|
|
};
|
|
|
|
struct BoundaryData {
|
|
PathBoundaryData path;
|
|
bool is_stroke;
|
|
};
|
|
|
|
DEVICE
|
|
Vector2f sample_boundary(const Circle &circle,
|
|
float t,
|
|
Vector2f &normal,
|
|
float &pdf,
|
|
BoundaryData &,
|
|
float stroke_perturb_direction,
|
|
float stroke_radius) {
|
|
// Parametric form of a circle (t in [0, 1)):
|
|
// x = center.x + r * cos(2pi * t)
|
|
// y = center.y + r * sin(2pi * t)
|
|
auto offset = Vector2f{
|
|
circle.radius * cos(2 * float(M_PI) * t),
|
|
circle.radius * sin(2 * float(M_PI) * t)
|
|
};
|
|
normal = normalize(offset);
|
|
pdf /= (2 * float(M_PI) * circle.radius);
|
|
auto ret = circle.center + offset;
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
DEVICE
|
|
Vector2f sample_boundary(const Ellipse &ellipse,
|
|
float t,
|
|
Vector2f &normal,
|
|
float &pdf,
|
|
BoundaryData &,
|
|
float stroke_perturb_direction,
|
|
float stroke_radius) {
|
|
// Parametric form of a ellipse (t in [0, 1)):
|
|
// x = center.x + r.x * cos(2pi * t)
|
|
// y = center.y + r.y * sin(2pi * t)
|
|
const auto &r = ellipse.radius;
|
|
auto offset = Vector2f{
|
|
r.x * cos(2 * float(M_PI) * t),
|
|
r.y * sin(2 * float(M_PI) * t)
|
|
};
|
|
auto dxdt = -r.x * sin(2 * float(M_PI) * t) * 2 * float(M_PI);
|
|
auto dydt = r.y * cos(2 * float(M_PI) * t) * 2 * float(M_PI);
|
|
// tangent is normalize(dxdt, dydt)
|
|
normal = normalize(Vector2f{dydt, -dxdt});
|
|
pdf /= sqrt(square(dxdt) + square(dydt));
|
|
auto ret = ellipse.center + offset;
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
DEVICE
|
|
Vector2f sample_boundary(const Path &path,
|
|
const float *path_length_cdf,
|
|
const float *path_length_pmf,
|
|
const int *point_id_map,
|
|
float path_length,
|
|
float t,
|
|
Vector2f &normal,
|
|
float &pdf,
|
|
BoundaryData &data,
|
|
float stroke_perturb_direction,
|
|
float stroke_radius) {
|
|
if (stroke_perturb_direction != 0.f && !path.is_closed) {
|
|
// We need to samples the "caps" of the path
|
|
// length of a cap is pi * abs(stroke_perturb_direction)
|
|
// there are two caps
|
|
auto cap_length = 0.f;
|
|
if (path.thickness != nullptr) {
|
|
auto r0 = path.thickness[0];
|
|
auto r1 = path.thickness[path.num_points - 1];
|
|
cap_length = float(M_PI) * (r0 + r1);
|
|
} else {
|
|
cap_length = 2 * float(M_PI) * stroke_radius;
|
|
}
|
|
auto cap_prob = cap_length / (cap_length + path_length);
|
|
if (t < cap_prob) {
|
|
t = t / cap_prob;
|
|
pdf *= cap_prob;
|
|
auto r0 = stroke_radius;
|
|
auto r1 = stroke_radius;
|
|
if (path.thickness != nullptr) {
|
|
r0 = path.thickness[0];
|
|
r1 = path.thickness[path.num_points - 1];
|
|
}
|
|
// HACK: in theory we want to compute the tangent and
|
|
// sample the hemi-circle, but here we just sample the
|
|
// full circle since it's less typing
|
|
if (stroke_perturb_direction < 0) {
|
|
// Sample the cap at the beginning
|
|
auto p0 = Vector2f{path.points[0], path.points[1]};
|
|
auto offset = Vector2f{
|
|
r0 * cos(2 * float(M_PI) * t),
|
|
r0 * sin(2 * float(M_PI) * t)
|
|
};
|
|
normal = normalize(offset);
|
|
pdf /= (2 * float(M_PI) * r0);
|
|
data.path.base_point_id = 0;
|
|
data.path.point_id = 0;
|
|
data.path.t = 0;
|
|
return p0 + offset;
|
|
} else {
|
|
// Sample the cap at the end
|
|
auto p0 = Vector2f{path.points[2 * (path.num_points - 1)],
|
|
path.points[2 * (path.num_points - 1) + 1]};
|
|
auto offset = Vector2f{
|
|
r1 * cos(2 * float(M_PI) * t),
|
|
r1 * sin(2 * float(M_PI) * t)
|
|
};
|
|
normal = normalize(offset);
|
|
pdf /= (2 * float(M_PI) * r1);
|
|
data.path.base_point_id = path.num_base_points - 1;
|
|
data.path.point_id = path.num_points - 2 -
|
|
path.num_control_points[data.path.base_point_id];
|
|
data.path.t = 1;
|
|
return p0 + offset;
|
|
}
|
|
} else {
|
|
t = (t - cap_prob) / (1 - cap_prob);
|
|
pdf *= (1 - cap_prob);
|
|
}
|
|
}
|
|
// Binary search on path_length_cdf
|
|
auto sample_id = sample(path_length_cdf,
|
|
path.num_base_points,
|
|
t,
|
|
&t);
|
|
assert(sample_id >= 0 && sample_id < path.num_base_points);
|
|
auto point_id = point_id_map[sample_id];
|
|
if (path.num_control_points[sample_id] == 0) {
|
|
// Straight line
|
|
auto i0 = point_id;
|
|
auto i1 = (i0 + 1) % path.num_points;
|
|
assert(i0 < path.num_points);
|
|
auto p0 = Vector2f{path.points[2 * i0], path.points[2 * i0 + 1]};
|
|
auto p1 = Vector2f{path.points[2 * i1], path.points[2 * i1 + 1]};
|
|
data.path.base_point_id = sample_id;
|
|
data.path.point_id = point_id;
|
|
data.path.t = t;
|
|
if (t < -1e-3f || t > 1+1e-3f) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
auto tangent = (p1 - p0);
|
|
auto tan_len = length(tangent);
|
|
if (tan_len == 0) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
normal = Vector2f{-tangent.y, tangent.x} / tan_len;
|
|
// length of tangent is the Jacobian of the sampling transformation
|
|
pdf *= path_length_pmf[sample_id] / tan_len;
|
|
auto ret = p0 + t * (p1 - p0);
|
|
if (stroke_perturb_direction != 0.f) {
|
|
auto r0 = stroke_radius;
|
|
auto r1 = stroke_radius;
|
|
if (path.thickness != nullptr) {
|
|
r0 = path.thickness[i0];
|
|
r1 = path.thickness[i1];
|
|
}
|
|
auto r = r0 + t * (r1 - r0);
|
|
ret += stroke_perturb_direction * r * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
} else if (path.num_control_points[sample_id] == 1) {
|
|
// Quadratic Bezier curve
|
|
auto i0 = point_id;
|
|
auto i1 = i0 + 1;
|
|
auto i2 = (i0 + 2) % path.num_points;
|
|
auto p0 = Vector2f{path.points[2 * i0], path.points[2 * i0 + 1]};
|
|
auto p1 = Vector2f{path.points[2 * i1], path.points[2 * i1 + 1]};
|
|
auto p2 = Vector2f{path.points[2 * i2], path.points[2 * i2 + 1]};
|
|
auto eval = [&](float t) -> Vector2f {
|
|
auto tt = 1 - t;
|
|
return (tt*tt)*p0 + (2*tt*t)*p1 + (t*t)*p2;
|
|
};
|
|
data.path.base_point_id = sample_id;
|
|
data.path.point_id = point_id;
|
|
data.path.t = t;
|
|
if (t < -1e-3f || t > 1+1e-3f) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
auto tangent = 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1);
|
|
auto tan_len = length(tangent);
|
|
if (tan_len == 0) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
normal = Vector2f{-tangent.y, tangent.x} / tan_len;
|
|
// length of tangent is the Jacobian of the sampling transformation
|
|
pdf *= path_length_pmf[sample_id] / tan_len;
|
|
auto ret = eval(t);
|
|
if (stroke_perturb_direction != 0.f) {
|
|
auto r0 = stroke_radius;
|
|
auto r1 = stroke_radius;
|
|
auto r2 = stroke_radius;
|
|
if (path.thickness != nullptr) {
|
|
r0 = path.thickness[i0];
|
|
r1 = path.thickness[i1];
|
|
r2 = path.thickness[i2];
|
|
}
|
|
auto tt = 1 - t;
|
|
auto r = (tt*tt)*r0 + (2*tt*t)*r1 + (t*t)*r2;
|
|
ret += stroke_perturb_direction * r * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
} else if (path.num_control_points[sample_id] == 2) {
|
|
// Cubic Bezier curve
|
|
auto i0 = point_id;
|
|
auto i1 = point_id + 1;
|
|
auto i2 = point_id + 2;
|
|
auto i3 = (point_id + 3) % path.num_points;
|
|
assert(i0 >= 0 && i2 < path.num_points);
|
|
auto p0 = Vector2f{path.points[2 * i0], path.points[2 * i0 + 1]};
|
|
auto p1 = Vector2f{path.points[2 * i1], path.points[2 * i1 + 1]};
|
|
auto p2 = Vector2f{path.points[2 * i2], path.points[2 * i2 + 1]};
|
|
auto p3 = Vector2f{path.points[2 * i3], path.points[2 * i3 + 1]};
|
|
auto eval = [&](float t) -> Vector2f {
|
|
auto tt = 1 - t;
|
|
return (tt*tt*tt)*p0 + (3*tt*tt*t)*p1 + (3*tt*t*t)*p2 + (t*t*t)*p3;
|
|
};
|
|
data.path.base_point_id = sample_id;
|
|
data.path.point_id = point_id;
|
|
data.path.t = t;
|
|
if (t < -1e-3f || t > 1+1e-3f) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
auto tangent = 3 * square(1 - t) * (p1 - p0) + 6 * (1 - t) * t * (p2 - p1) + 3 * t * t * (p3 - p2);
|
|
auto tan_len = length(tangent);
|
|
if (tan_len == 0) {
|
|
// return invalid sample
|
|
pdf = 0;
|
|
return Vector2f{0, 0};
|
|
}
|
|
normal = Vector2f{-tangent.y, tangent.x} / tan_len;
|
|
// length of tangent is the Jacobian of the sampling transformation
|
|
pdf *= path_length_pmf[sample_id] / tan_len;
|
|
auto ret = eval(t);
|
|
if (stroke_perturb_direction != 0.f) {
|
|
auto r0 = stroke_radius;
|
|
auto r1 = stroke_radius;
|
|
auto r2 = stroke_radius;
|
|
auto r3 = stroke_radius;
|
|
if (path.thickness != nullptr) {
|
|
r0 = path.thickness[i0];
|
|
r1 = path.thickness[i1];
|
|
r2 = path.thickness[i2];
|
|
r3 = path.thickness[i3];
|
|
}
|
|
auto tt = 1 - t;
|
|
auto r = (tt*tt*tt)*r0 + (3*tt*tt*t)*r1 + (3*tt*t*t)*r2 + (t*t*t)*r3;
|
|
ret += stroke_perturb_direction * r * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
} else {
|
|
assert(false);
|
|
}
|
|
assert(false);
|
|
return Vector2f{0, 0};
|
|
}
|
|
|
|
DEVICE
|
|
Vector2f sample_boundary(const Rect &rect,
|
|
float t, Vector2f &normal,
|
|
float &pdf,
|
|
BoundaryData &,
|
|
float stroke_perturb_direction,
|
|
float stroke_radius) {
|
|
// Roll a dice to decide whether to sample width or height
|
|
auto w = rect.p_max.x - rect.p_min.x;
|
|
auto h = rect.p_max.y - rect.p_min.y;
|
|
pdf /= (2 * (w +h));
|
|
if (t <= w / (w + h)) {
|
|
// Sample width
|
|
// reuse t for the next dice
|
|
t *= (w + h) / w;
|
|
// Roll a dice to decide whether to sample upper width or lower width
|
|
if (t < 0.5f) {
|
|
// Sample upper width
|
|
normal = Vector2f{0, -1};
|
|
auto ret = rect.p_min + 2 * t * Vector2f{rect.p_max.x - rect.p_min.x, 0.f};
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
} else {
|
|
// Sample lower width
|
|
normal = Vector2f{0, 1};
|
|
auto ret = Vector2f{rect.p_min.x, rect.p_max.y} +
|
|
2 * (t - 0.5f) * Vector2f{rect.p_max.x - rect.p_min.x, 0.f};
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
} else {
|
|
// Sample height
|
|
// reuse t for the next dice
|
|
assert(h > 0);
|
|
t = (t - w / (w + h)) * (w + h) / h;
|
|
// Roll a dice to decide whether to sample left height or right height
|
|
if (t < 0.5f) {
|
|
// Sample left height
|
|
normal = Vector2f{-1, 0};
|
|
auto ret = rect.p_min + 2 * t * Vector2f{0.f, rect.p_max.y - rect.p_min.y};
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
} else {
|
|
// Sample right height
|
|
normal = Vector2f{1, 0};
|
|
auto ret = Vector2f{rect.p_max.x, rect.p_min.y} +
|
|
2 * (t - 0.5f) * Vector2f{0.f, rect.p_max.y - rect.p_min.y};
|
|
if (stroke_perturb_direction != 0.f) {
|
|
ret += stroke_perturb_direction * stroke_radius * normal;
|
|
if (stroke_perturb_direction < 0) {
|
|
// normal should point towards the perturb direction
|
|
normal = -normal;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
DEVICE
|
|
Vector2f sample_boundary(const SceneData &scene,
|
|
int shape_group_id,
|
|
int shape_id,
|
|
float t,
|
|
Vector2f &normal,
|
|
float &pdf,
|
|
BoundaryData &data) {
|
|
const ShapeGroup &shape_group = scene.shape_groups[shape_group_id];
|
|
const Shape &shape = scene.shapes[shape_id];
|
|
pdf = 1;
|
|
// Choose which one to sample: stroke discontinuities or fill discontinuities.
|
|
// TODO: we don't need to sample fill discontinuities when stroke alpha is 1 and both
|
|
// fill and stroke color exists
|
|
auto stroke_perturb = false;
|
|
if (shape_group.fill_color != nullptr && shape_group.stroke_color != nullptr) {
|
|
if (t < 0.5f) {
|
|
stroke_perturb = false;
|
|
t = 2 * t;
|
|
pdf = 0.5f;
|
|
} else {
|
|
stroke_perturb = true;
|
|
t = 2 * (t - 0.5f);
|
|
pdf = 0.5f;
|
|
}
|
|
} else if (shape_group.stroke_color != nullptr) {
|
|
stroke_perturb = true;
|
|
}
|
|
data.is_stroke = stroke_perturb;
|
|
auto stroke_perturb_direction = 0.f;
|
|
if (stroke_perturb) {
|
|
if (t < 0.5f) {
|
|
stroke_perturb_direction = -1.f;
|
|
t = 2 * t;
|
|
pdf *= 0.5f;
|
|
} else {
|
|
stroke_perturb_direction = 1.f;
|
|
t = 2 * (t - 0.5f);
|
|
pdf *= 0.5f;
|
|
}
|
|
}
|
|
switch (shape.type) {
|
|
case ShapeType::Circle:
|
|
return sample_boundary(
|
|
*(const Circle *)shape.ptr, t, normal, pdf, data, stroke_perturb_direction, shape.stroke_width);
|
|
case ShapeType::Ellipse:
|
|
return sample_boundary(
|
|
*(const Ellipse *)shape.ptr, t, normal, pdf, data, stroke_perturb_direction, shape.stroke_width);
|
|
case ShapeType::Path:
|
|
return sample_boundary(
|
|
*(const Path *)shape.ptr,
|
|
scene.path_length_cdf[shape_id],
|
|
scene.path_length_pmf[shape_id],
|
|
scene.path_point_id_map[shape_id],
|
|
scene.shapes_length[shape_id],
|
|
t,
|
|
normal,
|
|
pdf,
|
|
data,
|
|
stroke_perturb_direction,
|
|
shape.stroke_width);
|
|
case ShapeType::Rect:
|
|
return sample_boundary(
|
|
*(const Rect *)shape.ptr, t, normal, pdf, data, stroke_perturb_direction, shape.stroke_width);
|
|
}
|
|
assert(false);
|
|
return Vector2f{};
|
|
}
|
|
|