

wwwtyro.net
source link: https://wwwtyro.net/2021/09/24/rounded-box.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Procedurally generating a rounded box mesh
If you haven’t heard the story about Steve Jobs, Bill Atkinson, and rounded corners, it’s a fun read. A big takeaway is that rounded corners are everywhere. Let’s take a look at procedurally generating a rounded box mesh.
Face definitions
First we’ll define the faces of our box. We’ll define a corner for each face (the start), then two vectors representing up and right that we can use to traverse the face and fill it with polygons:
We’ll define the dimensions of our faces such that they compose a box of unit length on each axis and centered at the origin. Later, we’ll scale these so that we can create a box of arbitrary size.
const faces = [
// Positive X
{
start: vec3.fromValues(0.5, -0.5, 0.5),
right: vec3.fromValues(0, 0, -1),
up: vec3.fromValues(0, 1, 0),
},
// Negative X
{
start: vec3.fromValues(-0.5, -0.5, -0.5),
right: vec3.fromValues(0, 0, 1),
up: vec3.fromValues(0, 1, 0),
},
// Positive Y
{
start: vec3.fromValues(-0.5, 0.5, 0.5),
right: vec3.fromValues(1, 0, 0),
up: vec3.fromValues(0, 0, -1),
},
// Negative Y
{
start: vec3.fromValues(-0.5, -0.5, -0.5),
right: vec3.fromValues(1, 0, 0),
up: vec3.fromValues(0, 0, 1),
},
// Positive Z
{
start: vec3.fromValues(-0.5, -0.5, 0.5),
right: vec3.fromValues(1, 0, 0),
up: vec3.fromValues(0, 1, 0),
},
// Negative Z
{
start: vec3.fromValues(0.5, -0.5, -0.5),
right: vec3.fromValues(-1, 0, 0),
up: vec3.fromValues(0, 1, 0),
},
];
Face meshes
Now that we have our face definitions, let’s turn them into a mesh that we can render. We’ll write a function that takes a face definition, a width and height, and the number of steps to take across the face in both directions. The return value will be a list of vertices representing the triangles composing the face.
function grid(
start: vec3,
right: vec3,
up: vec3,
width: number,
height: number,
widthSteps: number,
heightSteps: number
) {
// We'll store our vertices here.
const positions: vec3[] = [];
// Traverse the face.
for (let x = 0; x < widthSteps; x++) {
for (let y = 0; y < heightSteps; y++) {
// Lower left corner of this quad.
const pa = vec3.scaleAndAdd(vec3.create(), start, right, (width * x) / widthSteps);
vec3.scaleAndAdd(pa, pa, up, (height * y) / heightSteps);
// Lower right corner.
const pb = vec3.scaleAndAdd(vec3.create(), pa, right, width / widthSteps);
// Upper right corner.
const pc = vec3.scaleAndAdd(vec3.create(), pb, up, height / heightSteps);
// Upper left corner.
const pd = vec3.scaleAndAdd(vec3.create(), pa, up, height / heightSteps);
// Store the six vertices of the two triangles composing this quad.
positions.push(pa, pb, pc, pa, pc, pd);
}
}
return positions;
}
Generating the box
Now that we have our face definitions and a means of generating a mesh for each, building the whole box is straightforward:
for (const face of faces) {
const positions = grid(face.start, face.right, face.up, 1, 1, 16, 16);
faceGeometries.push({ positions });
}
Sizing the box
Of course, we don’t always want a unit cube! Let’s scale our box according to some size:
// Define a size and resolution.
const size = vec3.fromValues(1, 1.25, 1.5);
const resolution = 16;
for (const face of faces) {
// Shift the start to accommodate the new size.
const start = vec3.multiply(vec3.create(), face.start, size);
// Calculate a width and height.
const width = vec3.length(vec3.multiply(vec3.create(), face.right, size));
const height = vec3.length(vec3.multiply(vec3.create(), face.up, size));
// Use the scaled values when building each face.
const positions = grid(start, face.right, face.up, width, height, resolution, resolution);
faceGeometries.push({ positions });
}
Rounding the box
Alright, now for the fun part - making our box round. To keep things easy to conceptualize and visualize, let’s consider the 2D case first. Here’s how I think about it: Imagine you have a rectangle. Put a circle in it. Slide the circle around in the rectangle and note what happens at the corners and edges when the circle impacts the sides. At the edges, the circle can butt up against the side of the rectangle without issue. At the corners, however, the circle gets stuck and is prevented from filling the corners entirely. Conveniently, the arc the circle traces in the corner is precisely what we want to use for our rounded box!
So, here’s what we can do: For each vertex composing our rectangle, we move our circle as close to it as we can. Then we draw a line from the center of the circle to the vertex. Wherever that line intersects the circle is where we’ll move our vertex. Voilà - a rounded rectangle!
Here’s a visualization of the approach. Changing the radius changes the size of the circle, and, consequently, how rounded the rectangle is. Changing the resolution changes the number of vertices used to approximate the rounded rectangle.
The 3D case is pretty much the same - instead of a circle in a rectangle, we’ll put a sphere in a 3D box. The following function takes a vertex, the box size, and the radius of our rounded edges and corners. It returns a new position and the normal at that point (which we get for free when we calculate the position).
function roundedBoxPoint(point: vec3, size: vec3, radius: number) {
// Calculate the min and max bounds of the sphere center.
const boundMax = vec3.multiply(vec3.create(), size, vec3.fromValues(0.5, 0.5, 0.5));
vec3.subtract(boundMax, boundMax, [radius, radius, radius]);
const boundMin = vec3.multiply(vec3.create(), size, vec3.fromValues(-0.5, -0.5, -0.5));
vec3.add(boundMin, boundMin, [radius, radius, radius]);
// Clamp the sphere center to the bounds.
const clamped = vec3.max(vec3.create(), boundMin, point);
vec3.min(clamped, boundMax, clamped);
// Calculate the normal and position of our new rounded box vertex and return them.
const normal = vec3.normalize(vec3.create(), vec3.subtract(vec3.create(), point, clamped));
const position = vec3.scaleAndAdd(vec3.create(), clamped, normal, radius);
return {
normal,
position,
};
}
Now we can use our new function when we generate our box face meshes:
// Define a size, radius, and resolution.
const size = vec3.fromValues(1, 1.25, 1.5);
const resolution = 16;
const radius = 0.25;
for (const face of faces) {
const start = vec3.multiply(vec3.create(), face.start, size);
const width = vec3.length(vec3.multiply(vec3.create(), face.right, size));
const height = vec3.length(vec3.multiply(vec3.create(), face.up, size));
const positions = grid(start, face.right, face.up, width, height, 16, 16);
// Move each vertex to its rounded position.
for (const position of positions) {
const rounded = roundedBoxPoint(position, size, 0.25);
vec3.copy(position, rounded.position);
}
faceGeometries.push({ positions });
}
A more efficient mesh
We’re wasting a lot of vertices in our mesh, but we can pare that down a bit. Notice that in the center of each face, there’s a large flat rectangular region that could be a single quad. And while the corners have curvature along two axes, the edges have curvature along only one, so we can use long skinny quads there instead of needlessly dividing them along their length. Take a look at the following diagram: the four corners have full subdivision, the four edges have reduced subdivison, and the center has only a single large quad:
Because we cleverly wrote our grid function to take a variable number of steps along each axis independently, we can reuse it as-is to generate our more efficient face mesh. We’ll need to determine a new “start” position for each section of the mesh, but we can recycle our “up” and “right” vectors:
for (const face of faces) {
const { up, right, start } = face;
const positions: vec3[] = [];
const width = vec3.length(vec3.multiply(vec3.create(), right, size));
const height = vec3.length(vec3.multiply(vec3.create(), up, size));
// Calculate a start position for each section.
const s0 = vec3.multiply(vec3.create(), start, size);
const s1 = vec3.scaleAndAdd(vec3.create(), s0, right, radius);
const s2 = vec3.scaleAndAdd(vec3.create(), s0, right, width - radius);
const s3 = vec3.scaleAndAdd(vec3.create(), s0, up, radius);
const s4 = vec3.scaleAndAdd(vec3.create(), s3, right, radius);
const s5 = vec3.scaleAndAdd(vec3.create(), s3, right, width - radius);
const s6 = vec3.scaleAndAdd(vec3.create(), s0, up, height - radius);
const s7 = vec3.scaleAndAdd(vec3.create(), s6, right, radius);
const s8 = vec3.scaleAndAdd(vec3.create(), s6, right, width - radius);
// Generate a grid for each corner.
positions.push(...grid(s0, right, up, radius, radius, resolution, resolution));
positions.push(...grid(s2, right, up, radius, radius, resolution, resolution));
positions.push(...grid(s6, right, up, radius, radius, resolution, resolution));
positions.push(...grid(s8, right, up, radius, radius, resolution, resolution));
// Then for the left and right edges.
positions.push(...grid(s3, right, up, radius, height - 2 * radius, resolution, 1));
positions.push(...grid(s5, right, up, radius, height - 2 * radius, resolution, 1));
// Then the top and bottom edges.
positions.push(...grid(s1, right, up, width - 2 * radius, radius, 1, resolution));
positions.push(...grid(s7, right, up, width - 2 * radius, radius, 1, resolution));
// And finally the middle face.
positions.push(...grid(s4, right, up, width - 2 * radius, height - 2 * radius, 1, 1));
// Move each vertex to its rounded position.
for (const position of positions) {
const rounded = roundedBoxPoint(position, size, 0.25);
vec3.copy(position, rounded.position);
}
faceGeometries.push({ positions });
}
Normals
Finally, we’ll add some normals to our mesh. Since we get those for free when we calculate our rounded box positions, we can use them out of the box!
// Create an array to store our normals.
const normals: vec3[] = [];
for (const position of positions) {
const rounded = roundedBoxPoint(position, size, 0.25);
vec3.copy(position, rounded.position);
// Store the normal.
normals.push(rounded.normal);
}
faceGeometries.push({ positions, normals });
Final notes
- If you’re in javascript/typescript land and just want a solution, I put all of this into a library. It’s free to use in every way - enjoy!
- You can find the source for all the examples in this post in this github repo.
Recommend
-
64
As I don’t use ORMs anymore, I end up writing a lot of fairly complicated 100+ line SQL queries. I found a few ways to keep them readable. 1. Use lowercase In most languages newer than COBOL, most of the code...
-
95
有很多朋友有的因为兴趣,有的因为生计而走向了.Net中,有很多朋友想学,但是又不知道怎么学,学什么,怎么系统的学,为此我以我微薄之力总结归纳写了一篇.Net web开发技术栈,以此帮助那些想学,却不知从何起的朋友。 本文整理了当前企业web开发中的管理系统...
-
102
hackademix.net Giorgio Maone on NoScript, the Universe, and Everything ...
-
115
PHP on .NET Standard 2.0 For those of you who are hearing about Peachpie for the first time, the title of this article may seem even crazier than it does for the others. We are happy to announce that o...
-
82
Support Net Neutrality Comcast, Verizon and AT&T want to end net neutrality so they can control what we see & do online. Firs...
-
76
3年前,微软宣布开源.NET框架的大部分内容。正如ScottHanselman在Connect2016主题演讲中所说的那样,微软一直在做重大贡献:开源.NET框架并不总是一帆风顺的,可以肯定的说,总是会遇到一些困难。在过去的三年中,发生了一些值得注意的事...
-
38
.NET开源三周年 mattwarren...
-
55
-
47
README.md Vixel A WebGL path tracing voxel renderer built with regl.
-
8
CandyGraph A flexible and fast-by-default 2D plotting library tuned for rendering huge datasets on the GPU at interactive speeds. Adopts D3's elegant concept of scales, but implements them on the GPU to maxim...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK