35
loading...
This website collects cookies to deliver better user experience
latLngToCartesian
function. What we'll do is iterate over latitude and longitude kinda like how we would over X and Y to draw a square.export function sphere(density){
const radsPerUnit = Math.PI / density;
const sliceVertCount = density * 2;
const positions = [];
let latitude = -Math.PI / 2;
//latitude
for(let i = 0; i <= density; i++){
if(i === 0 || i === density){ //polar caps
positions.push(latLngToCartesian([1, latitude, 0]));
} else {
let longitude = 0;
for (let j = 0; j < sliceVertCount; j++) {
positions.push(latLngToCartesian([1, latitude, longitude]));
longitude += radsPerUnit;
}
}
latitude += radsPerUnit;
}
...
density * 2
because it has to rotate 2*PI instead of PI like latitude does. This allows use to control the sphere with one number though we can just as easily split it into 2 if necessary.//colors
const colors = [];
for(let i = 0; i < positions.length; i++){
colors.push([1, 0, 0]);
}
//triangles
const triangles = [];
for(let ring = 0; ring < density - 1; ring++){ // start at first ring
const initialP = (ring * sliceVertCount) + 1;
for (let sliceVert = 0; sliceVert < sliceVertCount; sliceVert++){
const thisP = initialP + sliceVert;
const nextP = initialP + ((sliceVert + 1) % sliceVertCount);
if(ring === 0){
triangles.push([0, nextP, thisP]);
}
if(ring === density - 2){
triangles.push([thisP, nextP, positions.length - 1])
}
if(ring < density - 2 && density > 2){
triangles.push([thisP, nextP + sliceVertCount, thisP + sliceVertCount])
triangles.push([thisP, nextP, nextP + sliceVertCount])
}
}
}
density - 2
. This can be a little confusing but if you get it wrong just play with the start and ending indices until it works.sliceVertCount
from above. The current vertex is thisP
. We also find what the next vertex in the ring would be nextP
and this will wrap around once we hit the end of the ring. Now there are 3 cases. For ring 0, each vertex is part of the bottom pyramid so we make a triangle with the bottom point and the next point. For ring density - 2
, the vertex is part of the top pyramid and so we make that triangle with the top point and the next point. If the ring index is greater than 0 and density is greater than 2, that means the sphere has quad bands and so we construct the quad with the ring above. The corresponding vertex in the next band will be sliceVertCount vertices ahead. And so with the four vertices thisP
, nextP
, thisP + sliceVertCount
, and nextP + sliceVertCount
we have a quad and so we add both triangles of the quad. The real trick here is to make sure the winding order is correct. For our engine this means counter-clockwise and by convention I like to start in the lower left.//positions and UVs
const positions = [];
const uvs = [];
let latitude = -Math.PI / 2;
//latitude
for(let i = 0; i <= density; i++){
if(i === 0 || i === density){ //polar caps
positions.push(latLngToCartesian([1, latitude, 0]));
uvs.push(0.5, latitude > 0 ? 1 : 0);
} else {
let longitude = 0;
//longutude
for (let j = 0; j < sliceVertCount; j++) {
positions.push(latLngToCartesian([1, latitude, longitude]));
uvs.push([inverseLerp(0, TWO_PI, longitude), inverseLerp(-QUARTER_TURN, QUARTER_TURN, -latitude)])
longitude += radsPerUnit;
}
}
latitude += radsPerUnit;
}
0.5
, we also don't need to calculate anything because it's either PI / 2
or -PI / 2
so we can just immediately set it as 0
or 1
in the V coordinate based on if it's negative or not. For all other points we do 2 inverse lerps one for U and one for V. An "inverse lerp" if you're unfamiliar takes a value in a range and converts it to a normalized value between 0 and 1. The range for U is 0
to 2*PI
and the range for V is -PI/2
to PI/2
.inverseLerp
looks like:export function inverseLerp(start, end, value) {
return (value - start) / (end - start);
}
0.95
to 0
(these are not the exact values though). This causes the UV plotting to go haywire because we're now trying to map from 0 to 0.95 going left instead of right and we get a seam. Sadly this is actually a problem with the vertices, we need the ones on the seam to be both 0 and 1 which is not possible because only 1 set of UVs can be associated with a vertex. So we actually need to change our code to duplicate the final vertex of each ring so that we can have a different UV value, sucks but 'dems the rules.//positions and UVs
const positions = [];
const uvs = [];
let latitude = -Math.PI / 2;
//latitude
for(let i = 0; i <= density; i++){
const v = inverseLerp(-QUARTER_TURN, QUARTER_TURN, -latitude);
if(i === 0 || i === density){ //polar caps
positions.push(latLngToCartesian([1, latitude, 0]));
uvs.push(0.5, latitude > 0 ? 0 : 1);
} else {
let longitude = 0;
//longitude
for (let j = 0; j < sliceVertCount; j++) {
positions.push(latLngToCartesian([1, latitude, longitude]));
uvs.push([inverseLerp(0, TWO_PI, longitude), v]);
if (j === sliceVertCount - 1) { //on the last set we create a seperate vertex for the UV end
positions.push(latLngToCartesian([1, latitude, longitude + radsPerUnit]));
uvs.push([1, v]);
}
longitude += radsPerUnit;
}
}
latitude += radsPerUnit;
}
//triangles
const triangles = [];
for(let ring = 0; ring < density - 1; ring++){ // start at first ring
const initialP = (ring * (sliceVertCount + 1)) + 1;
for (let sliceVert = 0; sliceVert < sliceVertCount; sliceVert++){
const thisP = initialP + sliceVert;
const nextP = initialP + sliceVert + 1;
if(ring === 0){
triangles.push([0, nextP, thisP]);
}
if(ring === density - 2){
triangles.push([thisP, nextP, positions.length - 1])
}
if(ring < density - 2 && density > 2){
triangles.push([thisP, nextP + sliceVertCount + 1, thisP + sliceVertCount +1])
triangles.push([thisP, nextP, nextP + sliceVertCount + 1])
}
}
}
initialP
needs to take into account that there is an extra vertex per row.0.5
as the U value at the poles because it really shouldn't matter what the point is because the first and last rows are complete compressed horizontally into a single point.//positions and UVs
const positions = [];
const uvs = [];
let latitude = -Math.PI / 2;
//latitude
for(let i = 0; i <= density; i++){
const v = inverseLerp(-QUARTER_TURN, QUARTER_TURN, -latitude);
let longitude = 0;
let vertLength = sliceVertCount + ((i > 0 && i < density) ? 1 : 0); //middle rings need extra vert for end U value
//longitude
for (let j = 0; j < vertLength; j++) {
positions.push(latLngToCartesian([1, latitude, longitude]));
uvs.push([inverseLerp(0, TWO_PI, longitude), v]);
longitude += radsPerUnit;
}
latitude += radsPerUnit;
}
//triangles
const triangles = [];
let ringStartP = 0;
for(let ring = 0; ring < density; ring++){ // start at first ring
const vertexBump = (ring > 0 ? 1 : 0);
for (let sliceVert = 0; sliceVert < sliceVertCount; sliceVert++){
const thisP = ringStartP + sliceVert;
const nextP = ringStartP + sliceVert + 1;
const nextRingP = thisP + sliceVertCount + vertexBump;
const nextRingNextP = nextP + sliceVertCount + vertexBump;
if(ring === 0){
triangles.push([thisP, nextRingNextP, nextRingP]);
}
if(ring === density - 1){
triangles.push([thisP, nextP, nextRingP]);
}
if(ring > 0 && ring < density - 1 && density > 2){
triangles.push([thisP, nextRingNextP, nextRingP])
triangles.push([thisP, nextP, nextRingNextP])
}
}
if(ring === 0){
ringStartP += sliceVertCount;
} else {
ringStartP += sliceVertCount + 1;
}
}
density*2
vertices and the rest have density*2 + 1
vertices it makes figuring out where you are a bit hard. We can no longer multiply easily so it felt best to add the conditional at the bottom where it's easier to tell. vertexBump
helps conditionalize the ring length as well, pretty unsightly but it's the best I came up with (it would totally be possible to optimize these). Finally we can simplify the loop to be from 0 to density
as we are considering the the poles as part of the rings now.normals: positions.flat(),
flat
is a copy so that's all we need.35