With our geometry being read, built, and saved in the proper order, it’s time to look back at the dynamic density map. Looking back at the problem with the top hat, I’m now seeing that the original problem where a lot of transparency was showing up has mostly corrected itself.


The transparent edges are due to the “hidden” rows where vertices were doubled, tripled, and even quadrupled to ensure hard corners when working with NURBS. This results in transparent faces since the face does not have any area.
My focus today is the top and bottom of the texture map. A sphere only has one triangle for each quad around the pole. Ideally, it should be represented as many right triangles lined up beside each other at the top and bottom of the texture map. In my case, all the triangles for the top and bottom rows join at the center of the image instead. This creates a weird pattern at the top of the model when the texture is applied, as the transparent part of all triangles written wrong is also showing up.



Giving it some thought, I know what the problem is. I reused the logic that was used to assemble the geometry using indexes that map to a row/column in the vertices grid. The poles always map to the center column, which in turn maps to the center of the image. Rather than use the indexes or row/column values, I need a list of texture coordinates instead.
Here is the logic that I’m currently using that gives me indexes of vertices for each triangle.
function createSphericalControlTriangles(options) {
var indexedTriangles = [];
for(let column = 0; column < options.columns; column++) {
for(let row = 0; row < options.rows; row++) {
const centerIndex = rowColumnToIndexOfVertex(row, column, options);
const bottomRightIndex = rowColumnToIndexOfVertex(row + 1, column + 1, options);
const bottomIndex = rowColumnToIndexOfVertex(row + 1, column, options);
const rightIndex = rowColumnToIndexOfVertex(row, column + 1, options);
// Add triangles in counter-clockwise order
if(row === 0) {
// triangles at top pole
indexedTriangles.push(centerIndex, bottomIndex, bottomRightIndex);
} else if(row === options.rows - 1) {
// triangles at bottom pole
indexedTriangles.push(centerIndex, bottomRightIndex, rightIndex);
} else {
// quads in the middle of poles
indexedTriangles.push(centerIndex, bottomRightIndex, rightIndex);
indexedTriangles.push(centerIndex, bottomIndex, bottomRightIndex);
}
}
}
return indexedTriangles;
}
I duplicated the logic and changed everything from indexes to coordinates. I’ve made a bit of progress. My triangles are not perfect, but they are filling in a bit more now.

Looking at the texture map, I am now seeing shark fins across the top and bottom of my texture map.


The top has a problem. That row of shark fins needs to start at the top. But wait a minute… They are extending all the way from top to bottom, and slightly moving away from the center with each face. I think we have a different problem. Let’s look at the number of cells in each row.

I’m only counting 31 triangles. There should be 32. Since my columns only represent 32 of 33 vertices due to stitching on the right/left edges, I believe I need to account for that and simulate an extra column of vertices for the texture coordinates as well.
No… I’m getting the same result. Perhaps the triangle is still being drawn, but past the boundaries of the image.
I realized that I’m thinking about this all wrong. I was still trying to tie the texture map to the locations of the coordinates in terms of where each rows Y position started. Mind you, the first and last rows would be very short. What I need is 32 even cells in each row, and 33 rows vertically. Rather than mapping coordinates from indexes, I just need to divide the total width by 32, the height by 33, and loop through x/y multiplying the cell width/height to get each corresponding texture coordinate.
Here is what I’ve got so far…
function getTextureMapCoordinatesForTriangles({
width,
height,
mapping: {
x: columnCount, // 32
y: rowCount // 33
}
}) {
var xyTriangles = [];
const cellWidth = width / columnCount;
const cellHeight = height / rowCount;
for(let column = 0; column < columnCount; column++) {
for(let row = 0; row < rowCount; row++) {
const x = column * cellWidth;
const y = row * cellHeight;
const centerXy = {x, y};
const bottomRightXy = {
x: x + cellWidth,
y: y + cellHeight
};
const bottomXy = {
x,
y: y + cellHeight
};
const rightXy = {
x: x + cellWidth,
y
};
// Add triangles in counter-clockwise order
if(row === 0) {
// triangles at top pole
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
} else if(row === rowCount - 1) {
// triangles at bottom pole
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
} else {
// quads in the middle of poles
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
}
}
}
return xyTriangles;
}
It is creating 32 triangles – but the mapping doesn’t match up with the vertices geometry, and so I get a diagonal swirl pattern and transparent seam.


Mentally visualizing what’s going on under the hood is a bit difficult and challenging. I think this has something to do with the hidden column used for stitching. The two arrays for texture map xy coordinates and face vertex indexes need to match in length in order to pull off the one-to-one relationship between the face in 3D space, and its location on the texture map.
I’ve gotten a little further. The maps no longer appear with spiral patterns, and the models wireframe matches the vertical lines. The horizontal lines don’t match up at the center of my models.


Okay… I solved the problem, but I don’t trust that I solved the problem. All of my points line up. The mesh lines up. The math just isn’t mathing in my mind. I need to think this through to understand why what I did works…
The main issue at hand is that instead of 32 sharks at the top of my texture map, I only have 31 – yet everything lines up.

I drew some lines on the first and last columns. Sure enough, the two columns appeared on the model.

Looking at the back of the model, I’m seeing what I expected to see. A mismatch between the texture map against both the point cloud and wireframe.

My hunch was correct. The math doesn’t math.
Okay, so I’m back again with the long transparent strip on the side of my models.

The triangles being evaluated here are representing the vertices that connect the left and right sides of the model to close the loop. It’s the “hidden” column of vertices that the end-user does not create. Instead, these values are sampled from the first column. I can visually see the faces. These triangles should have an area greater than one. On alignment map 1024, this is column AF – the last column of 32. It shows up.
So what is up with the area? Why is it zero? Is it zero?
Well… my gosh. I have it in the comments as to what is going on. I’m excluding faces that wrap.
// skip wrapping triangles
const rc1 = indexOfImageDataToRowAndColumn(index1, options);
const rc2 = indexOfImageDataToRowAndColumn(index2, options);
const rc3 = indexOfImageDataToRowAndColumn(index3, options);
if(rc1.column === 0 || rc2.column === 0 || rc3.column === 0) {
// stitched
if(
rc1.column === options.columns - 1 ||
rc2.column === options.columns - 1 ||
rc3.column === options.columns - 1
)
continue;
}
This was written on account that I thought the faces would have no area, or cause problems with textures. The fact of the matter is – that is only for UV mapping that needs to skip the last column of wrapped faces.
Here are a few density maps in all their glory.






I can’t believe I finally got it. I feel like I’ve been working on the density map problem for so long. Now that its over, I don’t know what to do with the rest of my life. Here is the code that was involved.
function getTextureMapCoordinatesForTriangles({
width,
height,
mapping: {
x: columnCount,
y: rowCount
}
}) {
var xyTriangles = [];
const cellWidth = width / columnCount;
const cellHeight = height / (rowCount - 1);
for(let column = 0; column < columnCount+1; column++) {
for(let row = 0; row < rowCount; row++) {
const x = column * cellWidth;
const y = row * cellHeight;
const centerXy = {x, y};
const bottomRightXy = {
x: x + cellWidth,
y: y + cellHeight
};
const bottomXy = {
x,
y: y + cellHeight
};
const rightXy = {
x: x + cellWidth,
y
};
// Add triangles in counter-clockwise order
if(row === 0) {
// triangles at top pole
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
} else if(row === rowCount - 1) {
// triangles at bottom pole
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
} else {
// quads in the middle of poles
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
}
}
}
return xyTriangles;
}
function getIndexesOfTriangleVectorIndexes(options) {
const {
width,
height,
mapping: {
x: columnCount,
y: rowCount
}
} = options;
var indexedTriangles = [];
for(let column = 0; column < columnCount+1; column++) {
for(let row = 0; row < rowCount; row++) {
const centerIndex = rowColumnToIndexOfVertex(row, column, options);
const bottomRightIndex = rowColumnToIndexOfVertex(row + 1, column + 1, options);
const bottomIndex = rowColumnToIndexOfVertex(row + 1, column, options);
const rightIndex = rowColumnToIndexOfVertex(row, column + 1, options);
// Add triangles in counter-clockwise order
if(row === 0) {
// triangles at top pole
indexedTriangles.push(centerIndex, bottomIndex, bottomRightIndex);
} else if(row === options.rows - 1) {
// triangles at bottom pole
indexedTriangles.push(centerIndex, bottomRightIndex, rightIndex);
} else {
// quads in the middle of poles
indexedTriangles.push(centerIndex, bottomRightIndex, rightIndex);
indexedTriangles.push(centerIndex, bottomIndex, bottomRightIndex);
}
}
}
return indexedTriangles;
}
Let’s go ahead and fix the transparency bits showing through the edges. I can get the edges to be crisp by increasing the image height. However, the three.js library limits the height to 16,384 pixels high (0x4000) and gives a warning message for anything higher.
THREE.WebGLRenderer: Texture has been resized from (4096×32768) to (2048×16384).


