Add everything
This commit is contained in:
parent
11110ea82f
commit
e7fd2a1b4b
69
README.md
69
README.md
@ -1,2 +1,67 @@
|
||||
# meshport
|
||||
Easily export areas in Minetest to meshes for 3D rendering.
|
||||
# Meshport (Minetest Mesh Exporter)
|
||||
|
||||

|
||||
|
||||
Meshport is a mod which allows easy exporting of scenes from Minetest to a `.obj` file, complete with materials and textures. These models can be imported directly into Blender or another 3D program for rendering and animation.
|
||||
|
||||
This mod is still in the "alpha" phase; as such, many types of nodes are not yet able to be exported. See below for more details.
|
||||
|
||||
## Usage
|
||||
|
||||
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported, then use `/meshport` to export the mesh. The saved `.obj` and `.mtl` files will be located in the `meshport` folder of the world directory, within a subfolder.
|
||||
|
||||
### Importing into Blender
|
||||
|
||||
Once the model is exported, you should be able to import the `.obj` file with default settings. Make sure "Image Search" in the import settings is selected to ensure the textures are imported as well. Texture modifiers are ignored, so some materials will likely have to be fixed by hand.
|
||||
|
||||
#### Fixing interpolation
|
||||
|
||||
If you intend to render the scene, you will want to set the interpolation mode of all the image textures to "Closest" in order to keep the pixelated look. This can either be done manually or by running this script in Blender's text editor:
|
||||
|
||||
```python
|
||||
import bpy
|
||||
|
||||
for mat in bpy.data.materials:
|
||||
try:
|
||||
nodes = mat.node_tree.nodes
|
||||
|
||||
for node in nodes:
|
||||
if node.type == "TEX_IMAGE":
|
||||
node.interpolation = "Closest"
|
||||
except:
|
||||
continue
|
||||
```
|
||||
|
||||
#### Fixing vertex normals
|
||||
|
||||
Some mesh nodes may not have any vertex normals, which can lead to lighing problems. To fix this, what I have found to work is to first select the all the problematic nodes, either manually or by selecting by material in edit mode; then, mark the selected edges as sharp, and then average the normals by face area.
|
||||
|
||||
Additional tip: Use an HDRI sky texture (such as one from [here](https://hdrihaven.com)) for awesome-looking renders. ;)
|
||||
|
||||
## Supported features
|
||||
|
||||
At the moment, only the following node drawtypes are supported:
|
||||
|
||||
- Cubic drawtypes, including `normal`, `glasslike`, `allfaces`, and their variants (see below)
|
||||
- `nodebox`
|
||||
- `mesh` (only `.obj` meshes are exported)
|
||||
|
||||
Many special rendering features are not yet supported.
|
||||
|
||||
### A note on cubic nodes
|
||||
|
||||
Due to the differences between Minetest's rendering engine and 3D programs such as Blender, it is not possible to exactly replicate how certain cubic nodes are rendered in Minetest. Instead, to avoid duplicate faces, a face priority system is used as follows:
|
||||
|
||||
| Priority level | Drawtypes |
|
||||
|----------------|----------------------------------------------------|
|
||||
| 4 | `normal` |
|
||||
| 3 | `glasslike` |
|
||||
| 2 | `glasslike_framed` and `glasslike_framed_optional` |
|
||||
| 1 | `allfaces` and `allfaces_optional` |
|
||||
| 0 | All other nodes |
|
||||
|
||||
In places where two nodes of different drawtypes touch, only the face of the node with the higher priority drawtype will be drawn. For `allfaces` type nodes (such as leaves), interior faces will be drawn only when facing X+, Y+, or Z+ in the Minetest coordinate space.
|
||||
|
||||
## License
|
||||
|
||||
All code is licensed under the GNU LGPL v3.0.
|
||||
|
189
api.lua
Normal file
189
api.lua
Normal file
@ -0,0 +1,189 @@
|
||||
meshport.Faces = {}
|
||||
|
||||
function meshport.Faces:new()
|
||||
local o = {
|
||||
faces = {},
|
||||
}
|
||||
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
return o
|
||||
end
|
||||
|
||||
function meshport.Faces:insert_face(face)
|
||||
table.insert(self.faces, face)
|
||||
end
|
||||
|
||||
function meshport.Faces:copy()
|
||||
local newFaces = meshport.Faces:new()
|
||||
|
||||
-- Using `table.copy` on all of `self.faces` does not work here.
|
||||
newFaces.faces = table.copy(self.faces)
|
||||
-- for _, face in ipairs(self.faces) do
|
||||
-- table.insert(newFaces.faces, table.copy(face))
|
||||
-- end
|
||||
|
||||
return newFaces
|
||||
end
|
||||
|
||||
function meshport.Faces:translate(vec)
|
||||
for _, face in ipairs(self.faces) do
|
||||
for i, vert in ipairs(face.verts) do
|
||||
face.verts[i] = vector.add(vert, vec)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.Faces:rotate_by_facedir(facedir)
|
||||
if facedir == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for _, face in ipairs(self.faces) do
|
||||
-- Rotate vertices.
|
||||
for i = 1, #face.verts do
|
||||
face.verts[i] = meshport.rotate_vector_by_facedir(face.verts[i], facedir)
|
||||
end
|
||||
|
||||
-- Rotate vertex normals.
|
||||
for i = 1, #face.vert_norms do
|
||||
face.vert_norms[i] = meshport.rotate_vector_by_facedir(face.vert_norms[i], facedir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.Faces:apply_tiles(nodeDef)
|
||||
local tile
|
||||
|
||||
for _, face in ipairs(self.faces) do
|
||||
tile = meshport.get_tile(nodeDef.tiles, face.tile_idx)
|
||||
face.texture = tile.name or tile
|
||||
end
|
||||
end
|
||||
|
||||
meshport.Mesh = {}
|
||||
|
||||
function meshport.Mesh:new()
|
||||
local o = {
|
||||
verts = {},
|
||||
vert_norms = {},
|
||||
tex_coords = {},
|
||||
faces = {},
|
||||
}
|
||||
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function meshport.Mesh:insert_face(face)
|
||||
local indices = {
|
||||
verts = {},
|
||||
vert_norms = {},
|
||||
tex_coords = {},
|
||||
}
|
||||
|
||||
local elementStr, vec
|
||||
|
||||
-- Add vertices to mesh.
|
||||
for i, vert in ipairs(face.verts) do
|
||||
-- Invert Z axis to comply with Blender's coordinate system.
|
||||
vec = meshport.clean_vector({x = vert.x, y = vert.y, z = -vert.z})
|
||||
elementStr = string.format("v %f %f %f\n", vec.x, vec.y, vec.z)
|
||||
indices.verts[i] = meshport.find_or_insert(self.verts, elementStr)
|
||||
end
|
||||
|
||||
-- Add texture coordinates (UV map).
|
||||
for i, texCoord in ipairs(face.tex_coords) do
|
||||
elementStr = string.format("vt %f %f\n", texCoord.x, texCoord.y)
|
||||
indices.tex_coords[i] = meshport.find_or_insert(self.tex_coords, elementStr)
|
||||
end
|
||||
|
||||
-- Add vertex normals.
|
||||
for i, vertNorm in ipairs(face.vert_norms) do
|
||||
-- Invert Z axis.
|
||||
vec = meshport.clean_vector({x = vertNorm.x, y = vertNorm.y, z = -vertNorm.z})
|
||||
elementStr = string.format("vn %f %f %f\n", vec.x, vec.y, vec.z)
|
||||
indices.vert_norms[i] = meshport.find_or_insert(self.vert_norms, elementStr)
|
||||
end
|
||||
|
||||
-- Add faces to mesh.
|
||||
local vertStrs = {}
|
||||
local vertList = {}
|
||||
|
||||
for i = 1, #indices.verts do
|
||||
vertList = table.insert(vertStrs, table.concat({
|
||||
indices.verts[i],
|
||||
-- If there is a vertex normal but not a texture coordinate, insert a blank string here.
|
||||
indices.tex_coords[i] or (indices.vert_norms[i] and ""),
|
||||
indices.vert_norms[i],
|
||||
}, "/"))
|
||||
end
|
||||
|
||||
self.faces[face.texture] = self.faces[face.texture] or {}
|
||||
table.insert(self.faces[face.texture], string.format("f %s\n", table.concat(vertStrs, " ")))
|
||||
end
|
||||
|
||||
function meshport.Mesh:write_obj(path)
|
||||
local objFile = io.open(path .. DIR_DELIM .. "/model.obj", "w")
|
||||
|
||||
objFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n")
|
||||
objFile:write("mtllib materials.mtl\n")
|
||||
|
||||
-- Write vertices.
|
||||
for _, vert in ipairs(self.verts) do
|
||||
objFile:write(vert)
|
||||
end
|
||||
|
||||
-- Write texture coordinates.
|
||||
for _, texCoord in ipairs(self.tex_coords) do
|
||||
objFile:write(texCoord)
|
||||
end
|
||||
|
||||
-- Write vertex normals.
|
||||
for _, vertNorm in ipairs(self.vert_norms) do
|
||||
objFile:write(vertNorm)
|
||||
end
|
||||
|
||||
-- Write faces, sorted in order of material.
|
||||
for mat, faces in pairs(self.faces) do
|
||||
objFile:write(string.format("usemtl %s\n", mat))
|
||||
|
||||
for _, face in ipairs(faces) do
|
||||
objFile:write(face)
|
||||
end
|
||||
end
|
||||
|
||||
objFile:close()
|
||||
end
|
||||
|
||||
function meshport.Mesh:write_mtl(path, playerName)
|
||||
local textures = meshport.get_asset_paths("textures")
|
||||
local matFile = io.open(path .. "/materials.mtl", "w")
|
||||
|
||||
matFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n\n")
|
||||
|
||||
-- Write material information.
|
||||
for mat, _ in pairs(self.faces) do
|
||||
matFile:write(string.format("newmtl %s\n", mat))
|
||||
|
||||
-- Attempt to get the base texture, ignoring texture modifiers.
|
||||
local texName = string.match(mat, "[%w%s%-_%.]+%.png") or mat
|
||||
|
||||
if textures[texName] then
|
||||
if texName ~= mat then
|
||||
meshport.print(playerName, "warning", string.format("Ignoring texture modifers in material %q.", mat))
|
||||
end
|
||||
|
||||
matFile:write(string.format("map_Kd %s\n\n", textures[texName]))
|
||||
else
|
||||
meshport.print(playerName, "warning",
|
||||
string.format("Could not find texture %q. Using a dummy material instead.", texName))
|
||||
matFile:write(string.format("Kd %f %f %f\n\n", math.random(), math.random(), math.random()))
|
||||
end
|
||||
|
||||
matFile:write("\n\n")
|
||||
end
|
||||
|
||||
matFile:close()
|
||||
end
|
175
export.lua
Normal file
175
export.lua
Normal file
@ -0,0 +1,175 @@
|
||||
meshport.nodebox_cache = {}
|
||||
meshport.mesh_cache = {}
|
||||
|
||||
meshport.cube_face_priority = {
|
||||
allfaces = 1,
|
||||
glasslike_framed = 2,
|
||||
glasslike = 3,
|
||||
normal = 4,
|
||||
}
|
||||
|
||||
function meshport.create_cube_node(nodeDrawtype, nodeTiles, pos, facedir, neighbors)
|
||||
local sideFaces = {
|
||||
{{x = 0.5, y = 0.5, z =-0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x =-0.5, y = 0.5, z =-0.5}}, -- Y+
|
||||
{{x = 0.5, y =-0.5, z = 0.5}, {x = 0.5, y =-0.5, z =-0.5}, {x =-0.5, y =-0.5, z =-0.5}, {x =-0.5, y =-0.5, z = 0.5}}, -- Y-
|
||||
{{x = 0.5, y =-0.5, z = 0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x = 0.5, y = 0.5, z =-0.5}, {x = 0.5, y =-0.5, z =-0.5}}, -- X+
|
||||
{{x =-0.5, y =-0.5, z =-0.5}, {x =-0.5, y = 0.5, z =-0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x =-0.5, y =-0.5, z = 0.5}}, -- X-
|
||||
{{x =-0.5, y =-0.5, z = 0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x = 0.5, y =-0.5, z = 0.5}}, -- Z+
|
||||
{{x = 0.5, y =-0.5, z =-0.5}, {x = 0.5, y = 0.5, z =-0.5}, {x =-0.5, y = 0.5, z =-0.5}, {x =-0.5, y =-0.5, z =-0.5}}, -- Z-
|
||||
}
|
||||
|
||||
local texCoords = {{x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}}
|
||||
|
||||
local faces = meshport.Faces:new()
|
||||
-- For glasslike_framed nodes, only the first tile is used.
|
||||
local tileIdx = nodeDrawtype == "glasslike_framed" and 1 or nil
|
||||
local neighborDrawtype, vertNorm
|
||||
|
||||
for i = 1, 6 do
|
||||
neighborDrawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(neighbors[i]).drawtype)
|
||||
|
||||
if meshport.cube_face_priority[nodeDrawtype] > (meshport.cube_face_priority[neighborDrawtype] or 0)
|
||||
-- For allfaces nodes (such are leaves), interior faces are drawn only when facing X+, Y+, or Z+.
|
||||
or (nodeDrawtype == "allfaces" and neighborDrawtype == "allfaces" and i % 2 == 1) then
|
||||
vertNorm = meshport.neighbor_dirs[i]
|
||||
|
||||
faces:insert_face(meshport.prepare_cuboid_face({
|
||||
verts = sideFaces[i],
|
||||
tex_coords = texCoords,
|
||||
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
|
||||
tile_idx = tileIdx,
|
||||
}, nodeTiles, pos, facedir, i))
|
||||
end
|
||||
end
|
||||
|
||||
return faces
|
||||
end
|
||||
|
||||
function meshport.create_nodebox_node(nodeName, pos, facedir, param2, neighbors)
|
||||
local nodeDef = minetest.registered_nodes[nodeName]
|
||||
|
||||
if not meshport.nodebox_cache[nodeName] then
|
||||
meshport.nodebox_cache[nodeName] = meshport.prepare_nodebox(nodeDef.node_box)
|
||||
end
|
||||
|
||||
local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, facedir, param2, neighbors)
|
||||
|
||||
if meshport.nodebox_cache[nodeName].type ~= "connected" then
|
||||
boxes:rotate_by_facedir(facedir)
|
||||
end
|
||||
|
||||
return boxes:to_faces(nodeDef.tiles, pos, facedir)
|
||||
end
|
||||
|
||||
function meshport.create_mesh_node(nodeDef, playerName)
|
||||
local meshName = nodeDef.mesh
|
||||
|
||||
if not meshName then
|
||||
return
|
||||
end
|
||||
|
||||
if not meshport.mesh_cache[meshName] then
|
||||
-- Get the paths of all .obj meshes.
|
||||
if not meshport.obj_paths then
|
||||
meshport.obj_paths = meshport.get_asset_paths("models", ".obj")
|
||||
end
|
||||
|
||||
if not meshport.obj_paths[meshName] then
|
||||
if string.lower(string.sub(meshName, -4)) ~= ".obj" then
|
||||
meshport.print(playerName, "warning", string.format("Mesh %q is not supported.", meshName))
|
||||
else
|
||||
meshport.print(playerName, "warning", string.format("Mesh %q could not be found.", meshName))
|
||||
end
|
||||
|
||||
-- Cache a blank faces object so the player isn't warned again.
|
||||
meshport.mesh_cache[meshName] = meshport.Faces:new()
|
||||
else
|
||||
meshport.mesh_cache[meshName] = meshport.parse_obj(meshport.obj_paths[meshName])
|
||||
end
|
||||
end
|
||||
|
||||
return meshport.mesh_cache[meshName]:copy()
|
||||
end
|
||||
|
||||
function meshport.create_node(idx, area, content, param2, playerName)
|
||||
if content[idx] == minetest.CONTENT_AIR or content[idx] == minetest.CONTENT_IGNORE then
|
||||
return
|
||||
end
|
||||
|
||||
local nodeDef = meshport.get_def_from_id(content[idx])
|
||||
|
||||
if not nodeDef.drawtype or nodeDef.drawtype == "airlike" then
|
||||
return
|
||||
end
|
||||
|
||||
local nodeDrawtype = meshport.get_aliased_drawtype(nodeDef.drawtype)
|
||||
local facedir = meshport.get_facedir(param2[idx], nodeDef.paramtype2)
|
||||
local isCubicType = meshport.cube_face_priority[nodeDrawtype] or nodeDrawtype == "nodebox"
|
||||
local neighbors, faces
|
||||
|
||||
if isCubicType then
|
||||
neighbors = meshport.get_node_neighbors(content, area, idx)
|
||||
end
|
||||
|
||||
if meshport.cube_face_priority[nodeDrawtype] then
|
||||
faces = meshport.create_cube_node(nodeDrawtype, nodeDef.tiles, area:position(idx), facedir, neighbors)
|
||||
elseif nodeDrawtype == "nodebox" then
|
||||
faces = meshport.create_nodebox_node(
|
||||
minetest.get_name_from_content_id(content[idx]), area:position(idx), facedir, param2[idx], neighbors)
|
||||
elseif nodeDrawtype == "mesh" then
|
||||
faces = meshport.create_mesh_node(nodeDef, playerName)
|
||||
end
|
||||
|
||||
if not faces then
|
||||
return
|
||||
end
|
||||
|
||||
if not isCubicType then
|
||||
faces:rotate_by_facedir(facedir)
|
||||
end
|
||||
|
||||
faces:apply_tiles(nodeDef)
|
||||
return faces
|
||||
end
|
||||
|
||||
function meshport.create_mesh(playerName, p1, p2)
|
||||
meshport.print(playerName, "info", "Generating mesh...")
|
||||
p1, p2 = vector.sort(p1, p2)
|
||||
local vm = minetest.get_voxel_manip()
|
||||
|
||||
-- Add one node of padding to area so we can read neighbor blocks.
|
||||
local vp1, vp2 = vm:read_from_map(vector.subtract(p1, 1), vector.add(p2, 1))
|
||||
local content = vm:get_data()
|
||||
local param2 = vm:get_param2_data()
|
||||
|
||||
-- Create a VoxelArea for converting from flat array indices to position vectors.
|
||||
local vArea = VoxelArea:new{MinEdge = vp1, MaxEdge = vp2}
|
||||
local mesh = meshport.Mesh:new()
|
||||
local faces
|
||||
|
||||
-- Loop through all positions in the desired area.
|
||||
for idx in vArea:iterp(p1, p2) do
|
||||
-- Generate a mesh for the node.
|
||||
faces = meshport.create_node(idx, vArea, content, param2, playerName)
|
||||
|
||||
if faces then
|
||||
-- Move the node to its proper position in the mesh.
|
||||
faces:translate(vector.add(vector.subtract(vArea:position(idx), p1), 0.5))
|
||||
|
||||
for _, face in ipairs(faces.faces) do
|
||||
-- Add each face to our final mesh.
|
||||
mesh:insert_face(face)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Create path for exported mesh.
|
||||
local path = string.format("%s%smeshport%s%s_%s",
|
||||
minetest.get_worldpath(), DIR_DELIM, DIR_DELIM, playerName, os.date("%Y-%m-%d_%H-%M-%S"))
|
||||
minetest.mkdir(path)
|
||||
|
||||
mesh:write_obj(path)
|
||||
mesh:write_mtl(path, playerName)
|
||||
|
||||
meshport.print(playerName, "info", "Finished.")
|
||||
end
|
299
helpers.lua
Normal file
299
helpers.lua
Normal file
@ -0,0 +1,299 @@
|
||||
meshport.neighbor_dirs = {
|
||||
{x = 0, y = 1, z = 0}, -- Y+
|
||||
{x = 0, y = -1, z = 0}, -- Y-
|
||||
{x = 1, y = 0, z = 0}, -- X+
|
||||
{x = -1, y = 0, z = 0}, -- X-
|
||||
{x = 0, y = 0, z = 1}, -- Z+
|
||||
{x = 0, y = 0, z = -1}, -- Z-
|
||||
}
|
||||
|
||||
meshport.facedir_to_tile_indices = {
|
||||
[0] =
|
||||
{1, 2, 3, 4, 5, 6},
|
||||
{1, 2, 5, 6, 4, 3},
|
||||
{1, 2, 4, 3, 6, 5},
|
||||
{1, 2, 6, 5, 3, 4},
|
||||
{6, 5, 3, 4, 1, 2},
|
||||
{3, 4, 5, 6, 1, 2},
|
||||
{5, 6, 4, 3, 1, 2},
|
||||
{4, 3, 6, 5, 1, 2},
|
||||
{5, 6, 3, 4, 2, 1},
|
||||
{4, 3, 5, 6, 2, 1},
|
||||
{6, 5, 4, 3, 2, 1},
|
||||
{3, 4, 6, 5, 2, 1},
|
||||
{4, 3, 1, 2, 5, 6},
|
||||
{6, 5, 1, 2, 4, 3},
|
||||
{3, 4, 1, 2, 6, 5},
|
||||
{5, 6, 1, 2, 3, 4},
|
||||
{3, 4, 2, 1, 5, 6},
|
||||
{5, 6, 2, 1, 4, 3},
|
||||
{4, 3, 2, 1, 6, 5},
|
||||
{6, 5, 2, 1, 3, 4},
|
||||
{2, 1, 4, 3, 5, 6},
|
||||
{2, 1, 6, 5, 4, 3},
|
||||
{2, 1, 3, 4, 6, 5},
|
||||
{2, 1, 5, 6, 3, 4},
|
||||
}
|
||||
|
||||
meshport.facedir_to_tile_rotations = {
|
||||
[0] =
|
||||
{0, 0, 0, 0, 0, 0},
|
||||
{3, 1, 0, 0, 0, 0},
|
||||
{2, 2, 0, 0, 0, 0},
|
||||
{1, 3, 0, 0, 0, 0},
|
||||
{0, 2, 3, 1, 2, 0},
|
||||
{0, 2, 3, 1, 1, 1},
|
||||
{0, 2, 3, 1, 0, 2},
|
||||
{0, 2, 3, 1, 3, 3},
|
||||
{2, 0, 1, 3, 2, 0},
|
||||
{2, 0, 1, 3, 3, 3},
|
||||
{2, 0, 1, 3, 0, 2},
|
||||
{2, 0, 1, 3, 1, 1},
|
||||
{3, 3, 3, 3, 1, 3},
|
||||
{3, 3, 2, 0, 1, 3},
|
||||
{3, 3, 1, 1, 1, 3},
|
||||
{3, 3, 0, 2, 1, 3},
|
||||
{1, 1, 1, 1, 3, 1},
|
||||
{1, 1, 2, 0, 3, 1},
|
||||
{1, 1, 3, 3, 3, 1},
|
||||
{1, 1, 0, 2, 3, 1},
|
||||
{2, 2, 2, 2, 2, 2},
|
||||
{3, 1, 2, 2, 2, 2},
|
||||
{0, 0, 2, 2, 2, 2},
|
||||
{1, 3, 2, 2, 2, 2},
|
||||
}
|
||||
|
||||
meshport.wallmounted_to_facedir = {[0] = 20, 0, 17, 15, 8, 6}
|
||||
|
||||
meshport.drawtype_aliases = {
|
||||
allfaces_optional = "allfaces",
|
||||
glasslike_framed_optional = "glasslike_framed",
|
||||
}
|
||||
|
||||
function meshport.print(name, level, s)
|
||||
local message
|
||||
|
||||
if level == "info" then
|
||||
message = minetest.colorize("#00EF00", s)
|
||||
elseif level == "warning" then
|
||||
message = minetest.colorize("#EFEF00", "Warning: " .. s)
|
||||
elseif level == "error" then
|
||||
message = minetest.colorize("#EF0000", "Error: " .. s)
|
||||
end
|
||||
|
||||
minetest.chat_send_player(name, "[meshport] " .. message)
|
||||
end
|
||||
|
||||
function meshport.find_or_insert(list, value)
|
||||
local idx = table.indexof(list, value)
|
||||
|
||||
-- If the element does not exist, create it.
|
||||
if idx < 0 then
|
||||
table.insert(list, value)
|
||||
idx = #list
|
||||
end
|
||||
|
||||
-- Return the index of the element.
|
||||
return idx
|
||||
end
|
||||
|
||||
function meshport.clean_vector(vec)
|
||||
-- Prevents an issue involving negative zero values, which are not handled properly by `string.format`.
|
||||
return {
|
||||
x = vec.x == 0 and 0 or vec.x,
|
||||
y = vec.y == 0 and 0 or vec.y,
|
||||
z = vec.z == 0 and 0 or vec.z,
|
||||
}
|
||||
end
|
||||
|
||||
function meshport.rotate_vector_by_facedir(vec, facedir)
|
||||
local v = vector.new(vec)
|
||||
local rotY = facedir % 4
|
||||
local rotSide = (facedir - rotY) / 4
|
||||
|
||||
-- Rotate the vector. Values of 0 for either `rotY` or `rotSide` do not change the vector.
|
||||
if rotY == 1 then
|
||||
v.x, v.z = v.z, -v.x -- 90 degrees clockwise
|
||||
elseif rotY == 2 then
|
||||
v.x, v.z = -v.x, -v.z -- 180 degrees clockwise
|
||||
elseif rotY == 3 then
|
||||
v.x, v.z = -v.z, v.x -- 270 degrees clockwise
|
||||
end
|
||||
|
||||
if rotSide == 1 then
|
||||
v.y, v.z = -v.z, v.y -- Facing Z+
|
||||
elseif rotSide == 2 then
|
||||
v.y, v.z = v.z, -v.y -- Facing Z-
|
||||
elseif rotSide == 3 then
|
||||
v.x, v.y = v.y, -v.x -- Facing X+
|
||||
elseif rotSide == 4 then
|
||||
v.x, v.y = -v.y, v.x -- Facing X-
|
||||
elseif rotSide == 5 then
|
||||
v.x, v.y = -v.x, -v.y -- Facing Y-
|
||||
end
|
||||
|
||||
return v
|
||||
end
|
||||
|
||||
function meshport.rotate_texture_coordinate(texCoord, rot)
|
||||
local vt = table.copy(texCoord)
|
||||
|
||||
-- Rotate the vector. Values of components range from 0 to 1, so adding 1 when inverting is necessary.
|
||||
if rot == 1 then
|
||||
vt.x, vt.y = vt.y, 1 - vt.x -- 90 degrees clockwise
|
||||
elseif rot == 2 then
|
||||
vt.x, vt.y = 1 - vt.x, 1 - vt.y -- 180 degrees clockwise
|
||||
elseif rot == 3 then
|
||||
vt.x, vt.y = 1 - vt.y, vt.x -- 270 degrees clockwise
|
||||
end
|
||||
|
||||
return vt
|
||||
end
|
||||
|
||||
function meshport.rotate_texture_coordinates(texCoords, rot)
|
||||
if rot == 0 then
|
||||
return texCoords
|
||||
end
|
||||
|
||||
local newTexCoords = {}
|
||||
|
||||
for _, texCoord in ipairs(texCoords) do
|
||||
table.insert(newTexCoords, meshport.rotate_texture_coordinate(texCoord, rot))
|
||||
end
|
||||
|
||||
return newTexCoords
|
||||
end
|
||||
|
||||
function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scale)
|
||||
-- Get the offset of the tile relative to the lower left corner of the texture.
|
||||
local texPos = {}
|
||||
|
||||
if sideIdx == 1 then
|
||||
texPos.x = pos.x % 16 % scale
|
||||
texPos.y = pos.z % 16 % scale
|
||||
elseif sideIdx == 2 then
|
||||
texPos.x = pos.x % 16 % scale
|
||||
texPos.y = scale - pos.z % 16 % scale - 1
|
||||
elseif sideIdx == 3 then
|
||||
texPos.x = pos.z % 16 % scale
|
||||
texPos.y = pos.y % 16 % scale
|
||||
elseif sideIdx == 4 then
|
||||
texPos.x = scale - pos.z % 16 % scale - 1
|
||||
texPos.y = pos.y % 16 % scale
|
||||
elseif sideIdx == 5 then
|
||||
texPos.x = scale - pos.x % 16 % scale - 1
|
||||
texPos.y = pos.y % 16 % scale
|
||||
elseif sideIdx == 6 then
|
||||
texPos.x = pos.x % 16 % scale
|
||||
texPos.y = pos.y % 16 % scale
|
||||
end
|
||||
|
||||
-- Scale and move the texture coordinates.
|
||||
local newTexCoords = {}
|
||||
|
||||
for _, texCoord in ipairs(texCoords) do
|
||||
table.insert(newTexCoords, {
|
||||
x = (texCoord.x + texPos.x) / scale,
|
||||
y = (texCoord.y + texPos.y) / scale,
|
||||
})
|
||||
end
|
||||
|
||||
return newTexCoords
|
||||
end
|
||||
|
||||
function meshport.prepare_cuboid_face(face, tiles, pos, facedir, sideIdx)
|
||||
-- If the tile index has not been set manually, assign a tile to the face based on the facedir value.
|
||||
face.tile_idx = face.tile_idx or meshport.facedir_to_tile_indices[facedir][sideIdx]
|
||||
local tile = meshport.get_tile(tiles, face.tile_idx)
|
||||
|
||||
if tile.align_style == "world" or tile.align_style == "user" then
|
||||
-- For scaled, world-aligned tiles, scale and reposition the texture coordinates as needed.
|
||||
if tile.scale and tile.scale ~= 1 then
|
||||
face.tex_coords = meshport.scale_global_texture_coordinates(face.tex_coords, pos, sideIdx, tile.scale)
|
||||
end
|
||||
else
|
||||
-- If the tile isn't world-aligned, rotate it according to the facedir.
|
||||
face.tex_coords = meshport.rotate_texture_coordinates(face.tex_coords,
|
||||
meshport.facedir_to_tile_rotations[facedir][sideIdx])
|
||||
end
|
||||
|
||||
return face
|
||||
end
|
||||
|
||||
function meshport.get_def_from_id(contentId)
|
||||
return minetest.registered_nodes[minetest.get_name_from_content_id(contentId)] or {}
|
||||
end
|
||||
|
||||
function meshport.get_aliased_drawtype(drawtype)
|
||||
return meshport.drawtype_aliases[drawtype or ""] or drawtype
|
||||
end
|
||||
|
||||
function meshport.get_facedir(param2, type)
|
||||
if type == "facedir" or type == "colorfacedir" then
|
||||
-- For colorfacedir, only the first 5 bits are needed.
|
||||
return param2 % 32
|
||||
elseif type == "wallmounted" or type == "colorwallmounted" then
|
||||
-- For colorwallmounted, only the first 3 bits are needed. If the wallmounted direction is invalid, return 0.
|
||||
return meshport.wallmounted_to_facedir[param2 % 8] or 0
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.get_node_neighbors(array, area, idx)
|
||||
-- Get the node's absolute position from the flat array index.
|
||||
local pos = area:position(idx)
|
||||
local neighbors = {}
|
||||
|
||||
-- Get the content/param2 value for each neighboring node.
|
||||
for i = 1, 6 do
|
||||
neighbors[i] = array[area:indexp(vector.add(pos, meshport.neighbor_dirs[i]))]
|
||||
end
|
||||
|
||||
return neighbors
|
||||
end
|
||||
|
||||
function meshport.node_connects_to(nodeName, connectsTo)
|
||||
-- If `connectsTo` is a string or nil, turn it into a table for iteration.
|
||||
if type(connectsTo) ~= "table" then
|
||||
connectsTo = {connectsTo}
|
||||
end
|
||||
|
||||
for _, connectName in ipairs(connectsTo) do
|
||||
if connectName == nodeName
|
||||
or string.sub(connectName, 1, 6) == "group:"
|
||||
and minetest.get_item_group(nodeName, string.sub(connectName, 7)) ~= 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function meshport.get_tile(tiles, n)
|
||||
if type(tiles) == "table" then
|
||||
return tiles[n] or tiles[#tiles]
|
||||
else
|
||||
return "unknown"
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.get_asset_paths(assetFolderName, extension)
|
||||
local modAssetPath
|
||||
local assets = {}
|
||||
|
||||
-- Iterate through each enabled mod.
|
||||
for _, modName in ipairs(minetest.get_modnames()) do
|
||||
modAssetPath = minetest.get_modpath(modName) .. DIR_DELIM .. assetFolderName
|
||||
|
||||
-- Iterate through all the files in the requested folder of the mod.
|
||||
for _, fileName in ipairs(minetest.get_dir_list(modAssetPath, false)) do
|
||||
-- Add files to the table. If an extendion is specified, only add files with that extension.
|
||||
if not extension or string.lower(string.sub(fileName, -string.len(extension))) == extension then
|
||||
assets[fileName] = modAssetPath .. DIR_DELIM .. fileName
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return assets
|
||||
end
|
68
init.lua
Normal file
68
init.lua
Normal file
@ -0,0 +1,68 @@
|
||||
meshport = {
|
||||
p1 = {},
|
||||
p2 = {},
|
||||
}
|
||||
|
||||
modpath = minetest.get_modpath("meshport")
|
||||
dofile(modpath .. "/helpers.lua")
|
||||
dofile(modpath .. "/api.lua")
|
||||
dofile(modpath .. "/parse_obj.lua")
|
||||
dofile(modpath .. "/nodebox.lua")
|
||||
dofile(modpath .. "/export.lua")
|
||||
|
||||
minetest.register_privilege("meshport", "Can save meshes with meshport.")
|
||||
|
||||
minetest.register_on_leaveplayer(function(player, timed_out)
|
||||
local name = player:get_player_name()
|
||||
meshport.p1[name] = nil
|
||||
meshport.p2[name] = nil
|
||||
end)
|
||||
|
||||
for i = 1, 2 do
|
||||
minetest.register_chatcommand("mesh" .. i, {
|
||||
params = "[pos]",
|
||||
description = string.format(
|
||||
"Set position %i for meshport. Player's position is used if no other position is specified.", i),
|
||||
privs = {meshport = true},
|
||||
|
||||
func = function(name, param)
|
||||
local pos
|
||||
|
||||
if param == "" then
|
||||
pos = minetest.get_player_by_name(name):get_pos()
|
||||
else
|
||||
pos = minetest.string_to_pos(param)
|
||||
end
|
||||
|
||||
if not pos then
|
||||
meshport.print(name, "error", "Not a valid position.")
|
||||
return
|
||||
end
|
||||
|
||||
pos = vector.round(pos)
|
||||
|
||||
if i == 1 then
|
||||
meshport.p1[name] = pos
|
||||
elseif i == 2 then
|
||||
meshport.p2[name] = pos
|
||||
end
|
||||
|
||||
meshport.print(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
minetest.register_chatcommand("meshport", {
|
||||
params = "",
|
||||
description = "Save a mesh of the selected area.",
|
||||
privs = {meshport = true},
|
||||
|
||||
func = function(name, param)
|
||||
if not meshport.p1[name] or not meshport.p2[name] then
|
||||
meshport.print(name, "error", "No area selected. Use /mesh1 and /mesh2 to select an area.")
|
||||
return
|
||||
end
|
||||
|
||||
meshport.create_mesh(name, meshport.p1[name], meshport.p2[name])
|
||||
end,
|
||||
})
|
2
mod.conf
Normal file
2
mod.conf
Normal file
@ -0,0 +1,2 @@
|
||||
name = meshport
|
||||
descrption = Easily export areas in Minetest to meshes for 3D rendering.
|
194
nodebox.lua
Normal file
194
nodebox.lua
Normal file
@ -0,0 +1,194 @@
|
||||
meshport.side_box_names = {
|
||||
"top", -- Y+
|
||||
"bottom", -- Y-
|
||||
"right", -- X+
|
||||
"left", -- X-
|
||||
"back", -- Z+
|
||||
"front", -- Z-
|
||||
}
|
||||
|
||||
function meshport.sort_box(box)
|
||||
return {
|
||||
math.min(box[1], box[4]),
|
||||
math.min(box[2], box[5]),
|
||||
math.min(box[3], box[6]),
|
||||
math.max(box[1], box[4]),
|
||||
math.max(box[2], box[5]),
|
||||
math.max(box[3], box[6]),
|
||||
}
|
||||
end
|
||||
|
||||
meshport.Boxes = {}
|
||||
|
||||
function meshport.Boxes:new(boxes)
|
||||
local o = {}
|
||||
|
||||
if type(boxes) ~= "table" or type(boxes[1]) == "number" then
|
||||
o.boxes = {boxes}
|
||||
else
|
||||
o.boxes = boxes
|
||||
end
|
||||
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function meshport.Boxes:insert_all(boxes)
|
||||
for _, box in ipairs(boxes.boxes) do
|
||||
table.insert(self.boxes, table.copy(box))
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.Boxes:transform(func)
|
||||
local a, b
|
||||
|
||||
for i, box in ipairs(self.boxes) do
|
||||
a = func(vector.new(box[1], box[2], box[3]))
|
||||
b = func(vector.new(box[4], box[5], box[6]))
|
||||
|
||||
self.boxes[i] = {a.x, a.y, a.z, b.x, b.y, b.z}
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.Boxes:rotate_by_facedir(facedir)
|
||||
local a, b
|
||||
|
||||
for i, box in ipairs(self.boxes) do
|
||||
a = meshport.rotate_vector_by_facedir(vector.new(box[1], box[2], box[3]), facedir)
|
||||
b = meshport.rotate_vector_by_facedir(vector.new(box[4], box[5], box[6]), facedir)
|
||||
|
||||
self.boxes[i] = {a.x, a.y, a.z, b.x, b.y, b.z}
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.Boxes:get_leveled(level)
|
||||
local newBoxes = meshport.Boxes:new(table.copy(self.boxes))
|
||||
|
||||
for i, box in ipairs(newBoxes.boxes) do
|
||||
box = meshport.sort_box(box)
|
||||
box[5] = level / 64 - 0.5
|
||||
newBoxes.boxes[i] = box
|
||||
end
|
||||
|
||||
return newBoxes
|
||||
end
|
||||
|
||||
function meshport.Boxes:to_faces(nodeTiles, pos, facedir)
|
||||
local faces = meshport.Faces:new()
|
||||
|
||||
for _, b in ipairs(self.boxes) do
|
||||
b = meshport.sort_box(b)
|
||||
|
||||
local sideFaces = {
|
||||
{{x = b[4], y = b[5], z = b[3]}, {x = b[4], y = b[5], z = b[6]}, {x = b[1], y = b[5], z = b[6]}, {x = b[1], y = b[5], z = b[3]}}, -- Y+
|
||||
{{x = b[4], y = b[2], z = b[6]}, {x = b[4], y = b[2], z = b[3]}, {x = b[1], y = b[2], z = b[3]}, {x = b[1], y = b[2], z = b[6]}}, -- Y-
|
||||
{{x = b[4], y = b[2], z = b[6]}, {x = b[4], y = b[5], z = b[6]}, {x = b[4], y = b[5], z = b[3]}, {x = b[4], y = b[2], z = b[3]}}, -- X+
|
||||
{{x = b[1], y = b[2], z = b[3]}, {x = b[1], y = b[5], z = b[3]}, {x = b[1], y = b[5], z = b[6]}, {x = b[1], y = b[2], z = b[6]}}, -- X-
|
||||
{{x = b[1], y = b[2], z = b[6]}, {x = b[1], y = b[5], z = b[6]}, {x = b[4], y = b[5], z = b[6]}, {x = b[4], y = b[2], z = b[6]}}, -- Z+
|
||||
{{x = b[4], y = b[2], z = b[3]}, {x = b[4], y = b[5], z = b[3]}, {x = b[1], y = b[5], z = b[3]}, {x = b[1], y = b[2], z = b[3]}}, -- Z-
|
||||
}
|
||||
|
||||
local sideTexCoords = {
|
||||
{{x = b[4], y = b[3]}, {x = b[4], y = b[6]}, {x = b[1], y = b[6]}, {x = b[1], y = b[3]}}, -- Y+
|
||||
{{x = b[4], y =-b[6]}, {x = b[4], y =-b[3]}, {x = b[1], y =-b[3]}, {x = b[1], y =-b[6]}}, -- Y-
|
||||
{{x = b[6], y = b[2]}, {x = b[6], y = b[5]}, {x = b[3], y = b[5]}, {x = b[3], y = b[2]}}, -- X+
|
||||
{{x =-b[3], y = b[2]}, {x =-b[3], y = b[5]}, {x =-b[6], y = b[5]}, {x =-b[6], y = b[2]}}, -- X-
|
||||
{{x =-b[1], y = b[2]}, {x =-b[1], y = b[5]}, {x =-b[4], y = b[5]}, {x =-b[4], y = b[2]}}, -- Z+
|
||||
{{x = b[4], y = b[2]}, {x = b[4], y = b[5]}, {x = b[1], y = b[5]}, {x = b[1], y = b[2]}}, -- Z-
|
||||
}
|
||||
|
||||
local tileIdx, vertNorm
|
||||
|
||||
for i = 1, 6 do
|
||||
-- Fix offset texture coordinates.
|
||||
for v = 1, 4 do
|
||||
sideTexCoords[i][v] = {x = sideTexCoords[i][v].x + 0.5, y = sideTexCoords[i][v].y + 0.5}
|
||||
end
|
||||
|
||||
vertNorm = meshport.neighbor_dirs[i]
|
||||
|
||||
faces:insert_face(meshport.prepare_cuboid_face({
|
||||
verts = sideFaces[i],
|
||||
tex_coords = sideTexCoords[i],
|
||||
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
|
||||
}, nodeTiles, pos, facedir, i))
|
||||
end
|
||||
end
|
||||
|
||||
return faces
|
||||
end
|
||||
|
||||
function meshport.prepare_nodebox(nodebox)
|
||||
local prepNodebox = {}
|
||||
prepNodebox.type = nodebox.type
|
||||
|
||||
if nodebox.type == "regular" then
|
||||
prepNodebox.fixed = meshport.Boxes:new({-0.5, -0.5, -0.5, 0.5, 0.5, 0.5})
|
||||
elseif nodebox.type == "fixed" or nodebox.type == "leveled" then
|
||||
prepNodebox.fixed = meshport.Boxes:new(nodebox.fixed)
|
||||
elseif nodebox.type == "connected" then
|
||||
prepNodebox.fixed = meshport.Boxes:new(nodebox.fixed)
|
||||
prepNodebox.connected = {}
|
||||
prepNodebox.disconnected = {}
|
||||
|
||||
for i, name in ipairs(meshport.side_box_names) do
|
||||
prepNodebox.connected[i] = meshport.Boxes:new(nodebox["connect_" .. name])
|
||||
prepNodebox.disconnected[i] = meshport.Boxes:new(nodebox["disconnected_" .. name])
|
||||
end
|
||||
|
||||
prepNodebox.disconnected_all = meshport.Boxes:new(nodebox.disconnected)
|
||||
prepNodebox.disconnected_sides = meshport.Boxes:new(nodebox.disconnected_sides)
|
||||
elseif nodebox.type == "wallmounted" then
|
||||
prepNodebox.wall_bottom = meshport.Boxes:new(nodebox.wall_bottom)
|
||||
prepNodebox.wall_top = meshport.Boxes:new(nodebox.wall_top)
|
||||
prepNodebox.wall_side = meshport.Boxes:new(nodebox.wall_side)
|
||||
|
||||
-- Rotate the boxes so they are in the correct orientation after rotation by facedir.
|
||||
prepNodebox.wall_top:transform(function(v) return {x = -v.x, y = -v.y, z = v.z} end)
|
||||
prepNodebox.wall_side:transform(function(v) return {x = -v.z, y = v.x, z = v.y} end)
|
||||
end
|
||||
|
||||
return prepNodebox
|
||||
end
|
||||
|
||||
function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors)
|
||||
local boxes = meshport.Boxes:new()
|
||||
|
||||
if prepNodebox.fixed then
|
||||
if prepNodebox.type == "leveled" then
|
||||
boxes:insert_all(prepNodebox.fixed:get_leveled(
|
||||
nodeDef.paramtype2 == "leveled" and param2 or nodeDef.leveled or 0))
|
||||
else
|
||||
boxes:insert_all(prepNodebox.fixed)
|
||||
end
|
||||
end
|
||||
|
||||
if prepNodebox.type == "connected" then
|
||||
local neighborName
|
||||
|
||||
for i = 1, 6 do
|
||||
neighborName = minetest.get_name_from_content_id(neighbors[i])
|
||||
|
||||
if meshport.node_connects_to(neighborName, nodeDef.connects_to) then
|
||||
boxes:insert_all(prepNodebox.connected[i])
|
||||
else
|
||||
boxes:insert_all(prepNodebox.disconnected[i])
|
||||
end
|
||||
end
|
||||
elseif prepNodebox.type == "wallmounted" then
|
||||
if nodeDef.paramtype2 == "wallmounted" or nodeDef.paramtype2 == "colorwallmounted" then
|
||||
if facedir == 20 then
|
||||
boxes:insert_all(prepNodebox.wall_top)
|
||||
elseif facedir == 0 then
|
||||
boxes:insert_all(prepNodebox.wall_bottom)
|
||||
else
|
||||
boxes:insert_all(prepNodebox.wall_side)
|
||||
end
|
||||
else
|
||||
boxes:insert_all(prepNodebox.wall_top)
|
||||
end
|
||||
end
|
||||
|
||||
return boxes
|
||||
end
|
93
parse_obj.lua
Normal file
93
parse_obj.lua
Normal file
@ -0,0 +1,93 @@
|
||||
function meshport.parse_vector_element(elementStr)
|
||||
local elementType
|
||||
local vec = {}
|
||||
|
||||
-- Get the element type and vector. `vec.z` will be left `nil` for two-dimensional vectors.
|
||||
elementType, vec.x, vec.y, vec.z = string.match(elementStr, "^(%a+)%s([%d%.%-]+)%s([%d%.%-]+)%s?([%d%.%-]*)")
|
||||
|
||||
for k, v in pairs(vec) do
|
||||
vec[k] = tonumber(v)
|
||||
end
|
||||
|
||||
-- Return the element type and value.
|
||||
if elementType == "v" then
|
||||
-- Invert X axis to match the Minetest coordinate system.
|
||||
vec.x = -vec.x
|
||||
return "verts", vec
|
||||
elseif elementType == "vt" then
|
||||
return "tex_coords", vec
|
||||
elseif elementType == "vn" then
|
||||
vec.x = -vec.x
|
||||
return "vert_norms", vec
|
||||
end
|
||||
end
|
||||
|
||||
function meshport.parse_face_element(elements, elementStr)
|
||||
-- Split the face element into strings containing the indices of elements associated with each vertex.
|
||||
local vertStrs = string.split(string.match(elementStr, "^f%s([%d/%s]+)"), " ")
|
||||
local elementIndices
|
||||
|
||||
local face = {
|
||||
verts = {},
|
||||
tex_coords = {},
|
||||
vert_norms = {},
|
||||
}
|
||||
|
||||
for i, vertStr in ipairs(vertStrs) do
|
||||
-- Split the string into a table of indices for position, texture coordinate, and/or vertex normal elements.
|
||||
elementIndices = string.split(vertStr, "/", true)
|
||||
|
||||
for k, v in pairs(elementIndices) do
|
||||
elementIndices[k] = tonumber(v)
|
||||
end
|
||||
|
||||
-- Set the position, texture coordinate, and vertex normal of the face. `or 0` prevents a nil index error.
|
||||
face.verts[i] = elements.verts[elementIndices[1] or 0]
|
||||
face.tex_coords[i] = elements.tex_coords[elementIndices[2] or 0]
|
||||
face.vert_norms[i] = elements.vert_norms[elementIndices[3] or 0]
|
||||
end
|
||||
|
||||
return face
|
||||
end
|
||||
|
||||
function meshport.parse_obj(path)
|
||||
local faces = meshport.Faces:new()
|
||||
local file = io.open(path, "r")
|
||||
|
||||
local elements = {
|
||||
verts = {},
|
||||
tex_coords = {},
|
||||
vert_norms = {},
|
||||
}
|
||||
|
||||
local groups = {}
|
||||
local curGroup
|
||||
local elementType
|
||||
|
||||
for line in file:lines() do
|
||||
elementType = string.sub(line, 1, 1)
|
||||
|
||||
if elementType == "v" then
|
||||
-- Parse the vector element. Used for "v", "vt", and "vn".
|
||||
local type, value = meshport.parse_vector_element(line)
|
||||
table.insert(elements[type], value)
|
||||
elseif elementType == "f" then
|
||||
-- If the face is not part of any group, use the placeholder group `0`.
|
||||
if not curGroup then
|
||||
table.insert(groups, 0)
|
||||
curGroup = table.indexof(groups, 0)
|
||||
end
|
||||
|
||||
-- Parse the face element.
|
||||
local face = meshport.parse_face_element(elements, line)
|
||||
-- Assign materials according to the group.
|
||||
face.tile_idx = curGroup
|
||||
faces:insert_face(face)
|
||||
elseif elementType == "g" then
|
||||
-- If this group has not been used yet, then add it to the list.
|
||||
curGroup = meshport.find_or_insert(groups, string.match(line, "^g%s(.+)"))
|
||||
end
|
||||
end
|
||||
|
||||
return faces
|
||||
end
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 565 KiB |
Loading…
x
Reference in New Issue
Block a user