NURBS

With the models geometry loaded, faces added, normalized, UVs mapped, and seamless textures applied (rotated, repeated, and offset) – we can move onto something different. There are few main things left to figure out.

  • NURBS – Non-Uniform Rational B-Splines Modeling
  • Editing individual vectors in the 3D space
  • Export:
    • TGA – Sculpted Prim Image (JPG, PNG, or TGA)
    • OBJ – Wavefront Object
    • DAE – COLLADA
    • glTF – Graphics Library Transmission Format (JSON)
    • glB – Graphics Library Transmission Format (Binary)
    • STL – stereolithography (3D Printing)
  • Import:
    • See Export

The beauty of NURBS is that it can create nicely curved geometry based off of a control net of vectors (which is what I have been able to read) and assemble as if it were a model itself. The problem with NURBS is that its difficult to create hard edges and corners without introducing additional vertices. It may not be a problem in most applications, but when you are trying to limit your models to 1,000 vectors, it becomes a problem. This is why my perfect cube looked a little rounded on the edges when uploading to Second Life.

A softened cube due to NURBS

I’m interested in the concept of NURBS since it can work well with organic objects, and be utilized for its curves. I have a hope that my lighting issues with normalized faces at the models seams may be resolved. However, I also have my doubts as the issue doesn’t happen with any other vertical path of vertices on the model.

So here is the big question – does Three.js support NURBS out of the box? Yes! They have a demonstration of geometry NURBS and there is another example of an FBX loader with NURBS as well.

How Do I Work This?

I found how to import the library for NURBSSurface. Looking through their examples, I also found ParametricGeometry. The documentation sounds like what I need as it curves faces.

Looking through the example code, I saw that they were building a two dimensional array for a control net. Each value contained an array of four Vector4’s.

const controlPoints = [
  [
    a, b, c, d
  ],
  [
    e, f, g, h
  ],
  [
    i, j, k, l
  ]
];
console.log('g = ', controlPoints[1][2]);
// Output: g = Vector4(0, 0, 0, 1);

I’m unfamiliar with Vector4, but that fourth parameter seamed to always be 1 (edit: two of them were 5). Good to know. Afterwards they had a few variables named degrees and some arrays of “knots” that seemed to be a list of indexed of degree values, which was then passed to construct the NURBS surface. This may be additional terminology with NURBS that I need to pickup. One thing of note was that the knots array had different lengths of 6 and 8. This gives me the impression that the number of knots does not need to match the number of vertices or the length of vertices in any of the control vectors in the control mesh. I’m wondering how they are related to the control points in terms of placement.

const degree1 = 2;
const degree2 = 3;
const knots1 = [ 0, 0, 0, 1, 1, 1 ];
const knots2 = [ 0, 0, 0, 0, 1, 1, 1, 1 ];
const nurbsSurface = new NURBSSurface(
  degree1, degree2,
  knots1, knots2,
  nsControlPoints
);

Currently, my model is in 1 degree, as there are no curved splines with the model. The goal is to use the standard 3 degrees everywhere. My array of knots would be full of indexes to a degree representing 3. Time to import the libraries and see if something works.

import { NURBSSurface } from
  'three/examples/jsm/curves/NURBSSurface.js';
import { ParametricGeometry } from
  'three/addons/geometries/ParametricGeometry.js';

My first attempt didn’t go so well. The typescript only had two parameters for knots and degrees. I just based the knot count on the number of vertices I had, all set to zero (for the first degree), and set both the first and second degrees to 3. As far as the vertices, I saw that It would accept vector 2, 3, and 4 – and simply passed the controlVertices themselves.

const degree1 = 3;
const degree2 = 3;
const knots1 = controlVertices.map(() => 0);
const knots2 = controlVertices.map(() => 0);
const nurbsSurface = new NURBSSurface(
  degree1, degree2,
  knots1, knots2,
  controlVertices,
);
const slices = horizontalSegments + 1;
const stacks = verticalSegments + 1;
const geometry = new ParametricGeometry(nurbsSurface.getPoint, slices, stacks);

