Translation notice
This page was translated with machine translation and may contain inaccuracies. If you can help improve it, please open an issue or submit a pull request.
Lightweight and low-loss algorithm and program implementation for converting OBJ model to voxel model
introduction
background
Minecraft model artists generally use the software Blockbench to create and edit models. A considerable number of artists also use OBJ format models as materials or design models by themselves in other more powerful 3D software, and then manually create voxel versions of these models in Blockbench.
At the same time, although there are a large number of tools for converting OBJ models into voxel models on the market, most of them simply discretize the OBJ models into grids with fixed side lengths for voxel processing. This method not only loses the original art style and model details, but also creates a large number of redundant voxels. It brings a lot of performance overhead during rendering, and is very unfavorable for artists to perform subsequent processing and optimization of the model.
This article will discuss the program implementation and optimization strategies of this framework based on the basic framework mentioned in the author's article "A Feasible Method for Converting OBJ Models to JSON Models" published in Feature 2025.12. During the implementation process, we will deeply explore the application of mathematical tools such as graph theory, number theory, and linear algebra in the model voxelization process.
Algorithm framework
Algorithm process
Combined with the author's observation and analysis of the process of artists using OBJ models to create voxel models, the conversion algorithm proposed in this article mainly goes through the following steps:
Constructed surfaces: The visual feature of the OBJ model is a complex mesh composed of flat surfaces, so we treat each flat surface as an object and express it with voxels.
Find the optimal rectangle: use several larger rectangles to cover every flat surface as much as possible, thereby expressing the entire surface with very few voxels.
Fitting triangles: After covering each flat surface with the optimal rectangle, the remaining uncovered areas are further fitted with triangles to express the geometry of the original OBJ model as accurately as possible.
Describing voxel blocks: After basically expressing the entire model with voxels, we will describe the position, color and other information of each voxel block in different formats to generate a final voxel model file that can be used in Minecraft or Blockbench.
Main questions
In the process of implementing the above algorithm, several main problems need to be solved:
The format of the OBJ model has different conventions and parsing methods, and it is necessary to correctly handle the reading and organization of vertices, normals, texture coordinates, and surface information in the program.
Flat surfaces may be concave polygons and may have holes. These situations require special attention.
The art style of Minecraft generally requires that the side length of a voxel be an integer multiple of a certain minimum unit. For example, the default precision in Blockbench is
units, fine adjustments are respectively and other units.
Algorithm implementation
constructed surface
Assuming that we have read the vertices, normals and surface information of the OBJ model (we don't care about other data), record the vertex set as
In order to construct the surface, we propose two conditions:
noodle
Two faces share at least one edge
The normal vectors of the two faces are the same, that is
we will
During implementation, since only faces sharing the same normal will be divided into the same set, you can first
When reading the program, if two normals
If they are the same (or approximately the same), they will be recorded as the same normal, and index mapping will be provided. Traverse all faces
, if its normal index corresponds to the same normal in the mapping , then add it to the set , and finally get multiple normal groups Maintain a hash table recording which faces each edge is shared by
Representation surface The set of edges formed by pairwise combinations of vertices is expressed by Represents an edge The undirected representation of . in each normal group
Within, initialize a union search set, each face belongs to an independent set Traverse all faces within this normal group
Query each edge of it in the hash table Which faces are shared, if an edge is shared by another face shared, then in the merge set the and Merge into the same set to obtain the connected components within the normal group, each connected component corresponds to a flat surface When the union-find sets in each normal group are processed, these union-find sets constitute the division results of all flat surfaces in the model, and each union-find set corresponds to a flat surface.
The following is a short code implementation:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
xroot = self.find(x)
yroot = self.find(y)
if xroot == yroot:
return
if self.rank[xroot] < self.rank[yroot]:
self.parent[xroot] = yroot
else:
self.parent[yroot] = xroot
if self.rank[xroot] == self.rank[yroot]:
self.rank[xroot] += 1
# 假设 faces 是面列表, 每个面是顶点索引的列表
# normals 是每个面的法线向量
from collections import defaultdict
def construct_surfaces(faces, normals):
normal_map = {}
normal_groups = defaultdict(list)
for i, n in enumerate(normals):
key = tuple(round(c, 6) for c in n) # 使用近似法线作为键
if key not in normal_map:
normal_map[key] = len(normal_map)
normal_groups[normal_map[key]].append(i)
edge_map = defaultdict(list)
for i, f in enumerate(faces):
for j in range(len(f)):
e = tuple(sorted((f[j], f[(j + 1) % len(f)])))
edge_map[e].append(i)
surfaces = []
for group in normal_groups.values():
uf = UnionFind(len(faces))
for i in group:
f = faces[i]
for j in range(len(f)):
e = tuple(sorted((f[j], f[(j + 1) % len(f)])))
for other in edge_map[e]:
if other in group:
uf.union(i, other)
components = defaultdict(list)
for i in group:
root = uf.find(i)
components[root].append(i)
surfaces.extend(components.values())
return surfacesFind the optimal rectangle
This step will basically cover the general model surface, by finding several rectangles with larger areas on each flat surface to cover the entire surface as much as possible, thereby expressing the surface with the fewest voxels.
Our task is on the surface
These rectangles
- The side length of the rectangle is
an integer multiple of, is a normal constant. - The rectangle must lie entirely on the surface
Inside, that is, each vertex of the rectangle is on the surface boundaries or interiors and do not overlap each other. - Under the condition that the first two conditions are met,
yes The rectangle with the largest area.
Since we need to loop each time to find the remaining surface
- calculate
In the tangent space, we use the covariance matrix to obtain two numerically stable tangents:
remember
Calculate the covariance matrix:
Perform eigendecomposition on the covariance matrix, and take the eigenvectors corresponding to the first two largest eigenvalues as the tangent direction, recorded as
place the surface
The vertices of are projected into tangent space above, the third component of coordinate is close to , discard and get the two-dimensional coordinate , thereby converting the three-dimensional surface problem into a rectangular coverage problem on a two-dimensional plane. Since we need to restore the three-dimensional coordinates later, we cannot omit it calculation.
in
Traverse the surface
All faces within, extract all directed edge indices and join the collection in, there are rules in Finally get the boundary set for each boundary edge
, construct a local plane coordinate system, let for direction, for direction, and then transform all the boundary segments of the current surface into the local coordinate system. Different from the previous article, in the current implementation, this edge is only used to determine the orientation of the rectangle search. It does not force the final rectangle to be attached to this edge, but allows the rectangle to freely translate within the search area of the local coordinate system. In the local coordinate system, take the axis-aligned bounding box of all boundary vertices.
as the search area, and use is the step size and is discretized into a grid. Sampling positions for each column , calculate its intersection points with all boundary line segments, and then press the intersection point The coordinates are sorted and paired to obtain several valid intervals of the column located inside the surface. The grids falling in these intervals are marked as available, and the remaining grids are marked as unavailable. Convert the available interval of each column into the height of the histogram, and use the monotonic stack dynamic programming of "maximum rectangle in the histogram" to online find the largest axis-aligned rectangle composed only of available grids, whose width and height are both
An integer multiple of . Repeat this process for all candidate boundary directions, and after passing the legality check of "the rectangle is completely located inside the outer ring and does not overlap with existing holes", the rectangle with the largest area is taken as the optimal rectangle of the current surface. If no legal rectangle can be found for each surface, the current surface is directly merged into the triangular surface set And in short this surface search. Note down the optimal rectangle currently found
The two-dimensional coordinate of Restore its three-dimensional coordinates through the matrix and add the optimal rectangle set middle Get the reconstruction boundary
E_r is the current optimal rectangle The set of boundary line segments of , and from Remove the quilt Cover the boundary line segments to get a new boundary set right
Triangulate and discard areas smaller than of triangles, merged into the triangular surface collection Perform surface reconstruction on the triangular mesh to obtain the remaining surface
, and repeat steps 4-7 on the remaining surfaces until no more qualified rectangles can be placed on the remaining surfaces, or the allowed number of iterations is exceeded.
fit triangle
Fitting a triangle is a definite mathematical process, that is, given the coordinates of the three vertices of the triangle
For triangular surfaces, since there is a minimum element
For small triangles, we try two fitting methods:
Fit the triangle using two bounding rectangles. For the vertex corresponding to the largest angle, place two rectangles parallel to the adjacent sides of the vertex inside the triangle so that the inner sides of the two rectangles intersect at a point on the opposite side of the vertex. By taking different intersection points, different fits can be obtained, and the method with the smallest error can be selected. The dimensions of the bounding rectangle need not be
An integer multiple of , but if the solution can be obtained is an integer multiple of , then select can minimize the error in integer multiples solution. Calculate the center of gravity of the triangle, and then use the center of gravity as the center directly using a
The rectangle fits the triangle, one side coincides with the side of the triangle, and the error is calculated 。
error here
$$e_S = \frac{1}{4} \sum_{S \in S_{voxel} - S_{triangle}} Area(S)$$
Choose the one with the smaller error among the two methods as the fitting method for the small triangle.
For medium triangles, we use double wrapping:
Select the maximum angle, recorded as
, the adjacent angle is recorded as and , the opposite sides of the three angles are written as 。 exist
Take a little bit , Pass do perpendicular to , respectively from the ray intersection point 。 by
For example, if exist above, then the rectangle An edge of , the length of the adjacent side perpendicular to it is Its value is equal to and distance; if exist along On the extension line of the direction, the rectangle An edge of , the length of its perpendicular adjacent side is also 。
To simplify calculations, we assume
Satisfy the following optimization constraints:
$$ S(R_1) = \frac{1}{2} \left( d_1^2 \tan B + (\max(0, \cot C d_1 - b))^2 \tan(B+C) \right) $$
$$ S = S(R_1) + S(R_2) $$
$$ e_S = \frac{S}{4} = \frac{S(R_1) + S(R_2)}{4} $$
$$\phi (d_1, d_2) = d_1 \sin B + d_2 \sin C - a = 0$$
Using the Lagrange multiplier method, we construct the Lagrange function:
$$ \mathcal{L}(d_1, d_2, \lambda) = e_S(d_1, d_2) + \lambda \phi(d_1, d_2) $$
right
$$
\frac{\partial \mathcal{L}}{\partial d_1} = 0, \quad \frac{\partial \mathcal{L}}{\partial d_2} = 0, \quad \frac{\partial \mathcal{L}}{\partial \lambda} = 0
$$
For large triangles, we use the wraparound method:
We use different LODs to divide the internal area, that is, divide the internal area into
grid, Pick ,in is the side length of the largest inscribed rectangle of a triangle and The integer part of the ratio. For the "large triangle" in our definition, 。 Under different LODs, pixelate the internal area (inscribed fitting), that is, within the divided grid, select the grid completely contained within the triangle as the pixel block.
Select three vertices of the triangle
, place three rectangles along the adjacent sides. , whose thicknesses are respectively . Choose the smallest Cover the area that cannot be covered by inscribed fitting, and calculate the distortion area caused by wrapping 。
For this step, we need to first select the direction, that is, select one side as the bottom edge. At this time, the determined pixels will generate the left, right, and top borders. For the hypotenuse on the left, we calculate the distance between each grid point of the left boundary and the upper boundary from the hypotenuse, and take the minimum value as
Calculate different sides as bases separately
$$ S = \frac{1}{2} \left((d_1^2 + d_2^2)\max(0, \cot A) + (d_1^2 + d_3^2) \max(0, \cot B) + (d_2^2 + d_3^2) \max(0, \cot C)\right) $$
4. Calculate the error value at each LOD$e_S = \frac{S}{4}$and number of voxels$M$, choose such that the cost index$\mathcal{J} = \alpha e_S + \beta M$The smallest solution is taken as the final solution.
here,
Likewise, we can find a good fit inside based on the histogram maximum rectangle algorithm.
Describe voxel blocks
Voxel blocks (elements) in Minecraft are defined by the following fields:
* from:Specifies the starting point of the model element cuboid.
(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the X axisx1`。(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the Y axisy1`。(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the Z axisz1`。 * to:Specifies the end point of the model element cuboid.
(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the X axisx2`。(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the Y axisy2`。(not less than`
- 16
and not greater than32) The coordinate of the cuboid on the Z axisz2`。 rotation: (default no rotation) sets the rotation of the element.
* origin:Set the center of rotation.
The coordinate of the rotation center on the X axis.
The coordinate of the rotation center on the Y axis.
The coordinate of the rotation center on the Z axis.
rescale: (default is
false) whether to rescale the rotated model elements.- Both single-axis rotation and multi-axis rotation can be used. At least one rotation must be specified, and the game will try to use single-axis rotations first.
- Single axis rotation format:
* angle:Rotation angle.
* axis:Rotation axis. can be
x、yorz。
- Multi-axis rotation format:
* x:The rotation angle on the X axis.
* y:The rotation angle on the Y axis.
* z:The rotation angle on the Z axis.
shade: (default is
true) whether to render shadows.light_emission:(
0arrive15) specifies the luminescence level to render this model element.* faces:All faces of the model element.
<face>:Specifies the attributes of a certain face.
If this definition is regarded as a transformation of the unit cube, then it can be written as
The decomposition is related to ajsonFields correspond one to one, so we only need to solve each matrix to write the correspondingjsonfield.
infromField is a translation matrix.rotation.x, rotation.y, rotation.z,rotation.originfield.
The decomposition of this transformation matrix is not uniquely determined by the final vertex position of the voxel.
- The first preference is due to
has limitations, fromandtoFields are restricted toWithin the range, we define it as the identity matrix All translations are contributed by rotation transformations about the vertices.
Under this preference, the transformation degenerates into
$$\mathscr{A}(x) = \mathbf{T_0 R T_0^{-1} S}x$$
Then one corner of the voxel and its four vertices adjacent to the corner
$$
\begin{cases}
\mathbf{T_0 R T_0^{-1} S}[0, 0, 0, 1]^T = v_0 \\
\mathbf{T_0 R T_0^{-1} S}[1, 0, 0, 1]^T = v_x \\
\mathbf{T_0 R T_0^{-1} S}[0, 1, 0, 1]^T = v_y \\
\mathbf{T_0 R T_0^{-1} S}[0, 0, 1, 1]^T = v_z
\end{cases}
$$
Can be solved:
$$\mathbf{S} = \begin{pmatrix}
\|v_x - v_0\| & 0 & 0 & 0 \\
0 & \|v_y - v_0\| & 0 & 0 \\
0 & 0 & \|v_z - v_0\| & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}$$
$$\mathbf{R} = \begin{pmatrix}
v_x - v_0 & v_y - v_0 & v_z - v_0 & \mathbf{\varepsilon_4} \\
\end{pmatrix}_{4 \times 4}
$$
in
$T_0$A center of rotation can be given using geometric methods, given by$\mathbf{R}$Find the axis of rotation$\mathbf{u} = [u_1, u_2, u_3]^T$and rotation angle$\theta$, and there is$\mathbf{u}$for$\mathbf{R}$upper left$3 \times 3$The matrix eigenvalues are$1$eigenvector.
Then the center of rotation is the point passing through the origin and
That is to say, satisfy $$ \mathbf{u_1}x_0 + \mathbf{u_2}y_0 + \mathbf{u_3}z_0 = 0, \quad \angle (\overrightarrow{CO}, \overrightarrow{Cv_0}) = \theta , \quad |\overrightarrow{OC}| = | \overrightarrow{Ov_0} | $$
If you remember
Then the solution can be written as
$$C = \frac{\|\mathbf{v}_0\|^2}{2 \|\mathbf{v}_\bot \|^2}\mathbf{v_\bot} + \frac{\|\mathbf{v}_0\|\sqrt{\|\mathbf{v}_\bot\|^2-\|\mathbf{v}_0\|^2\sin^2\frac{\theta}{2}}}{2\|\mathbf{u}\|\|\mathbf{v}_\bot\|^2\sin\frac{\theta}{2}}$$
but
$$\mathbf{T_0} = \begin{pmatrix} 0 & 0 & 0 & x_0 \\ 0 & 0 & 0 & y_0 \\ 0 & 0 & 0 & z_0 \\ 0 & 0 & 0 & 1 \end{pmatrix}, \quad \mathbf{T_0}^{-1} = \begin{pmatrix} 0 & 0 & 0 & -x_0 \\ 0 & 0 & 0 & -y_0 \\ 0 & 0 & 0 & -z_0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$
The second preference, in order to facilitate artists to adjust the model, we will
The offset represented is set to be aligned with the center of the voxel. Same as above, here The amount of translation should align the center of the voxel with the center of the unit cube. Just let the above derivation Replace with , in
Optimization space
All voxels generated by this method are patches, and the lower bound of the generated result is 6 times the minimum voxel. In the future, the number of voxels may be reduced by checking the internal space and merging upper and lower surfaces. If the error caused by triangle wrapping can be hidden in the internal space of the voxel, the final error can be further reduced.
Sometimes the connected components generated by the original model are not optimal, and fitting may be assisted by adding vertices and faces without destroying the visual effect. This also requires determining whether a certain position is inside the model.
At the same time, sometimes the model may need a scaling transformation to minimize the residual error generated by the optimal rectangular stage. In this case, a global optimal scaling ratio needs to be approximated by the aspect ratio within each connected component. See the appendix for details.
in conclusion
This article optimizes its complexity based on original research, and proposes several possible approximation solutions and improvement ideas to reduce the number and errors of generated voxels while ensuring visual effects. At the same time, a specific program implementation idea is given, which can ensure that the method runs within a certain complexity.
Acknowledgments and citations
We would like to thank Boanci for the financial support and Blade for discussions and suggestions on reducing the error of the optimal rectangle.
Thanks to Numio and Boanci for providing models to test and verify the effect of the algorithm.
Thanks to Blender for providing modeling tool support.
Original research: "A feasible method to convert OBJ model to json model" Xuanyu1725 flybridOuO Feature 202512 https://vanillalibrary.mcfpp.top/datapack-index/feature/archive/202512/1/content.html
Appendix - Approximate calculation of global optimal scaling ratio (optimizing optimal rectangle)
It has been found in practice that even8cubeExamples of
In order to optimize the performance of the best rectangle, we need to find a way to get the best rectangle from the model and
In the following discussion, we refer to the optimal rectangle with unrestricted side length as the theoretical optimal rectangle, with the side length as
How to define error:
in each connected component
Each candidate edge of , we can find a theoretical optimal rectangle T and the corresponding best rectangle At this time the error is defined as$\varepsilon_{e,i}(k) = S(T)
S(T_\delta)$
Note that scaling does not affect the determination of the best edge, so the best rectangle always appears on the same edge, and although the derivatives on the continuous interval may be different, for all
Obviously for each connected component
We scale the entire model
obviously
So we just need to verify all candidate scaling
At present, although we need to run through all connected components to determine the candidate
The best rectangle always appears on the same side - The position of the theoretical optimal rectangle does not change with scaling, only the value of the area is changed.
- therefore
The optimal rectangle can directly take the position of the known theoretical optimal rectangle, and only need to adjust its side length according to the scaling ratio. can be an integer multiple of (simply use rounding down)
Candidate scaling ratios are discussed below
Assuming no scaling initially, we find at this time that the length and width of the theoretical optimal rectangle are respectively
Note that k is infinite, and there is obviously a lower bound
an approximation
when
The analysis found that when
In fact, the first such
mathematical premise based on
is a shape like function, When it is a rational number, the distribution of discontinuous points is regular, and these discontinuous points can be written explicitly using number theory methods.

Scaling does not affect the determination of the best edge: after scaling, the theoretical optimal rectangle sum
The edges of the local space where the optimal rectangle is located remain unchanged. Functional properties of total error:
The discontinuities are all The union of discontinuities on the continuous interval of All are continuous Strictly increasing on each continuous interval
Eliminating the trivial solution of k = 0, in rational precision, the smallest k first appears in
k = qatUsing a rational approximation of aspect ratio
replace Calculation, discontinuity points will deviate, but a smaller error can still be obtained.
Note, in actual implementation
Obtained from several substitutions, the actual shape may be as follows$$ax^2 |e_i|^2 - \delta^2 \lfloor(\frac{kx}{\delta})\rfloor \cdot \lfloor(\frac{akx}{\delta})\rfloor$$