Refactor Curv3D and PathFollow3D
This commit makes the following major changes 1. Add "sample_baked_with_rotation()" to Curve3D, making it usable independently. A similar change was made to Curve2D previously. 2. Refactor the _bake() method on Curve3D, using Parallel Transport Frame instead of Frenet Frame. 3. Refactor the sample_* methods, including: i. Factor out common binary search code, following the DRY principe ii. sample_up_vector() interpolated up vector as part of rotation frame(posture) for consistancy and accuracy.
This commit is contained in:
@ -31,6 +31,7 @@
|
||||
#include "curve.h"
|
||||
|
||||
#include "core/core_string_names.h"
|
||||
#include "core/math/math_funcs.h"
|
||||
|
||||
const char *Curve::SIGNAL_RANGE_CHANGED = "range_changed";
|
||||
|
||||
@ -1413,8 +1414,9 @@ void Curve3D::_bake() const {
|
||||
if (points.size() == 0) {
|
||||
baked_point_cache.clear();
|
||||
baked_tilt_cache.clear();
|
||||
baked_up_vector_cache.clear();
|
||||
baked_dist_cache.clear();
|
||||
|
||||
baked_up_vector_cache.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1438,15 +1440,16 @@ void Curve3D::_bake() const {
|
||||
|
||||
Vector3 position = points[0].position;
|
||||
real_t dist = 0.0;
|
||||
List<Plane> pointlist;
|
||||
List<Plane> pointlist; // Abuse Plane for (position, dist)
|
||||
List<real_t> distlist;
|
||||
|
||||
// Start always from origin.
|
||||
pointlist.push_back(Plane(position, points[0].tilt));
|
||||
distlist.push_back(0.0);
|
||||
|
||||
// Step 1: Sample points
|
||||
const real_t step = 0.1; // At least 10 substeps ought to be enough?
|
||||
for (int i = 0; i < points.size() - 1; i++) {
|
||||
real_t step = 0.1; // at least 10 substeps ought to be enough?
|
||||
real_t p = 0.0;
|
||||
|
||||
while (p < 1.0) {
|
||||
@ -1461,7 +1464,7 @@ void Curve3D::_bake() const {
|
||||
if (d > bake_interval) {
|
||||
// OK! between P and NP there _has_ to be Something, let's go searching!
|
||||
|
||||
int iterations = 10; //lots of detail!
|
||||
const int iterations = 10; // Lots of detail!
|
||||
|
||||
real_t low = p;
|
||||
real_t hi = np;
|
||||
@ -1496,76 +1499,135 @@ void Curve3D::_bake() const {
|
||||
Vector3 npp = points[i + 1].position;
|
||||
real_t d = position.distance_to(npp);
|
||||
|
||||
position = npp;
|
||||
Plane post;
|
||||
post.normal = position;
|
||||
post.d = points[i + 1].tilt;
|
||||
if (d > CMP_EPSILON) { // Avoid the degenerate case of two very close points.
|
||||
position = npp;
|
||||
Plane post;
|
||||
post.normal = position;
|
||||
post.d = points[i + 1].tilt;
|
||||
|
||||
dist += d;
|
||||
dist += d;
|
||||
|
||||
pointlist.push_back(post);
|
||||
distlist.push_back(dist);
|
||||
pointlist.push_back(post);
|
||||
distlist.push_back(dist);
|
||||
}
|
||||
}
|
||||
|
||||
baked_max_ofs = dist;
|
||||
|
||||
baked_point_cache.resize(pointlist.size());
|
||||
Vector3 *w = baked_point_cache.ptrw();
|
||||
int idx = 0;
|
||||
const int point_count = pointlist.size();
|
||||
{
|
||||
baked_point_cache.resize(point_count);
|
||||
Vector3 *w = baked_point_cache.ptrw();
|
||||
|
||||
baked_tilt_cache.resize(pointlist.size());
|
||||
real_t *wt = baked_tilt_cache.ptrw();
|
||||
baked_tilt_cache.resize(point_count);
|
||||
real_t *wt = baked_tilt_cache.ptrw();
|
||||
|
||||
baked_up_vector_cache.resize(up_vector_enabled ? pointlist.size() : 0);
|
||||
Vector3 *up_write = baked_up_vector_cache.ptrw();
|
||||
baked_dist_cache.resize(point_count);
|
||||
real_t *wd = baked_dist_cache.ptrw();
|
||||
|
||||
baked_dist_cache.resize(pointlist.size());
|
||||
real_t *wd = baked_dist_cache.ptrw();
|
||||
int idx = 0;
|
||||
for (const Plane &E : pointlist) {
|
||||
w[idx] = E.normal;
|
||||
wt[idx] = E.d;
|
||||
wd[idx] = distlist[idx];
|
||||
|
||||
Vector3 sideways;
|
||||
Vector3 up;
|
||||
Vector3 forward;
|
||||
|
||||
Vector3 prev_sideways = Vector3(1, 0, 0);
|
||||
Vector3 prev_up = Vector3(0, 1, 0);
|
||||
Vector3 prev_forward = Vector3(0, 0, 1);
|
||||
|
||||
for (const Plane &E : pointlist) {
|
||||
w[idx] = E.normal;
|
||||
wt[idx] = E.d;
|
||||
wd[idx] = distlist[idx];
|
||||
|
||||
if (!up_vector_enabled) {
|
||||
idx++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!up_vector_enabled) {
|
||||
baked_up_vector_cache.resize(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Calculate the up vectors and the whole local reference frame
|
||||
//
|
||||
// See Dougan, Carl. "The parallel transport frame." Game Programming Gems 2 (2001): 215-219.
|
||||
// for an example discussing about why not the Frenet frame.
|
||||
{
|
||||
PackedVector3Array forward_vectors;
|
||||
|
||||
baked_up_vector_cache.resize(point_count);
|
||||
forward_vectors.resize(point_count);
|
||||
|
||||
Vector3 *up_write = baked_up_vector_cache.ptrw();
|
||||
Vector3 *forward_write = forward_vectors.ptrw();
|
||||
|
||||
const Vector3 *points_ptr = baked_point_cache.ptr();
|
||||
|
||||
Basis frame; // X-right, Y-up, Z-forward.
|
||||
Basis frame_prev;
|
||||
|
||||
// Set the initial frame based on Y-up rule.
|
||||
{
|
||||
Vector3 up(0, 1, 0);
|
||||
Vector3 forward = (points_ptr[1] - points_ptr[0]).normalized();
|
||||
if (forward.is_equal_approx(Vector3())) {
|
||||
forward = Vector3(1, 0, 0);
|
||||
}
|
||||
|
||||
if (abs(forward.dot(up)) > 1.0 - UNIT_EPSILON) {
|
||||
frame_prev = Basis::looking_at(-forward, up);
|
||||
} else {
|
||||
frame_prev = Basis::looking_at(-forward, Vector3(1, 0, 0));
|
||||
}
|
||||
|
||||
up_write[0] = frame_prev.get_column(1);
|
||||
forward_write[0] = frame_prev.get_column(2);
|
||||
}
|
||||
|
||||
forward = idx > 0 ? (w[idx] - w[idx - 1]).normalized() : prev_forward;
|
||||
// Calculate the Parallel Transport Frame.
|
||||
for (int idx = 1; idx < point_count; idx++) {
|
||||
Vector3 forward = (points_ptr[idx] - points_ptr[idx - 1]).normalized();
|
||||
if (forward.is_equal_approx(Vector3())) {
|
||||
forward = frame_prev.get_column(2);
|
||||
}
|
||||
|
||||
real_t y_dot = prev_up.dot(forward);
|
||||
Basis rotate;
|
||||
rotate.rotate_to_align(frame_prev.get_column(2), forward);
|
||||
frame = rotate * frame_prev;
|
||||
frame.orthonormalize(); // guard against float error accumulation
|
||||
|
||||
if (y_dot > (1.0f - CMP_EPSILON)) {
|
||||
sideways = prev_sideways;
|
||||
up = -prev_forward;
|
||||
} else if (y_dot < -(1.0f - CMP_EPSILON)) {
|
||||
sideways = prev_sideways;
|
||||
up = prev_forward;
|
||||
} else {
|
||||
sideways = prev_up.cross(forward).normalized();
|
||||
up = forward.cross(sideways).normalized();
|
||||
up_write[idx] = frame.get_column(1);
|
||||
forward_write[idx] = frame.get_column(2);
|
||||
|
||||
frame_prev = frame;
|
||||
}
|
||||
|
||||
if (idx == 1) {
|
||||
up_write[0] = up;
|
||||
bool is_loop = true;
|
||||
// Loop smoothing only applies when the curve is a loop, which means two ends meet, and share forward directions.
|
||||
{
|
||||
if (!points_ptr[0].is_equal_approx(points_ptr[point_count - 1])) {
|
||||
is_loop = false;
|
||||
}
|
||||
|
||||
real_t dot = forward_write[0].dot(forward_write[point_count - 1]);
|
||||
if (dot < 1.0 - 0.01) { // Alignment should not be too tight, or it dosen't work for coarse bake interval
|
||||
is_loop = false;
|
||||
}
|
||||
}
|
||||
|
||||
up_write[idx] = up;
|
||||
// Twist up vectors, so that they align at two ends of the curve.
|
||||
if (is_loop) {
|
||||
const Vector3 up_start = up_write[0];
|
||||
const Vector3 up_end = up_write[point_count - 1];
|
||||
|
||||
prev_sideways = sideways;
|
||||
prev_up = up;
|
||||
prev_forward = forward;
|
||||
real_t sign = SIGN(up_end.cross(up_start).dot(forward_write[0]));
|
||||
real_t full_angle = Quaternion(up_end, up_start).get_angle();
|
||||
|
||||
idx++;
|
||||
if (abs(full_angle) < UNIT_EPSILON) {
|
||||
return;
|
||||
} else {
|
||||
const real_t *dists = baked_dist_cache.ptr();
|
||||
for (int idx = 1; idx < point_count; idx++) {
|
||||
const real_t frac = dists[idx] / baked_max_ofs;
|
||||
const real_t angle = Math::lerp((real_t)0.0, full_angle, frac);
|
||||
Basis twist(forward_write[idx] * sign, angle);
|
||||
|
||||
up_write[idx] = twist.xform(up_write[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1577,27 +1639,15 @@ real_t Curve3D::get_baked_length() const {
|
||||
return baked_max_ofs;
|
||||
}
|
||||
|
||||
Vector3 Curve3D::sample_baked(real_t p_offset, bool p_cubic) const {
|
||||
if (baked_cache_dirty) {
|
||||
_bake();
|
||||
}
|
||||
Curve3D::Interval Curve3D::_find_interval(real_t p_offset) const {
|
||||
Interval interval = {
|
||||
-1,
|
||||
0.0
|
||||
};
|
||||
ERR_FAIL_COND_V_MSG(baked_cache_dirty, interval, "Backed cache is dirty");
|
||||
|
||||
// Validate: Curve may not have baked points.
|
||||
int pc = baked_point_cache.size();
|
||||
ERR_FAIL_COND_V_MSG(pc == 0, Vector3(), "No points in Curve3D.");
|
||||
|
||||
if (pc == 1) {
|
||||
return baked_point_cache.get(0);
|
||||
}
|
||||
|
||||
const Vector3 *r = baked_point_cache.ptr();
|
||||
|
||||
if (p_offset < 0) {
|
||||
return r[0];
|
||||
}
|
||||
if (p_offset >= baked_max_ofs) {
|
||||
return r[pc - 1];
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(pc < 2, interval, "Less than two points in cache");
|
||||
|
||||
int start = 0;
|
||||
int end = pc;
|
||||
@ -1617,9 +1667,27 @@ Vector3 Curve3D::sample_baked(real_t p_offset, bool p_cubic) const {
|
||||
real_t offset_end = baked_dist_cache[idx + 1];
|
||||
|
||||
real_t idx_interval = offset_end - offset_begin;
|
||||
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, Vector3(), "Couldn't find baked segment.");
|
||||
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, interval, "Offset out of range.");
|
||||
|
||||
real_t frac = (p_offset - offset_begin) / idx_interval;
|
||||
interval.idx = idx;
|
||||
if (idx_interval < FLT_EPSILON) {
|
||||
interval.frac = 0.5; // For a very short interval, 0.5 is a reasonable choice.
|
||||
ERR_FAIL_V_MSG(interval, "Zero length interval.");
|
||||
}
|
||||
|
||||
interval.frac = (p_offset - offset_begin) / idx_interval;
|
||||
return interval;
|
||||
}
|
||||
|
||||
Vector3 Curve3D::_sample_baked(Interval p_interval, bool p_cubic) const {
|
||||
// Assuming p_interval is valid.
|
||||
ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_point_cache.size(), Vector3(), "Invalid interval");
|
||||
|
||||
int idx = p_interval.idx;
|
||||
real_t frac = p_interval.frac;
|
||||
|
||||
const Vector3 *r = baked_point_cache.ptr();
|
||||
int pc = baked_point_cache.size();
|
||||
|
||||
if (p_cubic) {
|
||||
Vector3 pre = idx > 0 ? r[idx - 1] : r[idx];
|
||||
@ -1630,6 +1698,114 @@ Vector3 Curve3D::sample_baked(real_t p_offset, bool p_cubic) const {
|
||||
}
|
||||
}
|
||||
|
||||
real_t Curve3D::_sample_baked_tilt(Interval p_interval) const {
|
||||
// Assuming that p_interval is valid.
|
||||
ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_tilt_cache.size(), 0.0, "Invalid interval");
|
||||
|
||||
int idx = p_interval.idx;
|
||||
real_t frac = p_interval.frac;
|
||||
|
||||
const real_t *r = baked_tilt_cache.ptr();
|
||||
|
||||
return Math::lerp(r[idx], r[idx + 1], frac);
|
||||
}
|
||||
|
||||
Basis Curve3D::_sample_posture(Interval p_interval, bool p_apply_tilt) const {
|
||||
// Assuming that p_interval is valid.
|
||||
ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_point_cache.size(), Basis(), "Invalid interval");
|
||||
if (up_vector_enabled) {
|
||||
ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_up_vector_cache.size(), Basis(), "Invalid interval");
|
||||
}
|
||||
|
||||
int idx = p_interval.idx;
|
||||
real_t frac = p_interval.frac;
|
||||
|
||||
Vector3 forward_begin;
|
||||
Vector3 forward_end;
|
||||
if (idx == 0) {
|
||||
forward_begin = (baked_point_cache[1] - baked_point_cache[0]).normalized();
|
||||
forward_end = (baked_point_cache[1] - baked_point_cache[0]).normalized();
|
||||
} else {
|
||||
forward_begin = (baked_point_cache[idx] - baked_point_cache[idx - 1]).normalized();
|
||||
forward_end = (baked_point_cache[idx + 1] - baked_point_cache[idx]).normalized();
|
||||
}
|
||||
|
||||
Vector3 up_begin;
|
||||
Vector3 up_end;
|
||||
if (up_vector_enabled) {
|
||||
const Vector3 *up_ptr = baked_up_vector_cache.ptr();
|
||||
up_begin = up_ptr[idx];
|
||||
up_end = up_ptr[idx + 1];
|
||||
} else {
|
||||
up_begin = Vector3(0.0, 1.0, 0.0);
|
||||
up_end = Vector3(0.0, 1.0, 0.0);
|
||||
}
|
||||
|
||||
// Build frames at both ends of the interval, then interpolate.
|
||||
const Basis frame_begin = Basis::looking_at(-forward_begin, up_begin);
|
||||
const Basis frame_end = Basis::looking_at(-forward_end, up_end);
|
||||
const Basis frame = frame_begin.slerp(frame_end, frac).orthonormalized();
|
||||
|
||||
if (!p_apply_tilt) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Applying tilt.
|
||||
const real_t tilt = _sample_baked_tilt(p_interval);
|
||||
Vector3 forward = frame.get_column(2);
|
||||
|
||||
const Basis twist(forward, tilt);
|
||||
return twist * frame;
|
||||
}
|
||||
|
||||
Vector3 Curve3D::sample_baked(real_t p_offset, bool p_cubic) const {
|
||||
if (baked_cache_dirty) {
|
||||
_bake();
|
||||
}
|
||||
|
||||
// Validate: Curve may not have baked points.
|
||||
int pc = baked_point_cache.size();
|
||||
ERR_FAIL_COND_V_MSG(pc == 0, Vector3(), "No points in Curve3D.");
|
||||
|
||||
if (pc == 1) {
|
||||
return baked_point_cache[0];
|
||||
}
|
||||
|
||||
p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
|
||||
|
||||
Curve3D::Interval interval = _find_interval(p_offset);
|
||||
return _sample_baked(interval, p_cubic);
|
||||
}
|
||||
|
||||
Transform3D Curve3D::sample_baked_with_rotation(real_t p_offset, bool p_cubic, bool p_apply_tilt) const {
|
||||
if (baked_cache_dirty) {
|
||||
_bake();
|
||||
}
|
||||
|
||||
// Validate: Curve may not have baked points.
|
||||
const int point_count = baked_point_cache.size();
|
||||
ERR_FAIL_COND_V_MSG(point_count == 0, Transform3D(), "No points in Curve3D.");
|
||||
|
||||
if (point_count == 1) {
|
||||
Transform3D t;
|
||||
t.origin = baked_point_cache.get(0);
|
||||
ERR_FAIL_V_MSG(t, "Only 1 point in Curve3D.");
|
||||
}
|
||||
|
||||
p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
|
||||
|
||||
// 0. Find interval for all sampling steps.
|
||||
Curve3D::Interval interval = _find_interval(p_offset);
|
||||
|
||||
// 1. Sample position.
|
||||
Vector3 pos = _sample_baked(interval, p_cubic);
|
||||
|
||||
// 2. Sample rotation frame.
|
||||
Basis frame = _sample_posture(interval, p_apply_tilt);
|
||||
|
||||
return Transform3D(frame, pos);
|
||||
}
|
||||
|
||||
real_t Curve3D::sample_baked_tilt(real_t p_offset) const {
|
||||
if (baked_cache_dirty) {
|
||||
_bake();
|
||||
@ -1643,38 +1819,10 @@ real_t Curve3D::sample_baked_tilt(real_t p_offset) const {
|
||||
return baked_tilt_cache.get(0);
|
||||
}
|
||||
|
||||
const real_t *r = baked_tilt_cache.ptr();
|
||||
p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic
|
||||
|
||||
if (p_offset < 0) {
|
||||
return r[0];
|
||||
}
|
||||
if (p_offset >= baked_max_ofs) {
|
||||
return r[pc - 1];
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
int end = pc;
|
||||
int idx = (end + start) / 2;
|
||||
// Binary search to find baked points.
|
||||
while (start < idx) {
|
||||
real_t offset = baked_dist_cache[idx];
|
||||
if (p_offset <= offset) {
|
||||
end = idx;
|
||||
} else {
|
||||
start = idx;
|
||||
}
|
||||
idx = (end + start) / 2;
|
||||
}
|
||||
|
||||
real_t offset_begin = baked_dist_cache[idx];
|
||||
real_t offset_end = baked_dist_cache[idx + 1];
|
||||
|
||||
real_t idx_interval = offset_end - offset_begin;
|
||||
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, 0, "Couldn't find baked segment.");
|
||||
|
||||
real_t frac = (p_offset - offset_begin) / idx_interval;
|
||||
|
||||
return Math::lerp(r[idx], r[idx + 1], (real_t)frac);
|
||||
Curve3D::Interval interval = _find_interval(p_offset);
|
||||
return _sample_baked_tilt(interval);
|
||||
}
|
||||
|
||||
Vector3 Curve3D::sample_baked_up_vector(real_t p_offset, bool p_apply_tilt) const {
|
||||
@ -1683,61 +1831,17 @@ Vector3 Curve3D::sample_baked_up_vector(real_t p_offset, bool p_apply_tilt) cons
|
||||
}
|
||||
|
||||
// Validate: Curve may not have baked up vectors.
|
||||
int count = baked_up_vector_cache.size();
|
||||
ERR_FAIL_COND_V_MSG(count == 0, Vector3(0, 1, 0), "No up vectors in Curve3D.");
|
||||
ERR_FAIL_COND_V_MSG(!up_vector_enabled, Vector3(0, 1, 0), "No up vectors in Curve3D.");
|
||||
|
||||
int count = baked_up_vector_cache.size();
|
||||
if (count == 1) {
|
||||
return baked_up_vector_cache.get(0);
|
||||
}
|
||||
|
||||
const Vector3 *r = baked_up_vector_cache.ptr();
|
||||
const Vector3 *rp = baked_point_cache.ptr();
|
||||
const real_t *rt = baked_tilt_cache.ptr();
|
||||
p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
|
||||
|
||||
int start = 0;
|
||||
int end = count;
|
||||
int idx = (end + start) / 2;
|
||||
// Binary search to find baked points.
|
||||
while (start < idx) {
|
||||
real_t offset = baked_dist_cache[idx];
|
||||
if (p_offset <= offset) {
|
||||
end = idx;
|
||||
} else {
|
||||
start = idx;
|
||||
}
|
||||
idx = (end + start) / 2;
|
||||
}
|
||||
|
||||
if (idx == count - 1) {
|
||||
return p_apply_tilt ? r[idx].rotated((rp[idx] - rp[idx - 1]).normalized(), rt[idx]) : r[idx];
|
||||
}
|
||||
|
||||
real_t offset_begin = baked_dist_cache[idx];
|
||||
real_t offset_end = baked_dist_cache[idx + 1];
|
||||
|
||||
real_t idx_interval = offset_end - offset_begin;
|
||||
ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, Vector3(0, 1, 0), "Couldn't find baked segment.");
|
||||
|
||||
real_t frac = (p_offset - offset_begin) / idx_interval;
|
||||
|
||||
Vector3 forward = (rp[idx + 1] - rp[idx]).normalized();
|
||||
Vector3 up = r[idx];
|
||||
Vector3 up1 = r[idx + 1];
|
||||
|
||||
if (p_apply_tilt) {
|
||||
up.rotate(forward, rt[idx]);
|
||||
up1.rotate(idx + 2 >= count ? forward : (rp[idx + 2] - rp[idx + 1]).normalized(), rt[idx + 1]);
|
||||
}
|
||||
|
||||
Vector3 axis = up.cross(up1);
|
||||
|
||||
if (axis.length_squared() < CMP_EPSILON2) {
|
||||
axis = forward;
|
||||
} else {
|
||||
axis.normalize();
|
||||
}
|
||||
|
||||
return up.rotated(axis, up.angle_to(up1) * frac);
|
||||
Curve3D::Interval interval = _find_interval(p_offset);
|
||||
return _sample_posture(interval, p_apply_tilt).get_column(1);
|
||||
}
|
||||
|
||||
PackedVector3Array Curve3D::get_baked_points() const {
|
||||
@ -2034,6 +2138,7 @@ void Curve3D::_bind_methods() {
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_baked_length"), &Curve3D::get_baked_length);
|
||||
ClassDB::bind_method(D_METHOD("sample_baked", "offset", "cubic"), &Curve3D::sample_baked, DEFVAL(false));
|
||||
ClassDB::bind_method(D_METHOD("sample_baked_with_rotation", "offset", "cubic", "apply_tilt"), &Curve3D::sample_baked_with_rotation, DEFVAL(false), DEFVAL(false));
|
||||
ClassDB::bind_method(D_METHOD("sample_baked_up_vector", "offset", "apply_tilt"), &Curve3D::sample_baked_up_vector, DEFVAL(false));
|
||||
ClassDB::bind_method(D_METHOD("get_baked_points"), &Curve3D::get_baked_points);
|
||||
ClassDB::bind_method(D_METHOD("get_baked_tilts"), &Curve3D::get_baked_tilts);
|
||||
|
||||
Reference in New Issue
Block a user