It complained about reading x when creating the surface. My one dimensional array wasn’t working. I tried passing controlVertices within an array, and then within another array so that was a three dimensional array. I got a few errors. I needed to grasp the concept of how surface maps are constructed, and translate the vertices into it. I suspect that I needed to break it down into quads in the way that I constructed the faces of the geometry.

I took the same approach as building my faces and used the sphericalIndex to map each x/y coordinate to a vertex in 3D space relative to the center of the object. I created a separate array of points for each horizontal segment.

const degreeU = horizontalSegments;
const degreeV = verticalSegments;
let nsControlPoints = [];
for(let v = 0; v < degreeV + 1; v++) {
  const row = [];
  for(let u = 0; u < degreeU + 1; u++) {
    const index = sphericalIndex(u, v, degreeU, degreeV);
    const { x, y, z } = controlVertices[index];
    row.push(new THREE.Vector4(x, y, z, 1));
  }
  nsControlPoints.push(row);
}
const knotsU = new Array(degreeU).fill(0);
const knotsV = new Array(degreeV).fill(0);
const nurbsSurface = new NURBSSurface(
  degreeU, degreeV,
  knotsU, knotsV,
  [nsControlPoints],
);

The NURBSSurface constructor was happy. My next problem was with the ParametricGeometry with the call to getPoint(0, 0, <0, 0, 0>). Actually, it was the call passing the arguments to the nurbsSurface.getPoint that was hanging. My browser was unresponsive.

It seems I was setting my degrees to 32. It’s a very high number and computationally intensive. Hard coding them both to 3 got the browser to be responsive, but resulted in the original errors about a missing index at zero. Its time to look at the actual constructor.

    const len1 = knots1.length - degree1 - 1;
    const len2 = knots2.length - degree2 - 1;
    for (let i = 0; i < len1; ++i) {
     this.controlPoints[i] = [];
      for (let j = 0; j < len2; ++j) {
        const point = controlPoints[i][j];
        this.controlPoints[i][j] = new Vector4(point.x, point.y, point.z, point.w);
      }
    }

From here, I see there is a relationship between the degrees, knots, and length of control points. I feel like there should have been a guard clause here. I need to change my values of my knots and degrees after seeing the light of what is going on here. Lets do some simple algebra.

Degrees = 3
Path Length = 32
Path Length = Knots – Degrees – 1
32 = Knots – 3 – 1
Knots = 28

In code, the following:

  const degreeU = 3;
  const degreeV = 3;
  const knotsU = new Array(horizontalSegments - degreeU - 1).fill(0);
  const knotsV = new Array(verticalSegments - degreeV - 1).fill(0);
  const nurbsSurface = new NURBSSurface(
    degreeU, degreeV,
    knotsU, knotsV,
    nsControlPoints,
  );

We got past console errors setting up the NURBS surface. The call to getPoint was working as well. The ParametricGeometry was crated. Everything is A-Okay! Or not. The new problem is with calculating the bounding sphere for the ParametricGeometry while rendering the scene.

While I’m looking at the NURBSSurface code, it’s time to look at knots. What are they? The constructor just stores them for later use. They are used in the getPoint method. It looks like the first and last value in the array is actually a value that is used directly. I had originally thought it was an index lookup. Then it sends the knots to calculate the surface point.

const u = firstKnotU + x * (lastKnotU - firstKnotU);
const v = firstKnotV + y * (lastKnotV - firstKnotV);
calcSurfacePoint(degreeU, degreeV, knotsU, knotsV, controlPoints, u, v, target);

Something looks off… the getPoint function doesn’t return anything. Maybe calcSurfacePoint updates a pointer? Yes! It makes a call to target.set. Okay, that looks good. It starts to get into some logic to find the correct knot/span. Lets get back to our knots. I had set them all to zero, thinking they were index values. Perhaps they should all be 3 for the degrees themselves. Nope. Same error. Lets look at the value of the target before and after the call to getPoint.

Whoa… yes. This is a problem. All values on the points are NaN.

Vectors Without Numbers

Looking at the nsControlPoints sent to the NURBS surface, they all have values. Looking at the constructed NURBSSurface, it too has values for all points. I’m wondering about the slices and stacks passed to the ParametricGeometry constructor. It looks like they are optional. Removing them had no effect. Let’s look at the original example for insight. The example only has a net of 4×3 control points, but the parametric geometry is 20×20. That makes sense, as you need many vertices to create a smooth curve affected by the curve. Let’s bump up our segments to see if that has an effect. Nope. I think I found the problem in the ParametricGeometry that was created. It’s full of NaN for its 49,923 positions.

ParametricGeometry positions NaN

Why is the constructor filling it with NaN? The ParametricGeometry constructor instantiates five Vector3 objects. By default, they all have xyz values set to zero. The position attribute is passed an array of vertices. The vertices is populated with the p0 xyz values after calling the function (in my case, getPoints, and p0 is the target). There are a few calls from pv and pu to get subVectors using p0. I think I’m too far into the weeds here. I see the default function is using some trigonometry on the target, and both slices and stacks are set to 8. Let’s see if we can get the geometry to work without the influence of NURBS.

Default ParametricGeometry

Yes. It works. I got a slightly curved plane to appear. Lets restore our NURBS code and see if it ever had a number when getPoint was called. Yes. The first two calls to get point actually had numbers on the target. This means that the `nurbsSurface.getPoint` call is providing invalid numbers.

nurbsSurface.getPoint(0, 0, <x:0, y:0, z:0>)
// new value <x:NaN, y:NaN, z:NaN>
nurbsSurface.getPoint(0.00001, 0, <x:0, y:0, z:0>)
// new value <x:NaN, y:NaN, z:NaN>

I saw the example code started their knots with zero. Maybe my knots need to do the same? Nope. Setting the first index to zero had no influence. Setting the last knot had no influence as well. Looking at getPoint, it seems like lastKnot – firstKnot should have a value that is not zero, otherwise the uv mapping would aways be the first knots value.

I keep going in the weeds trying to evaluate what the code is trying to do. Let’s use a debugger and step through what is going on. I found that a call to calcBasisFunction is returning four NaN numbers.

Debugging calcSurfacePoints
const knots = Array(28).fill(3);
knots[0] = 0;
const span = 24;
const degrees = 3;
const u = 3;
calcBasisFunctions(span, u, degrees, knots);
// Output: [NaN, NaN, NaN, NaN]

Debugging the calcBasisFunctions showed me a scenario where zero was being multiplied by Infinity, resulting in NaN.

To Infinity, and Beyond

This seems like it has to all to do with my knots. I’ve tried filling the first and second values with zeros and ones, but I had the same problem. I needed to figure out how to influence the right and left values of the span.

I feel like I’m learning about NURBS from the wrong direction. I’m brute force learning it via trial by fire with implementation instead of learning the concepts of it. Time to research what knots are.

It seems like knots are something added in addition to control points. They are evenly spaced apart along a path and control how much influence a control point has on the paths curvature at that specific spot, in relation to the control points. Some software uses the distance between control points rather than using knot values. A span is the position between two knots which exhibits the curve. I’m creating multiple spline curves/braziers in a three dimensional space along the vertical segments, and the horizontal segments.

The concepts make sense, but I don’t have a working knowledge with controlling them with 3D modeling software. Lets try to create the surface with the control points in the example and go from there. It was huge, but a little bit of modifications to the vertices got it within view of the camera.

NURBS Surface with UFO Control Vertices

The knot values are only zero or one. I thought they were the amount of degrees to be applied along the path. Perhaps its the percentage of the overall degree. Lets try modifying mine to go from a couple zeros before changing to one. Nope. Same problem.

