self.cant_start, self.cant_end, self.skew_start,
self.skew_end, self.mirror)
def _compute_spline_gradient_coeffs(self):
"""
Computes the hermite spline coefficients for this spline from the gradient and delta height properties.
:return: the coefficients c, a, and b representing the z^3, z^2, and z terms respectively
"""
b = self.start_grad/100
if self.use_delta_height:
c = (self.end_grad-self.start_grad)/100 * self.length - 2 * (self.delta_height - self.start_grad/100 * self.length)
c /= self.length * self.length * self.length
a = -(self.end_grad-self.start_grad)/100 * self.length + 3 * (self.delta_height - self.start_grad/100 * self.length)
a /= self.length * self.length
else:
c = 0
a = (self.end_grad-self.start_grad)/100/(2*self.length)
return c, a, b
def generate_tesselation_points(self, spline_tess_dist, spline_curve_sag):
"""
Generates a set of z coordinates along the spline which represents an "ideal" sampling of the spline.
:param spline_tess_dist:
:param spline_curve_sag:
:return:
"""
"""
This is a non-trivial problem to solve, and hence the answer is somewhat approximate here. Our spline is
essentially a hermite curve along the y axis and an arc along the xz plane. For ease of implementation we
consider the sampling of these separately and then merge and weld the samplings together.
The Y axis can be described by the following function:
(see https://www.desmos.com/calculator/7xfjwmqtpz for my calculator)
f_y(z, x) = z^3*c + z^2*a + z*b + x*(z*cant_delta + cant_0) {0 <= z <= length}
To approximate the curvature of the spline we take the arctangent of the first derivative (essentially
converting the gradient of the function into an angle):
f_dy(z, x) = 3z^2*c + 2z*a + b + x*cant_delta {0 <= z <= length}
f_curvature(z, x) = abs(atan(f_dy(z, x))) * (1 + 1/r)
We want to sample this at regular horizontal slices of the curvature equation (ie the higher the gradient of
the curvature equation the more samples to generate), we do this by taking the inverse of the curvature equation
and sampling it at regular intervals along the x axis. This is represented by the following expression:
f_icurvature(z, x) = (-a +- sqrt(a^2 - 3c(b + x*cant_delta) +- 3c*tan(z)) / 3c {0 <= z <= pi/2}
Because of the two +- operations in the above equation, we can actually get up to four points per iteration.
"""
samples = []
n_grad_samples = 0
if self.start_grad != self.end_grad or self.use_delta_height:
c, a, b = self._compute_spline_gradient_coeffs()
if a != 0 or c != 0:
cant_delta = (self.cant_end - self.cant_start) / self.length / 100
# Maybe we should consider the value of x?
x = 0
_3c = c*3
if self.use_delta_height:
a23cbxc = a*a - _3c*(b + x * cant_delta)
else:
bxc2a = (-b - x * cant_delta)/(2*a)
z = 0
if (math.pi/2) / spline_curve_sag > 10000:
log("[ERROR] Spline tesselation would take too long, please increase the spline curve sag distance!")
# Return a basic tesselation...
n_segments = min(max(math.ceil(self.length / spline_tess_dist), 1), 100)
dx = self.length / n_segments
return [i * dx for i in range(n_segments + 1)]
while z <= math.pi/2:
if self.use_delta_height:
_3ctanz = _3c * math.tan(z)
ic00, ic01, ic10, ic11 = -1, -1, -1, -1
if a23cbxc + _3ctanz >= 0:
ic00 = (-a + math.sqrt(a23cbxc + _3ctanz))/_3c
ic01 = (-a - math.sqrt(a23cbxc + _3ctanz))/_3c
if a23cbxc - _3ctanz >= 0:
ic10 = (-a + math.sqrt(a23cbxc - _3ctanz))/_3c
ic11 = (-a - math.sqrt(a23cbxc - _3ctanz))/_3c
if 0 <= ic00 <= self.length:
samples.append(ic00)
if 0 <= ic01 <= self.length:
samples.append(ic01)
if 0 <= ic10 <= self.length:
samples.append(ic10)
if 0 <= ic11 <= self.length:
samples.append(ic11)
else:
# f_icurvature2(z, x) = (-b -x*c_d)/(2a) +- (tan z)/(2a)
t2a = math.tan(z)/(2*a)
ic20 = bxc2a + t2a
ic21 = bxc2a - t2a
if 0 <= ic20 <= self.length:
samples.append(ic20)
if 0 <= ic21 <= self.length:
samples.append(ic21)
z += spline_curve_sag * 10
n_grad_samples = len(samples)
# Now append the samples from the arc on the xz plane (spline radius)
radius = abs(self.radius)
dr = float("inf")
if radius > 0:
revs = min(self.length / radius, math.pi * 2)
# Arc distance based angle increment
dr_a = spline_tess_dist / radius
# Sag distance based angle increment, clamped to 0.06deg < x < 90deg
dr_b = 2 * math.acos(1 - clamp(spline_curve_sag / radius, 0.001, 0.294))
dr = min(dr_a, dr_b)
n_segments = max(math.ceil(revs / dr), 1)
dr = revs / n_segments
samples.extend([i*dr*radius for i in range(n_segments+1)])
else:
# Now append the samples from the constant tesselation factor
# Note that the arc segment already include the constant tesselation factor
n_segments = max(math.ceil(self.length / spline_tess_dist), 1)
dx = self.length / n_segments
samples.extend([i*dx for i in range(n_segments+1)])
# Now weld the samples together based on a heuristic
if len(samples) > 1:
samples.sort()
# This tuple stores the sample position, and it's weight (used for averaging); when a sample is consumed its
# weight is set to 0 and the sample it's merged with has its weight incremented
samples_weighted = [(s, 1) for s in samples]
last_s = 0
# log(f"spline-{self.id}\tmerge_dist: grad={(self.length/max(float(n_grad_samples), 0.1)/3):3f} "
# f"const={spline_tess_dist:3f} arc={dr*max(radius, 0.01)/2:3f} length={self.length*0.9:3f}")
merge_dist = min((self.length/max(float(n_grad_samples), 0.1)/3),
spline_tess_dist,
dr*max(radius, 0.01)/2,
self.length*0.9)
for i in range(1, len(samples_weighted)-1):
d = samples_weighted[i][0] - samples_weighted[last_s][0]
if d < merge_dist:
sl = samples_weighted[last_s]
samples_weighted[last_s] = ((sl[0]*sl[1]+samples_weighted[i][0]) / (sl[1]+1), sl[1]+1)
samples_weighted[i] = (0, 0)
else:
last_s = i
samples = [s[0] for s in samples_weighted if s[1] > 0]
# Make sure the start and end points are fixed
samples[0] = 0
samples[-1] = self.length
return samples
def evaluate_spline(self, pos_offset, apply_rot=False, world_space=False):
"""
Computes a position along a spline given offset coordinates.
:param world_space: whether the position should be relative to the tile or relative to the origin of the
spline
:param apply_rot: whether the spline segment's rotation should also be applied to the spline
:param pos_offset: position offset along the spline; y is forward along the spline, x is across the
width of the spline
:return: (the computed position, the computed rotation)
"""
# Split the split evaluation into separate gradient and radius steps
# Gradient
# Evaluate: f_z(y, x) = y^3*c + y^2*a + y*b + x*(y*cant_delta + cant_0) {0 <= y <= length}
# The rotation is given by:
# f_rx(y, x) = atan(3y^2*c + 2y*a + b + x*cant_delta) {0 <= y <= length}
ox, oy, oz = pos_offset
cant_delta = (self.cant_end-self.cant_start)/100/self.length
c, a, b = self._compute_spline_gradient_coeffs()
pz = oy*oy*oy*c + oy*oy*a + oy*b
rx = math.atan(3*oy*oy*c + 2*oy*a + b + ox*(oy*cant_delta + self.cant_start/100))
pz += oz
# Cant
ry = math.atan(oy*cant_delta + self.cant_start/100)
# TODO: The x coordinate should be clamped to the width of the spline
pz += -ox*(oy*cant_delta + self.cant_start/100)
# Radius
if abs(self.radius) > 0:
rz = oy/self.radius
k = ox - self.radius
px = k*math.cos(rz) + self.radius
py = -k*math.sin(rz)
rz = -rz
else:
rz = 0
px = ox
py = oy
# World space
pos = Vector((px, py, pz))
rot = Vector((rx, ry, rz))
if apply_rot or world_space:
if bpy.app.version < (2, 80):
pos = pos * mathutils.Matrix.Rotation(math.radians(self.rot), 4, "Z")
else:
pos = pos @ mathutils.Matrix.Rotation(math.radians(self.rot), 4, "Z")
rot.z += math.radians(-self.rot)
if world_space:
pos += Vector(self.pos).xzy
return pos, rot
def generate_mesh(self, sli_cache, spline_tess_dist, spline_curve_sag, apply_xform = False):
"""
Generates a Blender mesh for this spline.
:param sli_cache: the dictionary of spline profile definitions
:param spline_tess_dist: the distance between tesselation segments
:param spline_curve_sag: the maximum amount of curve sag for the tessellated segments
:param apply_xform:
:return: a reference to the Blender mesh for this spline
"""
sli = sli_cache[self.sli_path]
verts = []
uvs = []
https://api.github.com/space928/Blender-O3D-IO-Public/blob/734b4df774329c68085f978beb7d29e123b98b88/o3d_io/io_omsi_spline.py#L238