It’s good enough to see that increasing it any further is not actually going to help me. Let’s give the zero area a different color… and it wasn’t looking well. I decided to give the image a checkerboard pattern instead before I draw the triangles.


Here is the code to make sure there are 256 squares along the shortest dimension.
function drawTransparencyBackground(ctx, {width, height}) {
const cellSize = Math.min(width, height) / 256;
const xCells = width / cellSize;
const yCells = height / cellSize;
const color1 = 'rgba(255, 255, 255, 1)';
const color2 = 'rgba(191, 191, 191, 1)';
for(let x = 0; x < xCells; x++) {
for(let y = 0; y < yCells; y++) {
ctx.fillStyle = ((x+y) % 2 === 0) ? color1 : color2
ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
}
So… onto the next dynamic map. The next map will give a visual indication of how far the center of a face is from the center of the model. Why do we need this? I don’t know. Curiosity?


Well… that was very fast. Everything becomes easy as long as the underlying logic has been worked out to support it.
Next, let’s create a dynamic map that helps us identify the sides of our geometry. I’ll use a hue to determine what angle the face is facing away from the Z axis. As the face angles upwards, I’ll increase the luminance to 100%. Facing down, I’ll decrease it to 0%. We should get something similar looking to the original image maps to load the geometry with, but a bit off since it’s for the faces instead of the vertices. Although the cube has many vertices along each side, they are at different positions in the model – but all of their faces are oriented in the same direction, so we should see a consistent color for each side – 4 colors with a wide white rectangle on top, and a wide black rectangle on the bottom. This would help with creating new textures for existing models within image editors.
I’ve got something. It’s not perfect, but its a start.


There is trigonometry involved trying to work out what angle a face is facing away from the center. I think I got that part worked out. Just as I had predicted, there would be four distinct colors for the sides. I’m having trouble with the luminance. The top should be white, and the bottom should be black. It looks mixed, and we have one of our sides showing up as white as well.
I’ve got a policy and bylaws committee meeting that I need to attend to tonight. Here is the main bit of logic I’m working with.
const colors = [];
for(let i = 0; i < triangles.length; i++) {
const [vector1, vector2, vector3] = triangles[i];
const faceCenter = new THREE.Vector3()
.add(vector1)
.add(vector2)
.add(vector3)
.divideScalar(3);
let horizontalAngle = Math.atan2(
vector1.x - faceCenter.x,
vector1.z - faceCenter.z
);
horizontalAngle *= 180 / Math.PI;
if(vector1.y < faceCenter.y) {
horizontalAngle += 180;
}
if(horizontalAngle < 0) horizontalAngle += 360;
horizontalAngle = horizontalAngle % 360;
let verticalAngle = Math.atan2(
vector1.y - faceCenter.y,
vector1.z - faceCenter.z
);
verticalAngle *= 180 / Math.PI;
verticalAngle += 90;
if(vector1.x < faceCenter.x) {
verticalAngle += 90;
}
const hue = Math.floor(horizontalAngle);
const luminance = Math.floor(100 * (verticalAngle / 180));
colors.push(hslToRgbHex(hue, 100, luminance));
};
I’m trying to work it out so that any face that faces up will be white. Any face that faces down will be black. And you’ll see gradual colors in between.
And I’m back. I made a few recommendations to our policies and got to tour the old computer lab that’s currently being renovated into a maker space.
So about this issue with arctangents… I hadn’t taken trigonometry in high school, but the concepts seem simple enough. I think I need to take the brute force approach and get the maximum and minimum values of vertices… maybe I just need to google some math.
Did I get it? Here is what I see so far…




It looks really close. The problem I see is that the sphere angle map ends with black on the right hand side instead of red. Is it fading into grey? I’m wondering if this is an issue with my color conversion.
Uh.. yea. console.log(hslToRgbHex(316, 100, 28), '#8f0069') is returning #000069. What happened to my red channel? Let’s take a look at how I’m converting colors…
function hslToRgbHex(hue, saturation, luminance) {
// normalize values
hue /= 360;
saturation /= 100;
luminance /= 100;
// edge of luminance is black & white
if(luminance === 0) return HEX_BLACK;
if(luminance === 1) return HEX_WHITE;
if(saturation === 0) {
const gray = Math.floor(luminance * 255);
return rgbAsHex(gray, gray, gray);
}
const upper = luminance < 0.5 ?
luminance * (1 + saturation) :
luminance + saturation - luminance * saturation;
const lower = 2 * luminance - upper;
function channelIntensity(min, max, hueOffset) {
hueOffset = Math.max(0, Math.min(1, hueOffset));
let value = min;
if (hueOffset < 1 / 6) value = min + (max - min) * 6 * hueOffset;
else if (hueOffset < 1 / 2) value = max;
else if (hueOffset < 2 / 3) value = min + (max - min) * (2 / 3 - hueOffset) * 6;
return Math.round(value * 255);
}
return rgbAsHex(
channelIntensity(lower, upper, hue + 1 / 3),
channelIntensity(lower, upper, hue),
channelIntensity(lower, upper, hue - 1 / 3)
);
}
Well.. there it is. I’m clamping the hue offset. How on earth is anything past the blue hue supposed to show any red? The channelIntensity function needs to offset the offset if it is out of bounds.
function channelIntensity(min, max, hueOffset) {
hueOffset = (hueOffset + 1) % 1;
let value = min;
if (hueOffset < 1 / 6) value = min + (max - min) * 6 * hueOffset;
else if (hueOffset < 1 / 2) value = max;
else if (hueOffset < 2 / 3) value = min + (max - min) * (2 / 3 - hueOffset) * 6;
return Math.round(value * 255);
}




This is all good. This is great.
I took a bit of a break and added a few more color gradients for the dynamic maps. Here is a preview list of every gradient available now.






I haven’t been working with any models with image ratio dimensions other than 1:1. There is a reason for that. I’ve been having trouble applying dynamic maps to them. I just figured out why as well. Although I was reading from a 16×256 image, I was writing to a 1024×1024 image. The two different image sizes would normally have different counts of total vertices used to build the model. The answer was easy enough. I supplied both the source and target information so that I use the row/column counts from the source, and the image height/width from the target to calculate the cell size.
function getTextureMapCoordinatesForTriangles(source, target) {
const { x: columnCount, y: rowCount} = source.mapping;
const { width, height } = target;
var xyTriangles = [];
const cellWidth = width / columnCount;
const cellHeight = height / (rowCount - 1);
for(let column = 0; column < columnCount+1; column++) {
for(let row = 0; row < rowCount; row++) {
const x = column * cellWidth;
const y = row * cellHeight;
const centerXy = {x, y};
const bottomRightXy = {
x: x + cellWidth,
y: y + cellHeight
};
const bottomXy = {
x,
y: y + cellHeight
};
const rightXy = {
x: x + cellWidth,
y
};
// Add triangles in counter-clockwise order
if(row === 0) {
// triangles at top pole
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
} else if(row === rowCount - 1) {
// triangles at bottom pole
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
} else {
// quads in the middle of poles
xyTriangles.push([
centerXy,
bottomRightXy,
rightXy
]);
xyTriangles.push([
centerXy,
bottomXy,
bottomRightXy
]);
}
}
}
return xyTriangles;
}
Vertex Editing
It’s been a while, but lets move onto one of our original goals that led us down this path of dynamic maps. Vertex editing. Currently we can edit the position of the selected vertex using the numeric input and range sliders. I want to be able to move it by dragging the transform controls in the scene.

I added another tool and was able to get the transform controls to appear when it was selected. I had a bit of fighting with the camera orbit controls and a few other pieces of code, but I think it’s mostly because I never had any of the tools checked by default when the page first loads.
And with that, I’m going to cut out a bit early.

