space928 / Blender-O3D-IO-Public

A plugin supporting blender 2.79.x-3.x.x for importing and exporting OMSI .sco, .cfg, and .o3d files
GNU General Public License v3.0
36 stars 5 forks source link

The x coordinate should be clamped to the width of the spline #75

Open github-actions[bot] opened 10 months ago

github-actions[bot] commented 10 months ago

https://api.github.com/space928/Blender-O3D-IO-Public/blob/734b4df774329c68085f978beb7d29e123b98b88/o3d_io/io_omsi_spline.py#L238


                                                         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 = []