I took a look at how the number of knots was different than I expected. I worked out the logic of how to determine the knots as being the following:

// KnotCount = Segments + Degree + 1
const knotsU = Array(nsControlPoints.length + degreeU + 1).fill(1);
const knotsV = Array(nsControlPoints[0].length + degreeV + 1).fill(1);

Unfortunately, it didn’t work. Perhaps I need to scale the knots gradually from zero to one. Wait, it worked? That was it? Are you kidding me? It’s not perfect, but I see the shape of my UFO.

UFO Made with NURBS

The model has some problems with faces flying off into the distance and issues with the poles, but I can work with this! I’m wondering if the knot values actually represent where they are placed along the path (0% at start, 100% at end), or if they are the relative distance to the next knot. In any case, I think it represents where the knot is located along the path. I started experimenting with the degrees and found the model would have hard edges as the degrees were reduced. It seemed like the mapping for normals was affected for zero degrees as the model didn’t receive any light.

Degrees V = 0
Degrees V = 1
Degrees V = 2
Degrees V = 3

I checked out my cube to see the effect of NURBS on it as well, and found that just like the Second Life viewer, its edges were soft. You can even see the control vertices floating off on the edge of the cube. They are influencing the shape of the model, but they themselves are not the vectors that make up the model.

Vectors as NURBS
Second Life Sculpted Prim Preview

After making a few changes, I was able to get textures to work on the object as well. I didn’t have any fights with how faces were created. They just worked. I feel like I could have saved two days if I skipped making the geometry out of the vertices and went directly to NURBS. I did catch a lot of other small issues along the way, so it wasn’t for nothing.

NURBS UFO with texture

Now that I had a NURBS model, I changed the control net to display as a wireframe instead of a separate object so that it could overlay the NURBS model.

Control Net
NURBS and Control Net

We are almost in the home stretch. All of this has been leading up to letting the end user edit the model by moving a vector. We are not there yet, but we are so close. We need to figure out why the model has faces flying off at times.

I took a break with dinner and came back to play with the interface. I added a few input fields to enter the objects rotation and scale as numbers. I setup the rotation as degrees and took a bit of time to figure out how to map it so that 0 Degrees = a rotation of 0. It sounds easy until you get to the part where 180 degrees starts at -Math.PI, and 179.80 ends at Math.PI. I choose this mapping because its the same mapping as the Second Life viewer. Or is it? Let me reconfirm… Yes. 0.0 to 359.95.

Object Rotation
Input for Rotation & Scale

I think after all of that work getting the model to render over the past few days has burnt me out a bit, and I needed a bit of a break by focusing on other stuff. Continuing my insubordinate rebellion against the angst of burnout, I mapped a list of shortened names for my textures and models to make them a bit more easier to find.

Short texture names

The rebellion must end sometime. Let it be now.

Back to NURBS. We got our model. It looks almost good. Just about every model with a 16×256 image is broken. Let’s see if the “hidden vertices” used in looping may be the cause. One thing I’m noticing is that the texture is looping, and overlapping 3 quads. It changes based on the degrees of the control vectors. 1 degree is one cell of overlapping. 2 degrees has 3 overlapped cells. 3 degrees looks like 4 cells, but its difficult to tell.

NURBS 1°
NURBS 2°
NURBS 3°

Just doing more research. What I’m attempting to do is a closed Cubic NURBS Unclamped for each horizontal and vertical set of control points. When looking at one row of control points, I’ve been calling it a path, when it should be referred to as a span. Hold up – each individual area between the two control points is a span. Degree + Span = control point count. But that is for a path that doesn’t connect. I may need the -1. Maybe I have 32 spans, with 1 degree. Or 33 spans to close the loop?

I think I figured something out. I added a control to the UI to modify the degrees. I then added vectors from a spherical index for each segment, and a few more for each degree. Wait… maybe I need to reduce the vertical segments as degrees are increased since I always have the same number of control points. I can’t control the total number of control vertices in the image. I can’t add extra ones on the fly as I wouldn’t know where to put them, or how to move the other vertices to make everything equal out. Maybe chasing after this “3rd Degree” for sculpted prims is a bit of a misnomer. No one said that the standard degree for NURBS was also applied to Sculpted Prims.

With that said, the model looks okay at 1 degree, but it has a visible seam letting you see inside of it. The two edges of the parametric geometry do not meet.

NURBS 1° with hollow seam

Other 16×256 models still fail to load. Maybe I shouldn’t be using a NURBS surface mesh. There is still the volume mesh. Looking at the NURBS example, it seems like they use multiple ParametricGeometry objects and control what side of a cube the represent in a 3D space of control vectors. However, I am seeing something for MeshLambertMaterial. Lets look and see what that is… nope. Good to know, but not what I’m looking for.

I found some info on Rhino with What are NURBS?. They use integers for their knots, and an integer may not repeat more than degree times. I was progressing from 0 to 1. Changing a 32 segment path (31 spans) with 3 degrees resulted in some geometry errors during rendering and a knot path of:

0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10

Since I’m going in order from 0 to 10 without skipping a number, the knots are uniform (The U in NURBS means uniform). If I skipped a number, it would be Non-Uniform (The NU in NURBS means non-uniform). Since I’m duplicating my knots, the curve isn’t smooth in those areas. Okay… so, I want 0 to 32 to keep everything smooth? This is the same as 1 degree NURBS! Wait… I think I need 0 to 31, and add another 31 for the seam? Nope – that just creates an open seam that is vertical that is as wide as two vertices.

I think I’m coming to an end of NURBS. I’ve figured out how to implement it. Many models wont display if they are not from a square image. Other modals tend to have problems with the open seams. Just because Second Life uses it, doesn’t mean I need to. I’m still a bit confused as to why they choose to use NURBS in the first place over a basic mesh of geometry. It’s not like they were adding more complicated mesh to take advantage of the curves. If Anything, it added an additional layer of complexity.

Ok, I’ve got my objects showing up normal again. The NURBS modal is able to be displayed separately – if there is a square image loaded. I’m going to take a look at ParametricGeometry. I think it can help with the mapping at the top and bottom poles. I noticed the texture was smooth at the poles with NURBS. Maybe something useful can come out of this little journey.

Parametric UV mapping on a cube
Bottom pole UV mapping is off

Isn’t that pretty? The ParametricGeometry had UV mapping already built in, and it was perfectly matched up for the textures. The top pole is perfect. Everything goes to the center. No onion heads! Unfortunately I see the return of a pole mapping problem on the bottom. Everything is going to a corner in the second row.

Mucking about, I’m not getting any closer to the answer. It looks like the parametric geometry is getting the wrong information for the pole. I’ve tried different methods of changing the slices and stacks, mapping the points, and even applied the UV mapping from the model used before NURBS. I’m unable to improve the mapping on the bottom pole. Perhaps its not creating its own pole at the bottom. Maybe a wireframe will reveal something.

ParametricGeometry wireframe

As suspected, the geometry is not creating a pole for the last row. What’s even more interesting is that the control net wireframe doesn’t have a pole at the bottom either. What is going on?

Spherical index again? Really? It seems that something is off with the bottom rows center vertex. For the time being, I added a hack so that the last index is returned instead when getting the index for the last row.

  if(y < 0) {
    y = 0;
  } else if(y >= verticalSegments) {
    // HACK: seems center pixel is not in the proper place?
    return ((horizontalSegments + 1) * verticalSegments);
    // y = verticalSegments - 1;
  }
Bottom of Parametric Geometry with center pole

With that being done, it’s late. I’m about to head off to bed. Tomorrow we get to look at how to edit individual vertices!

Commentary of work done today

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading