Repository navigation
Geometry
core/buffer_geometry.mojo, core/buffer_attribute.mojo, core/interleaved_buffer.mojo, core/geometry_store.mojo and geometries/. A BufferGeometry holds named vertex attributes and an optional index. It can compute its own normals and bounds.
Builders make boxes, spheres, planes, circles, rings, cylinders, cones, tori, torus knots, the four regular polyhedra, capsules, lathes and tubes. Two more fill a drawn shape in and give it thickness. One sets text in a font and gives it thickness. Four addon builders make parametric surfaces, convex hulls, decals and rounded boxes. Two more read a surface back as the lines of its edges, and utilities merge, weld and compute tangents.

three.js: BufferGeometry, BufferAttribute, computeVertexNormals, computeBoundingBox, computeBoundingSphere, BoxGeometry, SphereGeometry, PlaneGeometry, CircleGeometry, RingGeometry, CylinderGeometry, ConeGeometry, TorusGeometry, TorusKnotGeometry, PolyhedronGeometry, TetrahedronGeometry, OctahedronGeometry, IcosahedronGeometry, DodecahedronGeometry, CapsuleGeometry, LatheGeometry, TubeGeometry. Also ShapeGeometry, ExtrudeGeometry and ShapeUtils.triangulateShape. From the addons: TextGeometry, ParametricGeometry, ParametricFunctions, ConvexGeometry, ConvexHull, DecalGeometry and RoundedBoxGeometry. Also toNonIndexed, center, computeTangents, addGroup, clone, applyMatrix4, applyQuaternion, rotateX, rotateY, rotateZ, translate, scale, lookAt, setFromPoints, deleteAttribute, normalizeNormals, setDrawRange, name, userData, and BufferGeometryUtils.mergeGeometries, mergeVertices and toCreasedNormals.
A flat List[Float32] with an item size. BufferAttribute(data, 3) holds vectors of three floats. BufferAttribute(stored, 3, UINT8_COMPONENT, True) holds normalized bytes. See Integer attributes.
| Member | Meaning |
|---|---|
count() -> Int |
The number of items. |
component(index, offset) -> Float32 |
One number of one item, as a read gives it. |
vector3(index) -> Vector3 |
One item as a vector. Item size must be three. |
set_component(index, offset, value) |
Replace one number of one item. |
apply_matrix3(matrix) |
Move 2D points, or multiply vectors of three, three.js's applyMatrix3. |
apply_matrix4(matrix) |
Move points, with the divide by w. |
apply_normal_matrix(matrix) |
Turn normals and make them unit length. |
transform_direction(matrix) |
Turn directions and make them unit length. A fourth number stays. |
set(values, offset), set_stored(values, offset)
|
Write numbers into the array as it stores them, three.js's set. |
copy_at(index, source, source_index) |
Copy one item of another attribute as its array stores it. |
component_type() -> ComponentType |
The typed array the numbers are stored in. |
is_normalized() -> Bool, set_normalized(flag)
|
three.js's normalized. |
stored_values() -> List[Int] |
The integers an integer attribute stores. |
gather(index) -> BufferAttribute |
One copy of an item for each entry of index. |
packed() -> List[Float32] |
Every float, item after item, with no stride. |
clone() -> BufferAttribute |
A copy with an array of its own. |
is_interleaved() -> Bool |
Whether it reads a shared buffer. See Interleaved buffers. |
is_instanced() -> Bool |
Whether it advances per instance. See Instanced buffer geometry. |
| Member | Meaning |
|---|---|
set_attribute(name, attribute) |
Add or replace an attribute. |
attribute_view(name) -> ref BufferAttribute |
Borrow an attribute. Bind it with ref. |
clone_attribute(name) -> BufferAttribute |
Copy an attribute. |
has_attribute(name) -> Bool |
Whether the attribute exists. |
set_index(index) |
Set the triangle index. An empty list clears it. |
vertex_count() -> Int |
The number of vertices in position. |
triangle_count() -> Int |
The number of triangles. |
corner(triangle, corner) -> Vector3 |
A corner position. |
corner_index(triangle, corner) -> Int |
Which vertex that corner is. |
compute_vertex_normals() |
Set normal from the triangles. |
bounding_box() -> Box3 |
The box around the vertices. |
bounding_sphere() -> Sphere |
A sphere around the vertices, centered on that box. |
clone() -> BufferGeometry |
A copy that shares nothing. |
to_non_indexed() -> BufferGeometry |
A copy in which every triangle owns its corners. |
center() |
Move the vertices so that their box is centered on the origin. |
compute_tangents() |
Set tangent from positions, normals and texture coordinates. |
add_group(start, count, material_index) |
Add a run of triangles that wears one material. |
clear_groups() |
Remove every group. |
stream_length() -> Int |
The index entries, or the vertices without an index. |
vertex_at(slot) -> Int |
Which vertex one slot of that stream reads. |
set_instance_count(count) |
Set how many instances to draw. |
drawn_instances() -> Int |
How many instances a mesh draws. |
delete_attribute(name) |
Remove an attribute. An unknown name is ignored. |
apply_matrix4(matrix), apply_quaternion(rotation)
|
Transform the geometry. See Transforms. |
rotate_x(angle), rotate_y(angle), rotate_z(angle)
|
Turn the geometry about one axis. |
translate(x, y, z), scale(x, y, z)
|
Move the geometry by lengths, or scale it by factors. |
look_at(target) |
Turn the geometry so that its +z axis points at target. |
set_from_points(points) |
Set position from a list of Vector3 or Vector2. |
normalize_normals() |
Make every normal unit length. |
set_draw_range(start, count) |
Draw only part of the triangle stream. See Draw range. |
name, user_data
|
three.js's name and userData. Scene JSON carries both. |
Attribute names are the constants POSITION, NORMAL, UV, UV1, COLOR and TANGENT. A geometry needs position. It needs normal for smooth shading and uv for a texture. It needs color, three or four linear floats per vertex, for a material with vertex_colors. See Materials.
A geometry can carry uv1, a second set of coordinates, for a baked map. See Light map.
compute_vertex_normals averages the normals of the triangles a vertex is in, weighted by their areas. A shared vertex shades smoothly. A vertex used once shades flat. A vertex no triangle uses keeps a zero normal, as in three.js.
Faces are joined by index, not by position. The two vertices on either side of a texture seam keep separate normals, so a recomputed seamed sphere shows its seam, as in three.js. A builder's own normals are better. Replace them only on purpose.
The bounds are computed each time they are asked for. Nothing is cached. See Math for Box3 and Sphere.
geometry.rotate_x(Angle(90.0, DEGREE))
geometry.translate(Length(0, METER), Length(1, METER), Length(0, METER))
geometry.scale(2, 2, 2)A transform changes the vertex data itself, once. Use a node's transform for a shape that moves each frame.
apply_matrix4 moves position as points. It turns normal by the normal matrix and makes each normal unit length. It turns tangent as a direction, and the handedness in w stays. The other transforms build a matrix and call apply_matrix4, as three.js does.
three.js recomputes a bounding box or sphere that it has cached. Here the bounds are computed each time they are asked for, so they are always current.
- The morph targets also move. A target of finished positions moves with the positions, and a target of offsets turns without the translation. three.js leaves both as they are. This keeps a worn target where it was relative to the shape, as
centerdoes. - A matrix that flattens a dimension raises when there are normals to turn. three.js gives zero normals, which light every surface black.
-
apply_matrix4on an attribute of fewer than three numbers an item raises. three.js reads the next item's numbers. -
set_from_pointson a geometry that has positions writes as many items as fit, as three.js does. It does not warn about the rest.
Range readers clamp the start and count before adding them. Large nonnegative counts stay inside the triangle or vertex stream.
geometry.set_draw_range(6, 12) # index entries 6 to 17: triangles 2 to 5
geometry.set_draw_range(0) # every slot again: three.js's InfinityThe draw range is the part of the triangle stream that is drawn and picked. It counts index entries for an indexed geometry and vertices for one without an index, as a group does.
drawn_run(start, count) intersects a group with the range, as three.js's renderBufferDirect does. Then it clamps the run to the stream and to whole triangles. The CPU and GPU rasterizers, the wireframe and the raycaster all read a run through it. So they draw and pick the same triangles.
A line or a points object reads drawn_vertices(). A line joins only the points in the range. A loop closes on the first of them, and a list of sticks leaves out a last point without a partner. The whole line must still suit its mode.
three.js: drawRange, setDrawRange.
- A negative start or count raises. three.js draws nothing for a range that ends before it starts.
- A wide line and an instanced mesh ignore the draw range. three.js draws a wide line as instances, and a draw range does not cut instances.
- The glTF and OBJ exporters write the whole geometry, as three.js's exporters do.
var colors = BufferAttribute(bytes, 3, UINT8_COMPONENT, True) # Uint8Array, normalized
colors.set_component(0, 0, 0.5) # stores 128An attribute can store its numbers in any of three.js's seven typed arrays: Float32, Uint32, Uint16, Uint8, Int32, Int16 and Int8. A normalized integer stands for a number from 0 to 1, or from -1 to 1 for a signed type.
A read divides by the type's largest value, three.js's denormalize. A write multiplies and rounds, three.js's normalize. Then the integer is cut and wrapped into the type, as a JavaScript typed array does. A Uint8Array stores 1.2 as 50. typed_value gives that integer.
set, set_stored and copy_at write the numbers as the array stores them. They do not normalize, as in three.js.
three.js: normalized, MathUtils.normalize, MathUtils.denormalize, Int8BufferAttribute and its kin, applyMatrix3, applyMatrix4, applyNormalMatrix, transformDirection, set, copyAt.
data stays a List[Float32] for every attribute. It holds what a read returns. An integer attribute also keeps the integers it stores, and every write goes through them first.
The reason is speed. About 150 call sites in 30 modules read an attribute through component, vector3 or packed. They include the rasterizers, the raycaster, the morph and skin code, the utilities and the exporters. With integer storage, each of these reads needs a branch on the type and a divide. The float view keeps every one of them as fast as before. An integer attribute pays for its integers only on a write.
The cost is memory. An integer attribute holds its numbers twice.
- The glTF loader reads each vertex attribute in its accessor's component type, with its
normalizedflag. A quantized attribute ofKHR_mesh_quantizationkeeps its integers. - The glTF exporter writes an integer attribute in its own component type, with
normalized. It requiresKHR_mesh_quantizationfor the types that extension names, as three.js does. A 32-bit integer attribute is written as floats, as three.js converts one. -
Scene JSON writes and reads the typed array's name, its integers and
normalized.
- An interleaved attribute is always
Float32. - A
Uint32orInt32attribute reads through aFloat32, so a value past 2^24 is rounded. three.js reads it as aFloat64. - A write takes a
Float32. A number at a rounding half can round the other way from three.js'sFloat64. - The glTF loader reads
WEIGHTS_0as floats, because it normalizes the weights. glTF morph targets are read as floats. - The glTF exporter writes
JOINTS_0as unsigned shorts whatever its type. -
GeometryCompressionUtilsis not ported. Its encodings need a shader that decodes them, and this renderer has none.
assets.geometries.add(geometry) stores a geometry and returns a GeometryId. get(id) borrows it. Many meshes share one geometry. See Meshes and assets.
var solid = cube(Length(1.0, METER))
var brick = box(Length(2.0, METER), Length(1.0, METER), Length(0.5, METER))
var grid = box(Length(2.0, METER), Length(1.0, METER), Length(0.5, METER), 4, 2, 1)The box is three.js's BoxGeometry, built by its buildPlane. The three counts after the sizes are width_segments, height_segments and depth_segments, one each by default. Each face is a grid of segments, with its own normal and texture coordinates, and the whole image covers each face once.
The faces come in three.js's order: +x, -x, +y, -y, +z, -z. Each face is a group, with material indices 0 to 5 in that order. The vertices, the triangles and the texture coordinates are three.js's, in three.js's order. So a hit's triangle is three.js's faceIndex, and an exported box matches three.js's.
With one segment a side, a face is two triangles, and the face of triangle t is t // 2. A count below one is refused. three.js rounds a count down and takes what it gets.
var ball = sphere(Length(1.0, METER), 24, 16) # segments around, rings down
var band = sphere(
Length(1.0, METER), 24, 16,
Angle(0.0, RADIAN), Angle(pi, RADIAN), # phi: half the way around
Angle(0.5, RADIAN), Angle(1.0, RADIAN), # theta: a band below the pole
)A latitude and longitude sphere, three.js's SphereGeometry. Each normal points away from the center, so shading is smooth. u runs once around the part drawn. v runs from one at the top of the part to zero at its bottom. It needs at least three segments and two rings.
phi_start and phi_length say where the surface starts around the y axis and how far it runs, a whole turn by default. theta_start and theta_length say where it starts down from the north pole and how far it runs, pole to pole by default. As in three.js, a part that reaches a pole ends in a point there. A part that stops short of a pole has a full ring of triangles at its edge. An angle that is not finite is refused.
var sheet = plane(Length(2.0, METER), Length(1.0, METER)) # one quad
var grid = plane(Length(20.0, METER), Length(20.0, METER), 4, 4) # sixteen quadsA rectangle in the xy plane, facing +z, centered on the origin. The vertex order, winding and texture coordinates match three.js. Turn the node a quarter turn about x to make a floor.
The texture coordinates run once across the rectangle. To tile an image, rewrite the uv attribute, as examples/floor.mojo does.
var disk = circle(Length(1.0, METER), 32) # a full disk
var slice = circle(Length(1.0, METER), 8, Angle(90.0, DEGREE), Angle(180.0, DEGREE)) # a pie sliceA disk in the xy plane, facing +z, centered on the origin. Vertex zero is the center. The rim follows it, one vertex per segment and one more to close the seam. Each triangle runs from a rim vertex to the next one and back to the center.
The third argument is where the rim starts, counter-clockwise from +x. The fourth is how far it sweeps. A full turn is the default. A shorter sweep makes a pie slice.
The texture coordinates map the square around the disk onto the image. The center is (0.5, 0.5). A pie slice shows its part of the image.
var washer = ring(Length(0.5, METER), Length(1.0, METER), 32, 1) # segments around, then across
var arc = ring(Length(0.5, METER), Length(1.0, METER), 16, 1, Angle(0.0, DEGREE), Angle(90.0, DEGREE))A flat ring in the xy plane, facing +z, centered on the origin. The first radius is the hole. The second is the outer edge. Vertices run in rows from the inner edge outwards, one per segment around and one more for the seam. The start angle and the sweep work as they do for the circle. A shorter sweep makes an arc.
The texture coordinates map the square around the outer edge onto the image, as the circle's do.
An inner radius of zero makes a disk, as in three.js. The inner row is then every vertex at the center.
var can = cylinder(Length(1.0, METER), Length(1.0, METER), Length(2.0, METER), 32, 1) # top radius, bottom radius, height
var bucket = cylinder(Length(0.8, METER), Length(1.2, METER), Length(2.0, METER))
var pipe = cylinder(Length(1.0, METER), Length(1.0, METER), Length(2.0, METER), 32, 1, open_ended=True)A cylinder stands on the y axis, centered on the origin. The first radius is at the top and the second at the bottom. Different radii make a frustum. The side comes first, in rows from the top down, one vertex per segment around and one more for the seam. A cap follows at each end unless open_ended is true. Each cap has one center vertex per segment, as in three.js.
The side's triangles come column by column, in three.js's order. The caps are the only closing faces. A partial sweep leaves its two cut faces open, as in three.js.
Side normals lean with the side, so a frustum shades smoothly. Cap normals point along the axis. u runs once around the side. v runs from one at the top to zero at the bottom. Each cap maps the square around it onto the image.
The sweep starts at +z and runs toward +x, as in three.js. The circle starts at +x instead. A shorter sweep makes a section with open sides.
var spike = cone(Length(1.0, METER), Length(2.0, METER), 32)A cone is a cylinder with a top radius of zero. Its point is at +y. The point has one vertex per segment, each with its own normal. A cylinder with a bottom radius of zero is a cone the other way up. An end with no radius gets no cap.
var ring = torus(Length(2.0, METER), Length(0.5, METER), 12, 48) # radius, tube, around the tube, along it
var bend = torus(Length(2.0, METER), Length(0.5, METER), 12, 48, Angle(90.0, DEGREE))A tube bent around a circle in the xy plane, centered on the origin. The first radius runs from the center to the middle of the tube. The second is the tube's own. Vertices run in rows, one row per step around the tube, one vertex per step along it and one more for the seam. Each normal points away from the middle of the tube, so the shading is smooth. u runs along the tube and v around it.
The arc runs from +x toward +y. A full turn is the default. A shorter arc makes a bent pipe with open ends.
var trefoil = torus_knot(Length(2.0, METER), Length(0.4, METER), 64, 8, 2, 3) # radius, tube, along, around, p, qA tube bent around a knot that winds p times around the axis and q times through the hole. The curve is three.js's. It lies between half and one and a half of the radius from the axis. Vertices run in rings along the curve, one vertex per step around the tube and one more for the seam. Each ring is built in three.js's frame, so a texture lands as it does there.
var die = tetrahedron(Length(1.0, METER))
var gem = octahedron(Length(1.0, METER))
var ball = icosahedron(Length(1.0, METER), 3) # detail three: a geodesic sphere
var dome = dodecahedron(Length(1.0, METER))
var own = polyhedron(vertices, indices, Length(1.0, METER), detail)A polyhedron is a list of vertices and a list of triangles over them. Every vertex is pushed out to the radius. detail cuts each edge that many times, so each face becomes (detail + 1) squared triangles, on the way to a sphere. The four regular solids have three.js's vertices and faces in three.js's order.
The geometry is not indexed. Each triangle owns its three vertices. At a detail of zero the normals are flat, one per face. At a detail of one or more each normal points away from the center.
Texture coordinates are longitude and latitude, with the seam repaired per face as three.js repairs it. v is one at the top pole and zero at the bottom, as on the sphere. A coordinate can exceed one on a face that straddles the seam.
var pill = capsule(Length(0.5, METER), Length(2.0, METER), 4, 16) # radius, length, cap rows, aroundA cylinder with a hemisphere on each end, standing on the y axis and centered on the origin. The length is the straight side between the caps, three.js's height. The builder is three.js's CapsuleGeometry, number for number.
Vertices run in rows from the bottom pole to the top, one vertex per step around and one more for the seam. The sweep starts at -x. A capsule of no length keeps both rims and a side of no height between them.
u runs around, and a pole's vertices sit half a step round, as in three.js. v runs up the profile by distance along it, from zero at the bottom pole to one at the top. A texture stays put when the segment counts change.
The normals come from the profile exactly. On a cap they run along its radius, and on the side straight out. The half of each cell against a pole has no area. three.js keeps it, and so does this.
var vase = lathe(points, 24) # a profile of Vector2, cells around
var half = lathe(points, 24, Angle(0.0, DEGREE), Angle(180.0, DEGREE))A profile revolved around the y axis. Each point has x out from the axis and y along it, in meters. Vertices run in columns, one per step around, one vertex per point.
The normals come from the profile's segments, as in three.js. A corner faces the sum of its two segments' normals, each as long as its segment, made unit length. A longer segment pulls the corner its way. The first point faces the way its segment does.
The last point faces the way its own segment does, and its normal is as long as that segment. three.js does not make it unit length, and this keeps that. The builder refuses a final segment normal whose components cannot fit in Float32.
Cylinder and lathe unit normals use a scaled calculation when a direct norm overflows or underflows. Lathe corner normals use the outer endpoint difference when rounded segment differences lose the direction. Ordinary profiles keep their existing arithmetic and order.
A point on the axis is a pole. The half of each cell against it has no area, and three.js keeps it, as this does. u runs around and v up the profile, one point per equal step. The sweep starts at +z, as the cylinder's does.
var pipe = tube(path, Length(0.2, METER), 8) # a path of Vector3, cells around
var loop = tube(path, Length(0.2, METER), 8, closed=True)
var bent = tube(curve, Length(0.2, METER), 64, 8) # a Curve3, cells along, cells aroundA tube of one radius swept along a path of points. Each point gets a ring, built in a frame that follows the path. The frames are three.js's parallel transport, so the tube does not twist where the path only bends. A closed path is given without repeating its first point. Its last ring repeats its first, and the twist the path built up is spread evenly back along it.
With a list of points, the path is the points. u runs along the path by distance, the closing segment included. v runs around the tube.
With a Curve3, the tube is three.js's TubeGeometry. It samples tubular_segments + 1 rings at equal distances along the curve, in the curve's Frenet frames. A closed tube puts its last ring on its first. See Curves.
var flat = shape_geometry(plate) # twelve runs a curve
var flat = shape_geometry(plate, 32) # a finer sample
var flat = shape_geometry([plate, badge]) # one group per shapeA list of shapes is three.js's ShapeGeometry given an array. The shapes follow one another in one geometry, and each is a group whose material index is its place in the list.
A shape filled in: a flat surface in the plane where z is zero, facing the positive z axis. The outline and its holes are sampled into points, and the points are cut into triangles. The texture coordinate of a vertex is the vertex, which is three.js's default generator.
Every other builder here is a grid. A shape is not, and cutting one up is the whole of geometries/shape.mojo.
By earcut, as three.js cuts it. shape_geometry builds the same arrays as three.js's ShapeGeometry, in the same order.
- The outline turns clockwise and each hole counter-clockwise. A contour that runs the other way is reversed, with its closing point still in the list.
- The closing point of each contour is dropped. A reversed contour therefore starts at its closing point.
- The vertices are the outline's points, then each hole's points.
- earcut cuts them into triangles, and the index keeps earcut's order.
triangulate(shape, curve_segments) returns the points, the triangles and where each contour starts. extract_points returns the sampled contours after the checks below.
Area and winding use a shared calculation with coordinates relative to one contour point and sums in doubles. Moving a small shape far from the origin does not change its winding or make its area disappear.
A hole must lie strictly inside the outline. Its sampled boundary must not cross or touch the outline or another hole. A shared point or a shared edge is refused. A hole inside another hole is refused in either input order. These checks run before triangulation and extrusion.
The checks read every sampled segment, including segments whose corners the area check removes. Inside corners alone do not prove that an edge stays inside a concave outline. Boundary coordinates must be finite. The contact test uses the exact orientation sign of the stored Float32 coordinates, without a distance tolerance. A positive representable gap stays a gap. Curves are checked at the requested sample resolution.
A contour with fewer than three corners, or with no area, is refused too. three.js builds something from each of these and gives no error. The checks change no point, vertex order, index or group. An outline that crosses itself is filled in by earcut, as in three.js.
var solid = extrude(plate, Length(2, METER))
var solid = extrude(plate, Length(2, METER), steps=4)
var solid = extrude(
plate,
Length(2, METER),
bevel_enabled=True,
bevel_thickness=Length(0.2, METER),
bevel_size=Length(0.1, METER),
bevel_segments=3,
)A shape given thickness along the z axis, from zero to depth. The shape is filled in twice, once at each end, and every edge of every contour becomes a wall between them. A hole becomes a shaft through the solid.
The vertices are built in layers, each holding every contour point once at one height. steps cuts the walls up without changing the solid.
A bevel rounds the two edges off. It adds bevel_segments layers at each end. They reach bevel_thickness past the end face, and stand bevel_size out from the outline, by the sine and the cosine of a quarter turn.
three.js measures a bevel out from the shape drawn, and so does this. The two end faces are the outline itself, and the body between them stands proud of it all the way round. bevel_offset moves every layer out before the bevel is measured, the end faces included, which makes a lip rather than rounding an edge.
Each point moves along three.js's getBevelVec, in doubles. The vector is the miter while it is no longer than the square root of two. A longer miter shrinks to that length, so a sharp corner does not become a spike. A point between two edges in a line moves square to them, or back along them.
With a bevel, earcut cuts the contours moved out by bevel_offset, as three.js does. That cut can take another diagonal than the contours themselves.
extrude builds the same arrays as three.js's ExtrudeGeometry, in the same order.
- An outline that runs counter-clockwise turns clockwise. Only then does each clockwise hole turn counter-clockwise. An outline drawn clockwise keeps its holes as they are drawn.
- A point on top of the point before it is merged, the closing point included. This takes out the first point of each contour.
- The back cap's triangles come first, then the front cap's triangles.
- The walls follow, contour by contour, each contour from its last point back to its first.
An extrusion has no index buffer, and its normals come from its triangles, so every vertex belongs to one face. A bevel of three segments is three flat bands. three.js does the same, and for the same reason: two walls that meet at a corner do not agree about the texture coordinate there.
A wall is measured along x or along y, whichever it runs further in, and up the negative of z. That is three.js's own generator, WorldUVGenerator.
def halved(vertices: List[Float64], a: Int, b: Int, c: Int) -> List[Vector2]:
... # three coordinates for a cap triangle
var solid = extrude(plate, Length(2, METER), uv_generator=UVGenerator(halved, walls))uv_generator is three.js's UVGenerator option, on every form of extrude. A UVGenerator holds two functions. top gives a cap triangle's three coordinates, and side_wall gives a wall quad's four. Each gets the vertices written so far, three doubles each, and the indices of its corners among them, as three.js's generateTopUV and generateSideWallUV get them.
The four wall coordinates go to the quad's six vertices in the order a, b, d, b, c, d, as three.js's f4 puts them. A generator that gives another count is refused. UVGenerator() is WorldUVGenerator.
extrude([plate, badge], depth) is three.js's ExtrudeGeometry given an array. The shapes follow one another in one geometry, and each adds its own two groups: its caps wear material 0 and its walls material 1.
The shape and its holes are sampled in Float32, and three.js samples them in doubles. The positions agree to about a millionth of a meter. A wall at exactly 45 degrees is a tie between x and y for its texture coordinate. The rounding can then choose the other axis than three.js.
var rail = extrude(plate, curve, steps=32) # a Curve3
var rail = extrude(plate, path, steps=32) # a CurvePath3A shape swept along a curve in space. This is the three.js ExtrudeGeometry with an extrudePath.
The layers stand at steps + 1 equal distances along the curve. Each layer is placed in the Frenet frame of the curve at that point. The x of a shape point runs along the normal of the frame, and its y runs along the binormal. The back cap is at the start of the curve, and the front cap faces on along the curve at its end.
This form has no bevel and no depth. three.js turns the bevel off when it gets a path, and the curve gives the length. The texture coordinates come from the same world generator, read from the swept vertices.
A wall between two frames that turn is not flat. So the diagonal that splits a quad changes the surface. Both forms split each quad across its second and fourth corners, as the three.js f4 does.
The tests compare every triangle with the output of three.js 0.180 for a Bezier curve and for a path of two lines.
from geometries.text import text_geometry
from loaders.font import read_font
var font = read_font("assets/fonts/fixture.typeface.json")
var sign = text_geometry("AO i8", font, Length(1, METER), Length(0.2, METER))
var bold = text_geometry(
"A\nD",
font,
Length(1, METER),
Length(0.2, METER),
bevel_enabled=True,
bevel_thickness=Length(0.02, METER),
bevel_size=Length(0.01, METER),
)text_geometry sets text in a font and gives it thickness. This is the three.js TextGeometry.
The font lays the text out as shapes, with Font.generate_shapes. extrude gives each shape its depth, with the same options as Extrude. The parts are joined end to end, one shape after another, which is the order three.js writes them in.
size is one em, and depth runs along z from zero. Both are a Length. The defaults are from three.js: a size of 100 meters and a depth of 50 meters. A bevel is 10 meters thick and 8 meters out. Set size to the height you want.
A text with nothing to draw, such as spaces, gives a geometry with empty position, normal and uv attributes. The options are checked all the same.
three.js adds two groups to each shape, one for the caps and one for the walls. extrude writes no groups, so a text has none.
The tests compare the vertex count and the bounding box with three.js 0.180, with and without a bevel.
from geometries.parametric import SurfaceFunction, klein, parametric
var bottle = parametric(SurfaceFunction[klein]()) # 8 slices, 8 stacks
var sheet = parametric(SurfaceFunction[my_function](), 24, 12)
var dome = parametric(Dome(Float32(2)), 24, 12) # a struct that holds a radiusA grid sampled from a function of u and v, each from zero to one. The function returns a point in meters. There are slices cells along u and stacks cells along v, and each cell is two triangles. The texture coordinate of a vertex is its (u, v).
The surface is a type, not a closure. Any struct that implements ParametricSurface is a surface: it has point(self, u, v) -> Vector3. SurfaceFunction[f] makes a surface from a plain function def f(u: Float32, v: Float32) -> Vector3. A surface that needs its own numbers is a struct that holds them.
The normals are three.js's finite differences. A vertex asks for a second point EPS away along u, and a third along v. The normal is the cross product of the two differences, made unit length. At the first row and column the step goes forward, so the function never gets a negative number.
The arithmetic is Float32, and three.js uses Float64. A step of 1e-5 in Float32 loses digits, so a normal can differ from three.js by a few parts in a thousand. The positions agree.
klein, mobius, mobius3d and parametric_plane are three.js's ParametricFunctions. klein swaps the names of its arguments, as three.js does.
from geometries.convex import convex
from math.convex_hull import ConvexHull
var pebble = convex(points) # a List[Vector3]
var hull = ConvexHull(points)
var inside = hull.contains_point(Vector3(0, 0, 0))convex returns the smallest convex solid that holds every point. Points inside it are left out. Each face is a triangle with three vertices of its own, and all three carry the face's normal. So the hull shades flat. There is no index and no texture coordinate, as in three.js.
ConvexHull is three.js's quickhull, step by step. It starts from a tetrahedron of four extreme points. It adds the farthest point that a face can see, again and again, until no face can see a point.
| Member | Meaning |
|---|---|
face_count() -> Int |
The number of triangles. |
face_vertex(face, corner) -> Int |
Which input point is at one corner. |
face_normal(face) -> Vector3 |
The outward unit normal of one face. |
contains_point(point) -> Bool |
Whether no face can see the point. |
tolerance |
Containment padding and the initial simplex height threshold, in original units. |
The hull stores original Float64 coordinates. Both the constructor and contains_point accept a List[SIMD[DType.float64, 4]] and a SIMD[DType.float64, 4], respectively. These forms use the first three components and ignore the fourth. They do not narrow coordinates to Float32. The Vector3 forms are also available. A query with a nonfinite spatial component returns False.
The tolerance is three.js's: three times the Float64 epsilon, times the sum of the greatest absolute coordinate on each axis. It uses the original coordinate units. You can change tolerance for later containment queries. A change does not rebuild the hull.
A negative or nonfinite tolerance makes containment return False. At subnormal scales, the stored tolerance can round to zero. Zero stays the comparison threshold. There is no hidden positive threshold.
Adaptive predicates decide exact orientation and affine differences. They compare the exact signed distance to the stored tolerance. Equality is inside. Ordinary calculations first use conservative arithmetic error bounds. Uncertain cases use outward-rounded intervals, then fixed-capacity signed dyadic integers. An error bound only certifies a comparison; it never replaces the public tolerance.
The exact comparison squares the determinant and tolerance-scaled normal length. It does not round a tiny distance to zero or normalize a plane before comparison.
The predicates read the original coordinates, including subnormal components. A common power-of-two scale can still improve ranking arithmetic. It cannot remove detail from a predicate. A slanted tetrahedron made from representable subnormal coordinates is accepted when it has exact nonzero volume and zero tolerance. Exactly collinear and coplanar input gives a degeneracy error. Noncoplanar input that does not exceed the positive construction tolerance gives a separate tolerance error.
After the initial dimension check, construction uses strict exact visibility. Assignment, horizon deletion and point reassignment use zero as the threshold. Thus every emitted plane supports all source points. Tolerance-based horizon deletion can retain a geometrically visible neighbor and fold the new face fan. Near-plane source points are retained when they are exact hull vertices, even when their distance is below tolerance. This corrects three.js r180 and can add faces for nearly coplanar input.
Ordinary-range distance ranks retain the three.js r180 calculation order. A rank is a search heuristic among points or faces that passed the exact geometric test. It cannot admit a point or change a visibility decision. Extreme or unresolved ranks use exact squared segment distances or exact plane-distance comparisons. Original vertex indices stay unchanged. Coplanar faces are not merged, as in three.js.
face_normal returns a normalized estimate of the cross product as a Vector3. A certified ordinary normal has direction error below 128 times 2^-53 before Float32 rounding. Difficult directions use an exact cross-product estimate. This estimate is for display and mesh normals. Containment does not use the rounded normal.
Each exact scalar uses 672 32-bit words and three integer fields. The fixed capacity is 21,504 bits. Finite binary64 coordinates are multiples of 2^-1074 and have magnitude below 2^1024. Affine differences have magnitude below 2^1025. The largest predicate compares two squared plane distances. Its degree is ten, and fewer than 21,010 bits suffice for its full exponent span and carries.
Addition visits the aligned active words. Multiplication visits at most 672 by 672 word pairs. Predicate storage and work do not grow with the point count.
Quickhull keeps its existing point, face and half-edge lists. The original coordinates add 32 bytes per point before list capacity. Separate ordinary ranking normals add 24 bytes per created face before list capacity. There is no exhaustive supporting-plane construction fallback. Overall hull work still depends on the input and the faces that Quickhull creates.
tools/generate_exact_predicates_oracle.mojo generates rational-arithmetic controls for the shared predicates. tools/reference_convex_hull.mojo separately enumerates exact supporting facets for small test clouds. The tests check positive-tolerance boundaries, original indices, winding, paired edges, source support, mutable zero tolerance and the retained three.js face-order fixture. bench/convex_hull_bench.mojo measures the same ordinary workload against a baseline and exercises accepted subnormal and mixed-range clouds. The following measurements are machine-specific.
The Linux x86_64 measurements used Mojo 1.1.0 on one logical CPU. Five native candidate repetitions used the same source as ten baseline repetitions. Baseline repetitions ran before and after the candidate. Compilation and input setup are excluded. Ordinary checksums matched.
| Workload | Calls per repetition | Baseline e53600eb
|
Exact hull | Ratio |
|---|---|---|---|---|
| Build a 300-point ordinary cloud | 100 | 0.107 ms | 0.256 ms | 2.39 |
| Ordinary containment | 10,000 | 0.136 microseconds | 0.784 microseconds | 5.76 |
| Build a slanted subnormal tetrahedron | 100 | Not timed | 0.0385 ms | Not compared |
| Build a mixed-exponent tetrahedron | 100 | Not timed | 0.654 ms | Not compared |
The added correctness has measurable ordinary-range overhead. Exact predicate scalars use 2,712 bytes of fixed storage on this host. A self-tested runtime allocation hook measured zero requested heap allocations in all 45 predicate benchmark rows. This excludes stack memory, allocator metadata and process RSS. bench/exact_predicates_bench.mojo includes the ordinary filters and full-span exact fallbacks.
geometries.convex stores positions and normals as Float32. It rounds input positions first, then constructs their exact supporting hull. Face normals come from these stored positions. This prevents a tiny Float64 face from becoming a collapsed triangle after triangulation. It raises if a coordinate cannot be stored as a finite Float32, or if the rounded cloud no longer spans a usable solid.
The mesh is the hull of rounded positions, not the original Float64 cloud. Rounding can change its topology. This corrects three.js r180 near storage limits and preserves the ordinary geometry and object-breaker fixtures. Use ConvexHull and original Float64 coordinates when the wider coordinate domain is required.
three.js returns an empty hull for fewer than four points. It returns a flat or broken hull for points on one line or one plane. This port raises in all three cases. setFromObject and intersectRay are not ported.
from geometries.decal import decal
var sticker = decal(
head_geometry,
scene.world_matrix(head_node),
Vector3(0.1, 1.6, 0.2), # the center of the box
Euler(Angle(0, DEGREE), Angle(30, DEGREE), Angle(0, DEGREE), XYZ),
Length(0.2, METER), # width, along the box's x
Length(0.2, METER), # height, along its y
Length(0.3, METER), # depth, along its z
)The part of a mesh inside a box, with texture coordinates from the box. The box is the projector. It stands at a position with an orientation, and it projects the image along its own z axis. u runs across its width and v across its height.
Each triangle of the mesh moves into the frame of the box. Then it is cut against the six faces of the box, in three.js's order. A triangle with one corner outside a face becomes two triangles. A triangle with two corners outside becomes one. A triangle with three corners outside is dropped.
A new corner gets the normal that is the same blend of the two ends' normals. three.js does not make that normal unit length again, and this port does not.
The result is in world space, as in three.js. Put it on a mesh with no transform. Draw it with a polygon offset, or it fights the surface under it for depth.
The mesh's normals are carried through its normal matrix when it has them. The result has no normal attribute when the mesh has none, or when nothing is inside the box. three.js takes a Mesh. This port takes the geometry and the world matrix, because a mesh here holds ids and the scene holds its matrix.
from geometries.rounded_box import rounded_box
var soft = rounded_box(Length(2, METER), Length(1, METER), Length(1, METER)) # 2 segments, 0.1 m radius
var pill = rounded_box(Length(1, METER), Length(1, METER), Length(1, METER), 4, Length(0.5, METER))A box with rounded edges and corners, centered on the origin: three.js's RoundedBoxGeometry addon. segments is the number of cells round each edge. radius is the radius of the edges. A radius larger than half the shortest side is cut down to that half. The extents and radius must be finite.
The builder starts from a unit box with 2 * segments + 1 cells each way on every face. Each vertex gets a normal from the center of that box, pulled half a cell in on each axis. The vertex moves to the corner of a box smaller by the radius, plus the radius along the normal. The middle band of each face stays flat.
The texture coordinates are three.js's. They measure the arc of each rounded edge and the flat band between, so a texture is not squeezed where the surface bends.
The geometry has no index. It has six groups, one for each face, in three.js's order: right, left, top, bottom, front, back. This order is not the order of box.
three.js returns a unit box for zero segments, whatever the size. This port raises.
geometries/edges.mojo reads a surface of triangles and gives back a geometry of points, two per segment. Draw it with a Line in SEGMENTS mode.
from geometries.edges import edges_geometry, wireframe_geometry
from objects.line import Line, SEGMENTS
var outline = assets.geometries.add(edges_geometry(cube(Length(1, METER))))
scene.add_line(Line(outline, ink, node, mode=SEGMENTS))| Builder | What it keeps |
|---|---|
wireframe_geometry(geometry) |
Every edge, once each. |
edges_geometry(geometry, threshold) |
The edges that show the shape. |
triangle_edges(geometry) |
Each edge once, paired by vertex index. |
welded_points(geometry) |
Which welded point each vertex stands on. |
edges_geometry keeps an edge when the two faces meeting at it turn by at least threshold, and when it has one face and no neighbor. A cube keeps its twelve edges and drops the diagonal across each face. threshold is one degree unless said otherwise, as three.js defaults thresholdAngle.
triangle_edges is what a wireframe material draws. It pairs by vertex index rather than by welded position, as three.js pairs the two rules. A wireframe is drawn from the mesh's own vertices, and two indices that stand at one place are still two vertices with their own colors. A cube has thirty edges by index and eighteen by position.
edges_geometry finds creases and boundaries, not silhouettes. It is given no camera, and its answer does not move when one does. What it keeps of a sphere depends on how finely the sphere is divided. Twenty-four segments turn fifteen degrees a facet, so the default threshold keeps most of those edges rather than none.
The comparison is inclusive. An edge turning exactly as far as the threshold is kept. ANGLE_SLACK is what makes that true in practice. An Angle holds radians as a Float32, so an authored 90 degrees has a cosine of −4.37e−8 rather than zero. Two perpendicular faces give a dot product of exactly zero. Without the slack a cube asked for its right angles lost all twelve.
Both weld by position before they pair. Two triangles that meet along an edge often hold two copies of each of its ends. A corner carries a normal and a texture coordinate as well as a position, and the two faces disagree about those. Pairing by vertex index would then find no shared edge anywhere.
WELD is the tolerance: a tenth of a millimeter, in meters. It is a distance and not a fraction, which is the bargain three.js makes with its four-decimal position hash. A geometry built at a scale far from one meter has to be welded at its own scale, and neither library does that for you.
A welded cube has eighteen unique edges, not thirty-six: twelve around the shape and one diagonal in each face.
geometries/utils.mojo holds three.js's BufferGeometryUtils. Three more operations are methods of BufferGeometry, as in three.js.
from geometries.utils import merge_geometries, merge_vertices, to_creased_normals
var parts = List[BufferGeometry]()
parts.append(cube(Length(1, METER)))
parts.append(sphere(Length(1, METER), 24, 16))
var both = merge_geometries(parts, use_groups=True)
var welded = merge_vertices(loose) # tolerance 1e-4
var creased = to_creased_normals(welded, Angle(30.0, DEGREE))
creased.compute_tangents()
creased.center()| Function | What it does |
|---|---|
merge_geometries(geometries, use_groups) |
Joins parts into one geometry. |
merge_vertices(geometry, tolerance) |
Welds vertices that agree on every attribute, and adds an index. |
to_creased_normals(geometry, crease_angle) |
Smooths normals across gentle edges and keeps creases sharp. |
geometry.to_non_indexed() |
Gives every triangle its own three corners. |
geometry.center() |
Centers the bounding box on the origin. |
geometry.compute_tangents() |
Writes a four-number tangent for normal maps. |
merge_geometries joins the attributes end to end, in the first part's order. It moves each part's index entries past the vertices of the parts before it. It joins morph targets target by target.
All parts must be indexed, or none. They must carry the same attributes and morph targets. Matching attributes must have the same item size, component type, normalization flag, and instance divisor. Merging keeps integer storage exact, including UInt32 values above 2^24.
With use_groups, the result gets one group per part, and part i wears material index i. The parts' own groups are not kept, as in three.js.
A group is a run of the triangle stream that wears one material: three.js's addGroup. start and count count index entries, or vertices for a geometry without an index. MaterialIndex is a position in a list of materials. It is a type, so a bare integer cannot stand in for it.
A mesh that wears a list of materials draws each group with its own material. A mesh with one material ignores the groups. See Meshes and assets. merge_geometries writes groups and compute_tangents reads them.
triangle_run(start, count) gives the first slot of a run and the number of whole triangles in it. It stops the run at the end of the stream, as three.js's renderBufferDirect stops a group. A count of minus one runs to the end. The renderer, the raycaster and the wireframe read a group this way.
The built-in geometries write three.js's groups, so a list of materials dresses them as it dresses three.js's:
| Geometry | Groups |
|---|---|
box, cube
|
Six, one for each face. The material index is three.js's: +x is 0, -x is 1, +y is 2, -y is 3, +z is 4 and -z is 5. |
cylinder, cone
|
The side wears 0. The top cap wears 1 and the bottom cap 2, when they are there. |
extrude |
The caps wear 0 and the walls wear 1. |
text_geometry |
Two for each shape: the caps wear 0 and the walls wear 1. |
| STL text | One for each solid. Solid i wears i. |
lathe, capsule, shape_geometry of one shape and the others write no groups, as in three.js.
merge_vertices scales every number of every attribute by one over the tolerance, and truncates it. Two vertices whose numbers all give the same keys are one vertex. Normals and texture coordinates count, so a cube keeps its twenty-four vertices. The same cube with positions only welds into eight.
The rounding is three.js's own. A number is moved by half a step and truncated toward zero, as JavaScript's ~~ does. Two numbers less than a step apart can fall either side of a boundary and stay apart, in three.js and here.
The tolerance is a plain number and not a Length. It applies to every attribute in that attribute's own units. A tolerance of zero is raised to EPSILON, as in three.js. The morph targets move with the kept vertices, but they do not decide what welds.
A key is clamped to KEY_LIMIT, nine times ten to the eighteenth. JavaScript's ~~ wraps at two to the thirty-first instead. The two differ only for a tolerance far below any real one.
The result is indexed. Its index must hold whole triangles, so a geometry of loose points that is not a multiple of three raises.
Welding keeps the draw range and groups because each triangle keeps its place in the stream. It also keeps the name, user data, type, parameters and stored shapes. Mutable data is copied, so changes to the result leave the source alone.
to_creased_normals makes the geometry non-indexed first. Many triangles can meet at the position of a corner. The corner's normal is the sum of the normals of those that turn from its own triangle by less than the crease angle. The crease angle is sixty degrees unless you give another, as in three.js.
Positions are matched by key, not by distance. The key is each coordinate times one hundred, truncated, as in three.js. Points closer than about a centimeter share a key, whatever the scale of the geometry.
The triangles are summed one by one, and a face of two triangles can count once or twice at a corner. So a rounded cube corner leans toward some faces more than others. three.js gives the same result.
three.js changes a geometry without an index in place and returns it. This port returns a new geometry and leaves the one you give alone.
to_non_indexed reads every attribute and every morph target through the index. A vertex that four triangles share becomes four vertices. The groups do not change, because an index entry and a vertex of the result count the same. three.js returns the geometry itself when it has no index. This port returns a copy.
center moves the positions so that the bounding box is centered on the origin. Normals and tangents are directions, and a move does not turn them.
Morph targets that hold finished positions move with the base, so a worn target lands in the same place on the shape. three.js moves the base and leaves those targets behind. Targets that hold offsets do not move, because an offset is the same wherever the shape is.
compute_tangents is three.js's computeTangents, step for step. Each triangle gives the directions in which u and v grow across it. They are added at its three corners. At each vertex the u sum is made square to the normal and unit length.
The fourth number is the handedness. It is minus one where the v sum points against the normal crossed with the u sum, and one otherwise. A mirrored texture needs it.
A triangle whose texture coordinates have no area gives no direction, and is skipped. A vertex that no triangle uses gets four zeros. With groups, only the triangles in the groups are visited, as in three.js.
three.js refuses a geometry without an index. This port reads one three corners at a time, as the renderer does. It is three.js's computeTangents and not MikkTSpace. For the tangents that normal-map bakers use, call compute_mikktspace_tangents. See Geometry addons.
var head = sphere(Length(1.0, METER), 24, 16)
head.add_morph_target(smiling, name="smile") # a BufferAttribute of positions
head.add_morph_target(frowning, name="frown")
var face = Mesh(assets.geometries.add(head.clone()), skin, node)
face.update_morph_targets(head) # names and zero weights
face.set_morph_influence("smile", 0.7) # seven tenths of a smile
face.set_morph_influence(1, 0.2) # a target by its indexA morph target is a second set of positions for the same vertices. The mesh gives each one a weight, and the vertex drawn is the base vertex moved toward the targets by their weights. That is how a face smiles: one geometry, one target per expression, and a number per expression.
three.js: BufferGeometry.morphAttributes, Mesh.morphTargetInfluences, Mesh.morphTargetDictionary, Mesh.updateMorphTargets, morphTargetsRelative.
| Member | Meaning |
|---|---|
geometry.add_morph_target(positions, name="") |
Add a target that moves vertices only. |
geometry.add_morph_target(positions, normals, name="") |
Add one that turns them too. |
geometry.set_morph_colors(colors) |
Give every target a color. three.js's morphAttributes.color. |
geometry.morph_count() -> Int |
How many targets the geometry carries. |
geometry.has_morph_normals() -> Bool |
Whether the targets carry normals. |
geometry.has_morph_colors() -> Bool |
Whether the targets carry colors. |
geometry.morph_target_name(target) -> String |
A target's name, or its index as text. |
geometry.morph_relative |
Whether a target holds destinations or offsets. |
mesh.set_morph_influence(target, weight) |
How much of one target to wear, by index or by name. |
mesh.morph_influence(target) -> Float32 |
What it is wearing. Zero for a target never set. |
mesh.update_morph_targets(geometry) |
One zero weight per target, and the dictionary of names. |
mesh.morph_target_dictionary |
Each target's name and index. |
mesh.is_morphed() -> Bool |
Whether any target is worn at all. |
The targets live on the geometry and the weights live on the mesh. That is what lets two meshes share one head and pull different faces. It is the same split that puts position on the geometry and the transform on the node.
Every target is measured from the unmorphed vertex, so wearing two of them at half each lands half way between both. Measuring each from where the last one left it would land somewhere neither names.
morph_relative is three.js's morphTargetsRelative. False, the default, means a target holds the finished positions and the mesh moves from the base toward them. True means it holds the offsets to add. Exporters write both.
Either every target carries normals or none does. A geometry whose targets carry none keeps the base normal however far the positions move, which is what three.js's shader does when morphAttributes.normal is absent.
set_morph_colors gives every target a color attribute of three or four numbers a vertex. Either every target carries a color or none does. A color target morphs the geometry's color attribute when the material has vertex_colors on. The arithmetic is three.js's morphcolor_vertex, in core.deform.morphed_colors.
A target of three numbers reads an alpha of one, as three.js fills it. The morphed alpha is used only when the color attribute has four numbers a vertex. three.js morphs it only under USE_COLOR_ALPHA, for the same reason.
The colors are morphed in Renderer.prepare, so the CPU and GPU rasterizers draw the same colors. tests/test_gpu.mojo has a parity test with twelve color targets.
A target's name is three.js's name on its morph attribute. A target without a name has its index as its name, as three.js's updateMorphTargets gives it. update_morph_targets fills the mesh's weights and its morph_target_dictionary from the geometry. A mesh names its geometry by id and cannot read it, so the caller gives the geometry. The glTF and scene JSON loaders make the call, and the glTF loader reads extras.targetNames.
A geometry carries any number of targets, and a mesh wears any number. three.js on WebGL2 passes its targets in a texture and has no cap. This port had a cap of eight until a mesh held its weights in a list. MorphInfluences is that list. A weight past its end reads as zero, and set grows it with zeros.
core/deform.mojo answers where a vertex is once its targets are worn, and both the renderer and the raycaster ask it. They did not always. Rendering wore the targets and picking did not, so a morphed mesh was drawn in one place and clicked in another.
A mesh wearing a target is not where its geometry's bounding sphere says it is, so the renderer does not measure it against the frustum. three.js culls it anyway, and clips morphed meshes at the edge of the view for exactly this reason.
var buffer = InterleavedBuffer(numbers^, 8) # 8 floats a vertex
geometry.set_attribute(POSITION, BufferAttribute(buffer, 3, 0))
geometry.set_attribute(NORMAL, BufferAttribute(buffer, 3, 3))
geometry.set_attribute(UV, BufferAttribute(buffer, 2, 6))An interleaved buffer holds the attributes of a vertex side by side in one array. The stride is the number of floats one vertex takes. Each attribute on the buffer gives its item size and its offset in that run. Item i starts at float i * stride + offset.
three.js: InterleavedBuffer, InterleavedBufferAttribute.
| Member | Meaning |
|---|---|
InterleavedBuffer(array, stride) |
A buffer that advances per vertex. |
buffer.count() -> Int |
The number of runs of stride floats. |
buffer.value(at), buffer.set_value(at, value)
|
Read or write one float of the array. |
buffer.shares_with(other) -> Bool |
Whether two handles name one array. |
buffer.clone() -> InterleavedBuffer |
A buffer with a copy of the floats. |
BufferAttribute(buffer, item_size, offset) |
An attribute that reads the buffer. |
attribute.interleaved_buffer() |
A handle on the buffer, three.js's data. |
attribute.offset(), attribute.stride()
|
Where an item starts in its run, and the run length. |
An interleaved attribute is a BufferAttribute, not a second type. count, component, vector3 and set_component use the stride. So every reader works without a change: the renderer, the raycaster, the morph and skin evaluators, the utilities and the exporters.
A copy of an InterleavedBuffer is a second handle on the same floats, as a JavaScript reference is. A write through one attribute shows through every other attribute on the buffer. clone copies the floats.
A copy of an interleaved attribute also shares the buffer. clone_attribute and attribute.clone() give a plain attribute with its own array, as three.js's clone() does. BufferGeometry.clone copies each buffer once, so attributes that shared a buffer share the copy.
data is empty on an interleaved attribute. Read packed() to get the floats in item order. to_non_indexed, gather and the merge utilities give plain attributes.
The JSON loader reads interleavedBuffers and arrayBuffers as three.js writes them. Attributes that name one buffer share it. The glTF loader reads a strided buffer view into plain attributes.
The exporters write an interleaved attribute as its own floats. three.js does the same when it writes one attribute alone. The file then has no shared buffer, but the values are the same.
var grass = BufferGeometry(instanced=True) # InstancedBufferGeometry
grass.set_attribute(POSITION, blade_positions^)
grass.set_attribute("offset", BufferAttribute(offsets^, 3, mesh_per_attribute=1))
grass.set_attribute(COLOR, BufferAttribute(colors^, 3, mesh_per_attribute=1))
scene.add_mesh(Mesh(assets.geometries.add(grass^), paint, node))An instanced geometry is drawn once for each instance. A per-instance attribute advances once for each instance, not once for each vertex. mesh_per_attribute is the number of instances in a row that read one item.
three.js: InstancedBufferGeometry, instanceCount, InstancedBufferAttribute, InstancedInterleavedBuffer, meshPerAttribute.
| Member | Meaning |
|---|---|
BufferGeometry(instanced=True) |
An instanced geometry. |
geometry.set_instance_count(count) |
Draw count instances. |
geometry.instance_count |
The count, or none for as many as the attributes hold. |
geometry.drawn_instances() -> Int |
How many instances a mesh draws. |
BufferAttribute(data, item_size, mesh_per_attribute=n) |
A per-instance attribute. |
InterleavedBuffer(array, stride, mesh_per_attribute=n) |
A per-instance interleaved buffer. |
A mesh draws the instances. Each instance is a separate draw, so it is sorted and culled at its own place. The renderer reads two per-instance attributes:
-
offsetmoves each instance before the mesh's transform. three.js has no built-inoffset. Its instancing examples add one in their own shader, and this port does the same work without a shader. -
colorcolors each instance when the material hasvertex_colors. That is three.js's behavior for a dividedcolorattribute.
A per-instance position, normal, uv or tangent is refused when drawn. The renderer has no shader that reads them per instance. A draw that is not an instance reads the first per-instance color, as WebGL does.
drawn_instances is the instance count, capped by what the per-instance attributes hold. Each attribute holds count() * mesh_per_attribute() instances. The cap is the smallest of these. three.js takes the cap from the first attribute its shader reads, which a port without shaders cannot ask. An unbounded count with no per-instance attribute is refused.
- A line and points draw an instanced geometry once, not once per instance. Only a mesh draws the instances.
- The raycaster tests the base geometry, as three.js does. It does not test each instance.
-
to_non_indexedkeeps the per-instance attributes as they are. three.js reads them through the index. -
merge_geometriesandmerge_verticesrefuse an instanced geometry. - The JSON loader reads
instanceCountandmeshPerAttribute. three.js's loader leaves both at their defaults. - A clone of an instanced attribute keeps its instancing.
- A negative or zero extent raises.
- A sphere with too few segments or rings raises.
- A plane with fewer than one segment raises.
- A circle needs a positive radius and at least three segments.
- A ring needs an inner radius of zero or more, a larger outer radius, three segments around and one across.
- A cylinder needs a positive height, radii that are not negative and not both zero, three segments around and one down the side.
- A cone needs a positive radius and a positive height.
- A torus needs positive radii and three segments each way.
- A torus knot needs positive radii, three segments each way, and
pandqof at least one. - A polyhedron needs a positive radius, a detail of zero or more, whole vertices and faces, and faces that name vertices it has.
- A capsule needs a positive radius, a length of zero or more, one cap row, three segments around and one row up its side.
- A lathe needs at least two points, one segment, and a sweep of at most one turn. No point can have a negative
x, and no two consecutive points can be the same. A profile that turns straight back to the point before raises, because that corner has no normal. - A tube needs at least two points, or three when closed, no two consecutive the same, a positive radius and three segments around. A path that returns to the point before the last raises, because that tangent is zero. A path that folds straight back on itself raises, because there is no axis to turn the frame about.
- A sweep must be finite, positive and at most one turn.
- A morph target must cover every vertex, three numbers each, and a geometry holds at most eight.
- Either every morph target carries normals or none does.
- A morph influence must be a number, and there are eight of them.
- A shape's contour needs three corners and an area. A contour drawn in a line, or out and back, has neither.
- A shape's hole must lie strictly inside its outline and outside every other hole. Sampled boundaries must not cross or touch.
- An outline that crosses itself raises, because it has no inside and runs out of ears.
- An extrusion needs a positive depth, one step and one curve segment. A bevel needs a positive thickness, a size that is not negative, and one band.
- A text needs the same options as an extrusion and a positive size. Every character needs a glyph, or the font needs a
?glyph. - An extrusion along a path needs one step, one curve segment and a curve path with one curve. The curve must have a frame at each step.
- A parametric surface needs one slice and one stack, and its function must give finite points.
- A convex hull needs four finite points, not all on one point, one line or one plane.
- A decal needs positive extents. A mesh with normals needs a world matrix that does not flatten a dimension.
- A rounded box needs positive extents, one segment and a radius that is not negative.
- An index entry beyond the last vertex raises.
- A group needs a start and a count that are not negative, and a material index of zero or more.
- A draw range needs a start and a count that are not negative.
- A transform that flattens a dimension raises when there are normals to turn.
-
apply_matrix4,apply_normal_matrixandtransform_directionneed three numbers an item. -
set,set_storedandcopy_atraise on an interleaved attribute, and when the numbers do not fit. -
copy_atneeds a source item at least as long as the target item. - An integer attribute needs one of the seven component types.
-
normalize_normalsneedsnormal. - A merge needs one geometry at least. The parts must all be indexed or none, and carry the same attributes, item sizes and morph targets.
- A weld tolerance must be finite and not negative. A crease angle must be finite and not negative.
-
compute_tangentsneedsposition,normalanduv. - A slot outside the triangle stream raises.
-
compute_vertex_normals,bounding_boxandbounding_sphereraise on a geometry with no positions. - An interleaved buffer needs a positive stride and whole runs of it. An interleaved attribute needs a positive item size and an item inside the stride.
- A per-instance attribute or buffer needs a
mesh_per_attributeof one or more. - An instance count cannot be negative, and only an instanced geometry has one. An instanced geometry needs a count or a per-instance attribute.
- The renderer refuses a per-instance
position,normal,uvortangent. - A merge and a weld refuse an instanced geometry.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang