Add numerous features, refactor extensively

master
random-geek 2021-07-05 01:05:06 -07:00
parent 67d08f760a
commit ff62e98edc
9 changed files with 1518 additions and 490 deletions

View File

@ -2,14 +2,17 @@ unused_args = false
allow_defined_top = true
max_line_length = 999
globals = {
"meshport",
}
read_globals = {
string = {fields = {"split", "trim"}},
table = {fields = {"copy", "getn", "indexof"}},
"minetest", "DIR_DELIM",
"vector", "VoxelArea",
}
ignore = {
"vertList",
"minetest",
"DIR_DELIM",
"PseudoRandom",
"vector",
"VoxelArea",
}

View File

@ -5,52 +5,90 @@
![screenshot](screenshot.png)
Meshport is a mod which allows easy exporting of scenes from Minetest to `.obj` files, complete with materials and textures. These models can be imported directly into Blender or another 3D program for rendering and animation.
Meshport is a mod which allows easy exporting of scenes from Minetest to `.obj`
files, 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.
This mod is still in the beta phase; certain texturing features and node
drawtypes are not yet supported.
## Usage
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported, then use `/meshport [filename]` to export the mesh (filename is optional). The saved `.obj` and `.mtl` files will be located in the `meshport` folder of the world directory, within a subfolder.
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported,
then use `/meshport [filename]` to export the mesh (filename is optional).
Folders containing exported meshes, including `.obj` and `.mtl` files, are
saved in the `meshport` folder of the world directory.
### 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.
Once the model is exported, you can import the `.obj` file into Blender with
default settings. Make sure "Image Search" in the import settings is selected
to ensure textures from the `.mtl` file are imported as well.
#### Fixing materials
Blender's packaged material assigned to OBJ textures are not effective or easy to use. By default, textures will also appear blurry and lack alpha. The `materials.py` script is included in the mod to simplify the materials, change interpolation, and add transparency. Open the script in Blender's text editor and run the script with the mesh selected.
Upon importing the file, Blender assigns basic materials to the model which are
inaccurate and not very usable. By default, these materials appear blurry and
lack transparency. The `materials.py` script is included in the mod to fix
these issues. Open the script in Blender's text editor and run the script with
the mesh selected.
#### Fixing vertex normals
Meshport does not handle texture modifiers or node coloring, so some materials
will probably still need to be fixed by hand after running the script.
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.
#### Other fixes
Additional tip: Use an HDRI sky texture (such as one from [here](https://hdrihaven.com)) for awesome-looking renders. ;)
Some mesh nodes may not have any vertex normals, which can lead to lighting
problems. To fix this, what I have found to work is to select the all the
problematic nodes (either manually or by selecting by material in edit mode),
mark the selected edges as sharp, and average the normals by face area.
Some animated textures may also appear incorrect. Meshport tries to scale
texture coordinates of animated textures to fit within one frame, but some
nodes (especially flowing liquids) can exceed this boundary. If this is an
issue, switch to a non-animated texture and scale up the affected UV maps to
match the new texture.
Additional tip: Use an HDRI sky texture (such as one from [here][1]) for
awesome-looking renders. ;)
[1]: https://hdrihaven.com
## Supported features
At the moment, only the following node drawtypes are supported:
The following node drawtypes are currently supported:
- Cubic drawtypes, including `normal`, `glasslike`, `allfaces`, and their variants (see below)
- Cubic drawtypes, including `normal`, `allfaces`, `glasslike`, and their
variants (see below)
- `glasslike_framed`
- `liquid` and `flowingliquid`
- `nodebox`
- `mesh` (only `.obj` meshes are exported)
- `plantlike` and `plantlike_rooted`
Many special rendering features are not yet supported.
Meshport also supports many of Minetest's relevant features, including:
### A note on cubic nodes
- Most `paramtype2`s (note that color is ignored for colored types)
- `visual_scale`
- World-aligned textures
- Animated textures (only one frame is used)
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:
Some special rendering features are unsupported, including texture modifiers,
overlay textures, and node coloring.
| Priority level | Drawtypes |
|----------------|----------------------------------------------------|
| 4 | `normal` |
| 3 | `glasslike` |
| 2 | `glasslike_framed` and `glasslike_framed_optional` |
| 1 | `allfaces` and `allfaces_optional` |
| 0 | All other nodes |
### Notes on cubic 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.
Drawtypes `allfaces_optional` and `glasslike_framed_optional` are output the
same as `allfaces` and `glasslike`, respectively.
Due to the differences between Minetest's rendering engine and 3D programs such
as Blender, it is impossible to exactly replicate how certain cubic nodes are
rendered in Minetest. Instead, Meshport aims for a compromise between accuracy
and simplicity of geometry. In certain cases where two cubic nodes are
touching, one face may be offset slightly to avoid duplicate faces while still
allowing both faces to be visible.
## License
All code is licensed under the GNU LGPL v3.0.
Meshport is licensed under the GNU LGPL v3.0.

189
api.lua
View File

@ -1,189 +0,0 @@
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

View File

@ -1,67 +1,564 @@
meshport.nodebox_cache = {}
meshport.mesh_cache = {}
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
Minetest: Copyright (C) 2010-2021 celeron55, Perttu Ahola <celeron55@gmail.com>
meshport.cube_face_priority = {
This file is part of Meshport.
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
-- Much of the mesh generation code in this file is derived from Minetest's
-- MapblockMeshGenerator class. See minetest/src/client/content_mapblock.cpp.
--[[
THE CUBIC NODE PRIORITY SYSTEM
For each face on each cubic node, Meshport decides whether or not to draw
that face based on a combination of the current node's drawtype (show in
the top row of the table below), the neighboring node's drawtype (shown in
the leftmost column), the direction of the face, and both nodes'
visual_scale.
A "YES" combination means the face is drawn, "no" means the face is not
drawn, and "Offset" means the face is drawn, but slightly inset to avoid
duplication of faces.
| This node => | allfaces (1) | glasslike | liquid | normal (2) |
|--------------:|:------------:|:---------:|:-------:|:----------:|
| air/non-cubic | YES | YES | YES (3) | YES |
| allfaces | (4) | YES | YES | YES |
| glasslike | Offset | (5) | YES | YES |
| liquid | Offset | Offset | no | YES |
| normal (2) | no | no | no | no |
1. Allfaces faces are always drawn if `visual_scale` is not 1.
2. The base of `plantlike_rooted` is treated as a normal node.
3. Liquid faces are not drawn bordering a corresponding flowing liquid.
4. Only drawn if facing X+, Y+, or Z+, or if either node's `visual_scale`
is not 1.
5. Only drawn if the nodes are different. X-, Z-, and Y- faces are offset.
]]
local CUBIC_FACE_PRIORITY = {
allfaces = 1,
glasslike_framed = 2,
glasslike = 3,
glasslike = 2,
liquid = 3,
normal = 4,
plantlike_rooted = 4, -- base of plantlike_rooted is equivalent to `normal`.
}
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 vec = vector.new -- Makes defining tables of vertices a little less painful.
local texCoords = {{x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}}
local CUBIC_SIDE_FACES = {
{vec(-0.5, 0.5, -0.5), vec( 0.5, 0.5, -0.5), vec( 0.5, 0.5, 0.5), vec(-0.5, 0.5, 0.5)}, -- Y+
{vec(-0.5, -0.5, 0.5), vec( 0.5, -0.5, 0.5), vec( 0.5, -0.5, -0.5), vec(-0.5, -0.5, -0.5)}, -- Y-
{vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5, 0.5), vec( 0.5, 0.5, 0.5), vec( 0.5, 0.5, -0.5)}, -- X+
{vec(-0.5, -0.5, 0.5), vec(-0.5, -0.5, -0.5), vec(-0.5, 0.5, -0.5), vec(-0.5, 0.5, 0.5)}, -- X-
{vec( 0.5, -0.5, 0.5), vec(-0.5, -0.5, 0.5), vec(-0.5, 0.5, 0.5), vec( 0.5, 0.5, 0.5)}, -- Z+
{vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5, 0.5, -0.5), vec(-0.5, 0.5, -0.5)}, -- Z-
}
-- For normal, plantlike_rooted, and liquid drawtypes
local function create_cubic_node(pos, content, param2, nodeDef, drawtype, neighbors)
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
-- If the current node is a liquid, get the flowing version of it.
local flowingLiquid = drawtype == "liquid"
and meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing) or nil
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)
local drawFace
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]
if neighbors[i] == minetest.CONTENT_AIR then
drawFace = true
elseif neighbors[i] == minetest.CONTENT_IGNORE
-- Don't draw faces between identical nodes
or neighbors[i] == content
-- Don't draw liquid faces bordering a corresponding flowing liquid
or neighbors[i] == flowingLiquid then
drawFace = false
else
local neighborDef = meshport.get_def_from_id(neighbors[i])
local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
drawFace = selfPriority > (CUBIC_FACE_PRIORITY[neighborDrawtype] or 0)
end
if drawFace then
local norm = 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))
verts = table.copy(CUBIC_SIDE_FACES[i]),
vert_norms = {norm, norm, norm, norm},
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
}, nodeDef.tiles, pos, facedir, i))
end
end
return faces
end
function meshport.create_nodebox_node(nodeName, pos, facedir, param2, neighbors)
-- For allfaces and glasslike drawtypes, and equivalent variants.
local function create_special_cubic_node(pos, content, nodeDef, drawtype, neighbors)
local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
local isAllfaces = drawtype == "allfaces"
local allfacesScale = isAllfaces and nodeDef.visual_scale or 1
local faces = meshport.Faces:new()
for i = 1, 6 do
local drawFace
local inset = false
if allfacesScale ~= 1 or neighbors[i] == minetest.CONTENT_AIR or neighbors[i] == minetest.CONTENT_IGNORE then
drawFace = true
elseif neighbors[i] == content then
drawFace = isAllfaces and i % 2 == 1
else
local neighborDef = meshport.get_def_from_id(neighbors[i])
local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
local neighborPriority = CUBIC_FACE_PRIORITY[neighborDrawtype] or 0
if neighborPriority < selfPriority then
drawFace = true
elseif neighborPriority >= 4 then
-- Don't draw faces bordering normal nodes.
drawFace = false
elseif neighborPriority > selfPriority then
drawFace = true
inset = true
elseif isAllfaces then -- neighborPriority == selfPriority
drawFace = i % 2 == 1 or neighborDef.visual_scale ~= 1
else -- neighborPriority == selfPriority
drawFace = true
inset = i % 2 == 0
end
end
if drawFace then
local verts = table.copy(CUBIC_SIDE_FACES[i])
if inset then
local offset = vector.multiply(meshport.NEIGHBOR_DIRS[i], -0.003)
for j, vert in ipairs(verts) do
verts[j] = vector.add(vert, offset)
end
end
local norm = meshport.NEIGHBOR_DIRS[i]
faces:insert_face(meshport.prepare_cuboid_face({
verts = verts,
vert_norms = {norm, norm, norm, norm},
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
tile_idx = 1, -- Only the first tile is used.
}, nodeDef.tiles, pos, 0, i))
end
end
faces:scale(allfacesScale)
return faces
end
local GLASSLIKE_FRAMED_CONSTANTS = (function()
local a = 0.5
local g = 0.5 - 0.003
local b = 0.876 * 0.5
return {
G = g,
B = b,
FRAME_EDGES = {
{ b, b, -a, a, a, a}, -- Y+ / X+
{-a, b, -a, -b, a, a}, -- Y+ / X-
{ b, -a, -a, a, -b, a}, -- Y- / X+
{-a, -a, -a, -b, -b, a}, -- Y- / X-
{ b, -a, b, a, a, a}, -- X+ / Z+
{ b, -a, -a, a, a, -b}, -- X+ / Z-
{-a, -a, b, -b, a, a}, -- X- / Z+
{-a, -a, -a, -b, a, -b}, -- X- / Z-
{-a, b, b, a, a, a}, -- Z+ / Y+
{-a, -a, b, a, -b, a}, -- Z+ / Y-
{-a, b, -a, a, a, -b}, -- Z- / Y+
{-a, -a, -a, a, -b, -b}, -- Z- / Y-
},
GLASS_FACES = {
{vec(-a, g, -a), vec( a, g, -a), vec( a, g, a), vec(-a, g, a)}, -- Y+
{vec(-a, -g, a), vec( a, -g, a), vec( a, -g, -a), vec(-a, -g, -a)}, -- Y-
{vec( g, -a, -a), vec( g, -a, a), vec( g, a, a), vec( g, a, -a)}, -- X+
{vec(-g, -a, a), vec(-g, -a, -a), vec(-g, a, -a), vec(-g, a, a)}, -- X-
{vec( a, -a, g), vec(-a, -a, g), vec(-a, a, g), vec( a, a, g)}, -- Z+
{vec(-a, -a, -g), vec( a, -a, -g), vec( a, a, -g), vec(-a, a, -g)}, -- Z-
},
EDGE_NEIGHBORS = {
{1, 3, 8}, {1, 4, 7}, {2, 3, 16}, {2, 4, 15},
{3, 5, 12}, {3, 6, 14}, {4, 5, 11}, {4, 6, 13},
{5, 1, 9}, {5, 2, 17}, {6, 1, 10}, {6, 2, 18},
},
}
end)()
local function create_glasslike_framed_node(pos, param2, nodeDef, area, vContent)
local idx = area:indexp(pos)
local llParam2 = nodeDef.paramtype2 == "glasslikeliquidlevel" and param2 or 0
local hMerge = llParam2 < 128 -- !(param2 & 128)
local vMerge = llParam2 % 128 < 64 -- !(param2 & 64)
local intLevel = llParam2 % 64
-- Localize constants
local G, B, FRAME_EDGES, GLASS_FACES, EDGE_NEIGHBORS = (function(c)
return c.G, c.B, c.FRAME_EDGES, c.GLASS_FACES, c.EDGE_NEIGHBORS
end)(GLASSLIKE_FRAMED_CONSTANTS)
local neighbors = {
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false
}
if hMerge or vMerge then
for i = 1, 18 do
local dir = meshport.NEIGHBOR_DIRS[i]
if (hMerge or (dir.x == 0 and dir.z == 0)) and (vMerge or dir.y == 0) then
local nIdx = area:indexp(vector.add(pos, dir))
neighbors[i] = vContent[nIdx] == vContent[idx]
end
end
end
local boxes = meshport.Boxes:new()
for i = 1, 12 do
local edgeVisible
local touching = EDGE_NEIGHBORS[i]
if neighbors[touching[3]] then
edgeVisible = not (neighbors[touching[1]] and neighbors[touching[2]])
else
edgeVisible = neighbors[touching[1]] == neighbors[touching[2]]
end
if edgeVisible then
boxes:insert_box(FRAME_EDGES[i])
end
end
local faces = boxes:to_faces(nodeDef, pos, 0, 1)
for i = 1, 6 do
if not neighbors[i] then
local norm = meshport.NEIGHBOR_DIRS[i]
faces:insert_face({
verts = table.copy(GLASS_FACES[i]),
vert_norms = {norm, norm, norm, norm},
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
tile_idx = 2,
})
end
end
if intLevel > 0 and nodeDef.special_tiles and nodeDef.special_tiles[1] then
local level = intLevel / 63 * 2 - 1
local liquidBoxes = meshport.Boxes:new()
liquidBoxes:insert_box({
-(neighbors[4] and G or B),
-(neighbors[2] and G or B),
-(neighbors[6] and G or B),
(neighbors[3] and G or B),
(neighbors[1] and G or B) * level,
(neighbors[5] and G or B)
})
faces:insert_all(liquidBoxes:to_faces(nodeDef, pos, 0, 1, true))
end
return faces
end
local FLOWING_LIQUID_CONSTANTS = {
SIDE_DIRS = {vec(1, 0, 0), vec(-1, 0, 0), vec(0, 0, 1), vec(0, 0, -1)},
SIDE_CORNERS = {
{{x = 1, z = 1}, {x = 1, z = 0}}, -- X+
{{x = 0, z = 0}, {x = 0, z = 1}}, -- X-
{{x = 0, z = 1}, {x = 1, z = 1}}, -- Z+
{{x = 1, z = 0}, {x = 0, z = 0}}, -- Z-
},
}
local function create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
local cSource = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_source)
local cFlowing = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing)
local range = math.min(math.max(meshport.get_def_from_id(cFlowing).liquid_range or 8, 1), 8)
--[[ Step 1: Gather neighbor data ]]
local neighbors = {[-1] = {}, [0] = {}, [1] = {}}
for dz = -1, 1 do
for dx = -1, 1 do
local nPos = vector.add(pos, vector.new(dx, 0, dz))
local nIdx = area:indexp(nPos)
neighbors[dz][dx] = {
content = vContent[nIdx],
level = -0.5,
is_same_liquid = false,
top_is_same_liquid = false,
}
local nData = neighbors[dz][dx]
if vContent[nIdx] ~= minetest.CONTENT_IGNORE then
if vContent[nIdx] == cSource then
nData.is_same_liquid = true
nData.level = 0.5
elseif vContent[nIdx] == cFlowing then
nData.is_same_liquid = true
local intLevel = math.max(vParam2[nIdx] % 8 - 8 + range, 0)
nData.level = -0.5 + (intLevel + 0.5) / range
end
local tPos = vector.add(nPos, vector.new(0, 1, 0))
local tIdx = area:indexp(tPos)
if vContent[tIdx] == cSource or vContent[tIdx] == cFlowing then
nData.top_is_same_liquid = true
end
end
end
end
--[[ Step 2: Determine level at each corner ]]
local cornerLevels = {[0] = {[0] = 0, 0}, {[0] = 0, 0}}
local function get_corner_level(cx, cz)
local sum = 0
local count = 0
local airCount = 0
for dz = -1, 0 do
for dx = -1, 0 do
local nData = neighbors[cz + dz][cx + dx]
if nData.top_is_same_liquid or nData.content == cSource then
return 0.5
elseif nData.content == cFlowing then
sum = sum + nData.level
count = count + 1
elseif nData.content == minetest.CONTENT_AIR then
airCount = airCount + 1
if airCount >= 2 then
return -0.5 + 0.02
end
end
end
end
if count > 0 then
return sum / count
end
return 0
end
for cz = 0, 1 do
for cx = 0, 1 do
cornerLevels[cz][cx] = get_corner_level(cx, cz)
end
end
--[[ Step 3: Actually create the liquid mesh ]]
local faces = meshport.Faces:new()
-- Localize constants
local SIDE_DIRS, SIDE_CORNERS = (function(c)
return c.SIDE_DIRS, c.SIDE_CORNERS
end)(FLOWING_LIQUID_CONSTANTS)
-- Add side faces
local sideVerts = {
{vec( 0.5, 0.5, 0.5), vec( 0.5, 0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5, 0.5)}, -- X+
{vec(-0.5, 0.5, -0.5), vec(-0.5, 0.5, 0.5), vec(-0.5, -0.5, 0.5), vec(-0.5, -0.5, -0.5)}, -- X-
{vec(-0.5, 0.5, 0.5), vec( 0.5, 0.5, 0.5), vec( 0.5, -0.5, 0.5), vec(-0.5, -0.5, 0.5)}, -- Z+
{vec( 0.5, 0.5, -0.5), vec(-0.5, 0.5, -0.5), vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5)}, -- Z-
}
local function need_side(dir)
local neighbor = neighbors[dir.z][dir.x]
if neighbor.is_same_liquid
and (not neighbors[0][0].top_is_same_liquid or neighbor.top_is_same_liquid) then
return false
end
local nContent = neighbors[dir.z][dir.x].content
local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(nContent).drawtype)
if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
return false -- Don't draw bordering normal nodes
end
return true
end
for i = 1, 4 do
local dir = SIDE_DIRS[i]
if need_side(dir) then
local verts = sideVerts[i]
local sideTexCoords = {{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}, {x = 1, y = 0}}
if not neighbors[0][0].top_is_same_liquid then -- If there's liquid above, default to a full block.
local corners = SIDE_CORNERS[i]
for j = 1, 2 do
local corner = cornerLevels[corners[j].z][corners[j].x]
verts[j].y = corner
sideTexCoords[j].y = corner + 0.5
end
end
faces:insert_face({
verts = verts,
vert_norms = {dir, dir, dir, dir},
tex_coords = sideTexCoords,
tile_idx = 2,
use_special_tiles = true,
})
end
end
-- Add top faces
if not neighbors[0][0].top_is_same_liquid then -- Check node above the current node
local verts = {
vec( 0.5, cornerLevels[0][1], -0.5),
vec( 0.5, cornerLevels[1][1], 0.5),
vec(-0.5, cornerLevels[1][0], 0.5),
vec(-0.5, cornerLevels[0][0], -0.5),
}
local norm1 = vector.normalize(vector.cross(
vector.subtract(verts[1], verts[2]),
vector.subtract(verts[3], verts[2])
))
local norm2 = vector.normalize(vector.cross(
vector.subtract(verts[3], verts[4]),
vector.subtract(verts[1], verts[4])
))
local dz = (cornerLevels[0][0] + cornerLevels[0][1]) -
(cornerLevels[1][0] + cornerLevels[1][1])
local dx = (cornerLevels[0][0] + cornerLevels[1][0]) -
(cornerLevels[0][1] + cornerLevels[1][1])
local textureAngle = -math.atan2(dz, dx)
-- Get texture coordinate offset based on position.
local tx, ty = pos.z, -pos.x
-- Rotate offset around (0, 0) by textureAngle.
-- Then isolate the fractional part, since the texture is tiled anyway.
local sinTA = math.sin(textureAngle)
local cosTA = math.cos(textureAngle)
local textureOffset = {
x = (tx * cosTA - ty * sinTA) % 1,
y = (tx * sinTA + ty * cosTA) % 1
}
faces:insert_face({
verts = {verts[1], verts[2], verts[3]},
vert_norms = {norm1, norm1, norm1},
tex_coords = meshport.translate_texture_coordinates(
meshport.rotate_texture_coordinates_rad(
{{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}},
textureAngle
),
textureOffset
),
tile_idx = 1,
use_special_tiles = true,
})
faces:insert_face({
verts = {verts[3], verts[4], verts[1]},
vert_norms = {norm2, norm2, norm2},
tex_coords = meshport.translate_texture_coordinates(
meshport.rotate_texture_coordinates_rad(
{{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}},
textureAngle
),
textureOffset
),
tile_idx = 1,
use_special_tiles = true,
})
end
-- Add bottom face
local function need_liquid_bottom()
local bContent = vContent[area:indexp(vector.add(pos, vector.new(0, -1, 0)))]
if bContent == cSource or bContent == cFlowing then
return false
end
local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(bContent).drawtype)
if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
return false -- Don't draw bordering normal nodes
end
return true
end
if need_liquid_bottom() then
local norm = vector.new(0, -1, 0)
faces:insert_face({
verts = {
vec(-0.5, -0.5, 0.5),
vec( 0.5, -0.5, 0.5),
vec( 0.5, -0.5, -0.5),
vec(-0.5, -0.5, -0.5),
},
vert_norms = {norm, norm, norm, norm},
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
tile_idx = 1,
use_special_tiles = true,
})
end
return faces
end
local function create_nodebox_node(pos, content, param2, neighbors)
local nodeName = minetest.get_name_from_content_id(content)
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)
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, param2, facedir, neighbors)
if meshport.nodebox_cache[nodeName].type ~= "connected" then
boxes:rotate_by_facedir(facedir)
end
return boxes:to_faces(nodeDef.tiles, pos, facedir)
return boxes:to_faces(nodeDef, pos, facedir)
end
function meshport.create_mesh_node(nodeDef, playerName)
local function create_mesh_node(nodeDef, param2, playerName)
local meshName = nodeDef.mesh
if not meshName then
@ -76,97 +573,227 @@ function meshport.create_mesh_node(nodeDef, playerName)
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))
meshport.log(playerName, "warning", string.format("Mesh %q is not supported.", meshName))
else
meshport.print(playerName, "warning", string.format("Mesh %q could not be found.", meshName))
meshport.log(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])
-- TODO: pcall this in case of failure
local meshFaces = meshport.parse_obj(meshport.obj_paths[meshName])
meshFaces:scale(nodeDef.visual_scale)
meshport.mesh_cache[meshName] = meshFaces
end
end
return meshport.mesh_cache[meshName]:copy()
local faces = meshport.mesh_cache[meshName]:copy()
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
faces:rotate_by_facedir(facedir)
local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
faces:rotate_xz_degrees(rotation)
return faces
end
function meshport.create_node(idx, area, content, param2, playerName)
if content[idx] == minetest.CONTENT_AIR or content[idx] == minetest.CONTENT_IGNORE then
local function create_plantlike_node(pos, param2, nodeDef)
local style = 0
local height = 1.0
local scale = 0.5 * nodeDef.visual_scale
local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
local offset = vector.new(0, 0, 0)
local randomOffsetY = false
local faceNum = 0
if nodeDef.paramtype2 == "meshoptions" then
style = param2 % 8
if param2 % 16 >= 8 then -- param2 % 8
-- TODO: Use MT's seed generators
local seed = (pos.x % 0xFF) * 0x100 + (pos.z % 0xFF) + (pos.y % 0xFF) * 0x10000
local rng = PseudoRandom(seed)
offset.x = (rng:next() % 16 / 16) * 0.29 - 0.145
offset.z = (rng:next() % 16 / 16) * 0.29 - 0.145
end
if param2 % 32 >= 16 then -- param2 & 16
scale = scale * 1.41421
end
if param2 % 64 >= 32 then -- param2 & 32
randomOffsetY = true
end
elseif nodeDef.paramtype2 == "leveled" then
height = param2 / 16
end
local function create_plantlike_quad(faceRotation, topOffset, bottomOffset)
local faces = meshport.Faces:new()
local plantHeight = 2.0 * scale * height
local norm = vector.normalize(vector.new(0, bottomOffset - topOffset, plantHeight))
faces:insert_face({
verts = {
vec(-scale, -0.5 + plantHeight, topOffset),
vec( scale, -0.5 + plantHeight, topOffset),
vec( scale, -0.5, bottomOffset),
vec(-scale, -0.5, bottomOffset),
},
tex_coords = {{x = 0, y = height}, {x = 1, y = height}, {x = 1, y = 0}, {x = 0, y = 0}},
vert_norms = {norm, norm, norm, norm},
tile_idx = 0,
use_special_tiles = nodeDef.drawtype == "plantlike_rooted"
})
if randomOffsetY then
local seed = faceNum + (pos.x % 0xFF) * 0x10000 + (pos.z % 0xFF) * 0x100 + (pos.y % 0xFF) * 0x1000000
local yRng = PseudoRandom(seed)
faces:translate(vector.new(0, (yRng:next() % 16) / 16.0 * -0.125, 0))
faceNum = faceNum + 1
end
faces:rotate_xz_degrees(faceRotation + rotation)
return faces
end
local faces = meshport.Faces:new()
if style == 0 then
faces:insert_all(create_plantlike_quad(46, 0, 0))
faces:insert_all(create_plantlike_quad(-44, 0, 0))
elseif style == 1 then
faces:insert_all(create_plantlike_quad(91, 0, 0))
faces:insert_all(create_plantlike_quad(1, 0, 0))
elseif style == 2 then
faces:insert_all(create_plantlike_quad(121, 0, 0))
faces:insert_all(create_plantlike_quad(241, 0, 0))
faces:insert_all(create_plantlike_quad(1, 0, 0))
elseif style == 3 then
faces:insert_all(create_plantlike_quad(1, 0.25, 0.25))
faces:insert_all(create_plantlike_quad(91, 0.25, 0.25))
faces:insert_all(create_plantlike_quad(181, 0.25, 0.25))
faces:insert_all(create_plantlike_quad(271, 0.25, 0.25))
elseif style == 4 then
faces:insert_all(create_plantlike_quad(1, -0.5, 0))
faces:insert_all(create_plantlike_quad(91, -0.5, 0))
faces:insert_all(create_plantlike_quad(181, -0.5, 0))
faces:insert_all(create_plantlike_quad(271, -0.5, 0))
end
faces:translate(offset)
return faces
end
local function create_node(idx, area, vContent, vParam2, playerName)
if vContent[idx] == minetest.CONTENT_AIR
or vContent[idx] == minetest.CONTENT_IGNORE
or vContent[idx] == minetest.CONTENT_UNKNOWN then -- TODO: Export unknown nodes?
return
end
local nodeDef = meshport.get_def_from_id(content[idx])
if not nodeDef.drawtype or nodeDef.drawtype == "airlike" then
local nodeDef = meshport.get_def_from_id(vContent[idx])
if nodeDef.drawtype == "airlike" then
return
end
local pos = area:position(idx)
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)
if CUBIC_FACE_PRIORITY[nodeDrawtype] or nodeDrawtype == "nodebox" then
neighbors = meshport.get_node_neighbors(vContent, area, idx)
end
if meshport.cube_face_priority[nodeDrawtype] then
faces = meshport.create_cube_node(nodeDrawtype, nodeDef.tiles, area:position(idx), facedir, neighbors)
if (CUBIC_FACE_PRIORITY[nodeDrawtype] or 0) >= 3 then -- liquid, normal, plantlike_rooted
faces = create_cubic_node(pos, vContent[idx], vParam2[idx], nodeDef, nodeDrawtype, neighbors)
if nodeDrawtype == "plantlike_rooted" then
local plantPos = vector.add(pos, vector.new(0, 1, 0))
local plantFaces = create_plantlike_node(nodeDef, plantPos, vParam2[idx])
plantFaces:translate(vector.new(0, 1, 0))
faces:insert_all(plantFaces)
end
elseif CUBIC_FACE_PRIORITY[nodeDrawtype] then -- Any other cubic nodes (allfaces, glasslike)
faces = create_special_cubic_node(pos, vContent[idx], nodeDef, nodeDrawtype, neighbors)
elseif nodeDrawtype == "glasslike_framed" then
faces = create_glasslike_framed_node(pos, vParam2[idx], nodeDef, area, vContent)
elseif nodeDrawtype == "flowingliquid" then
faces = create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
elseif nodeDrawtype == "nodebox" then
faces = meshport.create_nodebox_node(
minetest.get_name_from_content_id(content[idx]), area:position(idx), facedir, param2[idx], neighbors)
faces = create_nodebox_node(pos, vContent[idx], vParam2[idx], neighbors)
elseif nodeDrawtype == "mesh" then
faces = meshport.create_mesh_node(nodeDef, playerName)
faces = create_mesh_node(nodeDef, vParam2[idx], playerName)
elseif nodeDrawtype == "plantlike" then
faces = create_plantlike_node(pos, vParam2[idx], nodeDef)
end
if not faces then
return
end
if not isCubicType then
faces:rotate_by_facedir(facedir)
end
faces:apply_tiles(nodeDef)
return faces
end
local function initialize_resources()
meshport.texture_paths = meshport.get_asset_paths("textures")
meshport.texture_dimension_cache = {}
-- meshport.obj_paths is only loaded if needed
meshport.nodebox_cache = {}
meshport.mesh_cache = {}
end
local function cleanup_resources()
meshport.texture_paths = nil
meshport.texture_dimension_cache = nil
meshport.obj_paths = nil
meshport.nodebox_cache = nil
meshport.mesh_cache = nil
end
function meshport.create_mesh(playerName, p1, p2, path)
meshport.print(playerName, "info", "Generating mesh...")
meshport.log(playerName, "info", "Generating mesh...")
initialize_resources()
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()
local vContent = vm:get_data()
local vParam2 = 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 meshOrigin = vector.subtract(p1, 0.5)
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)
local faces = create_node(idx, vArea, vContent, vParam2, 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))
-- Move the node to its proper position.
faces:translate(vector.subtract(vArea:position(idx), meshOrigin))
for _, face in ipairs(faces.faces) do
-- Add each face to our final mesh.
mesh:insert_face(face)
end
-- Add faces to our final mesh.
mesh:insert_faces(faces)
end
end
minetest.mkdir(path)
mesh:write_obj(path)
mesh:write_mtl(path, playerName)
meshport.print(playerName, "info", "Finished. Saved to " .. path)
cleanup_resources()
meshport.log(playerName, "info", "Finished. Saved to " .. path)
end

View File

@ -1,11 +1,29 @@
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
This file is part of Meshport.
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
meshport = {
p1 = {},
p2 = {},
player_data = {},
}
modpath = minetest.get_modpath("meshport")
dofile(modpath .. "/helpers.lua")
dofile(modpath .. "/api.lua")
dofile(modpath .. "/utils.lua")
dofile(modpath .. "/mesh.lua")
dofile(modpath .. "/parse_obj.lua")
dofile(modpath .. "/nodebox.lua")
dofile(modpath .. "/export.lua")
@ -14,8 +32,7 @@ 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
meshport.player_data[name] = nil
end)
for i = 1, 2 do
@ -35,19 +52,23 @@ for i = 1, 2 do
end
if not pos then
meshport.print(name, "error", "Not a valid position.")
meshport.log(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
if not meshport.player_data[name] then
meshport.player_data[name] = {}
end
meshport.print(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
if i == 1 then
meshport.player_data[name].p1 = pos
elseif i == 2 then
meshport.player_data[name].p2 = pos
end
meshport.log(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
end,
})
end
@ -58,13 +79,15 @@ minetest.register_chatcommand("meshport", {
privs = {meshport = true},
func = function(name, filename)
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.")
local playerData = meshport.player_data[name] or {}
if not playerData.p1 or not playerData.p2 then
meshport.log(name, "error", "No area selected. Use /mesh1 and /mesh2 to select an area.")
return
end
if filename:find("[^%w-_]") then
meshport.print(name, "error", "Invalid name supplied. Please use valid characters ([A-Z][a-z][0-9][-_]).")
meshport.log(name, "error", "Invalid name supplied. Please use valid characters ([A-Z][a-z][0-9][-_]).")
return
elseif filename == "" then
filename = os.date("%Y-%m-%d_%H-%M-%S")
@ -74,12 +97,12 @@ minetest.register_chatcommand("meshport", {
local folderName = name .. "_" .. filename
if table.indexof(minetest.get_dir_list(mpPath, true), folderName) > 0 then
meshport.print(name, "error",
meshport.log(name, "error",
string.format("Folder %q already exists. Try using a different name.", folderName))
return
end
local path = mpPath .. DIR_DELIM .. folderName
meshport.create_mesh(name, meshport.p1[name], meshport.p2[name], path)
meshport.create_mesh(name, playerData.p1, playerData.p2, path)
end,
})

338
mesh.lua Normal file
View File

@ -0,0 +1,338 @@
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
This file is part of Meshport.
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
--[[
A buffer of faces.
Faces are expected to be in this format:
{
verts = table, -- list of vertices (as vectors)
vert_norms = table, -- list of vertex normals (as vectors)
tex_coords = table, -- list of texture coordinates, e.g. {x = 0.5, y = 1}
tile_idx = int, -- index of tile to use
use_special_tiles = bool, -- if true, use tiles from special_tiles field of nodedef
texture = string, -- name of actual texture to use)
}
Note to self/contributors--to avoid weird bugs, please follow these rules
regarding table fields:
1. Each table of vertices, vertex normals, or texture coordinates should be
unique to its parent face. That is, multiple faces or Faces objects
should not share references to the same table.
2. Values within these tables are allowed to be duplicated. For example, one
face can reference the same vertex normal four times, and other faces or
Faces objects can also reference the same vertex normal.
]]
meshport.Faces = {}
function meshport.Faces:new()
local o = { -- TODO: Separate tables for vertices/indices.
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:insert_all(faces)
for _, face in ipairs(faces.faces) do
table.insert(self.faces, face)
end
end
function meshport.Faces:copy()
local newFaces = meshport.Faces:new()
newFaces.faces = table.copy(self.faces)
return newFaces
end
function meshport.Faces:translate(vec)
if vec.x == 0 and vec.y == 0 and vec.z == 0 then
return
end
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:scale(scale)
if scale == 1 then
return
end
for _, face in ipairs(self.faces) do
for i, vert in ipairs(face.verts) do
face.verts[i] = vector.multiply(vert, scale)
end
end
end
function meshport.Faces:rotate_by_facedir(facedir)
if facedir == 0 then
return
end
for _, face in ipairs(self.faces) do
for i, vert in ipairs(face.verts) do
face.verts[i] = meshport.rotate_vector_by_facedir(vert, facedir)
end
for i, norm in ipairs(face.vert_norms) do
face.vert_norms[i] = meshport.rotate_vector_by_facedir(norm, facedir)
end
end
end
function meshport.Faces:rotate_xz_degrees(degrees)
if degrees == 0 then
return
end
local rad = math.rad(degrees)
local sinRad = math.sin(rad)
local cosRad = math.cos(rad)
for _, face in ipairs(self.faces) do
for i, vert in ipairs(face.verts) do
face.verts[i] = vector.new(
vert.x * cosRad - vert.z * sinRad,
vert.y,
vert.x * sinRad + vert.z * cosRad
)
end
for i, norm in ipairs(face.vert_norms) do
face.vert_norms[i] = vector.new(
norm.x * cosRad - norm.z * sinRad,
norm.y,
norm.x * sinRad + norm.z * cosRad
)
end
end
end
function meshport.Faces:apply_tiles(nodeDef)
for _, face in ipairs(self.faces) do
local tiles
if face.use_special_tiles then
tiles = nodeDef.special_tiles
else
tiles = nodeDef.tiles
end
local tile = meshport.get_tile(tiles, face.tile_idx)
face.texture = tile.name or tile
-- If an animated texture is used, scale texture coordinates so only the first image is used.
local animation = tile.animation
if type(animation) == "table" then
local xScale, yScale = 1, 1
if animation.type == "vertical_frames" then
local texW, texH = meshport.get_texture_dimensions(tile.name)
if texW and texH then
xScale = (animation.aspect_w or 16) / texW
yScale = (animation.aspect_h or 16) / texH
end
elseif animation.type == "sheet_2d" then
xScale = 1 / (animation.frames_w or 1)
yScale = 1 / (animation.frames_h or 1)
end
if xScale ~= 1 or yScale ~= 1 then
for i, coord in ipairs(face.tex_coords) do
face.tex_coords[i] = {x = coord.x * xScale, y = coord.y * yScale}
end
end
end
end
end
local function clean_vector(vec)
-- Prevents an issue involving negative zero values, which are not handled properly by `string.format`.
return vector.new(
vec.x == 0 and 0 or vec.x,
vec.y == 0 and 0 or vec.y,
vec.z == 0 and 0 or vec.z
)
end
local function bimap_find_or_insert(forward, reverse, item)
local idx = reverse[item]
if not idx then
idx = #forward + 1
forward[idx] = item
reverse[item] = idx
end
return idx
end
-- Stores a mesh in a form which is easily convertible to an .OBJ file.
meshport.Mesh = {}
function meshport.Mesh:new()
local o = {
-- Using two tables for elements makes insert_face() significantly faster.
-- verts[1] = "0 -1 0"
-- verts_reverse["0 -1 0"] = 1
-- etc...
verts = {},
verts_reverse = {},
vert_norms = {},
vert_norms_reverse = {},
tex_coords = {},
tex_coords_reverse = {},
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 = clean_vector(vector.new(vert.x, vert.y, -vert.z))
elementStr = string.format("%f %f %f", vec.x, vec.y, vec.z)
indices.verts[i] = bimap_find_or_insert(self.verts, self.verts_reverse, elementStr)
end
-- Add texture coordinates (UV map).
for i, texCoord in ipairs(face.tex_coords) do
elementStr = string.format("%f %f", texCoord.x, texCoord.y)
indices.tex_coords[i] = bimap_find_or_insert(self.tex_coords, self.tex_coords_reverse, elementStr)
end
-- Add vertex normals.
for i, vertNorm in ipairs(face.vert_norms) do
-- Invert Z axis to comply with Blender's coordinate system.
vec = clean_vector(vector.new(vertNorm.x, vertNorm.y, -vertNorm.z))
elementStr = string.format("%f %f %f", vec.x, vec.y, vec.z)
indices.vert_norms[i] = bimap_find_or_insert(self.vert_norms, self.vert_norms_reverse, elementStr)
end
-- Add faces to mesh.
if not self.faces[face.texture] then
self.faces[face.texture] = {}
end
local vertStrs = {}
for i = 1, #indices.verts do
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
table.insert(self.faces[face.texture], table.concat(vertStrs, " "))
end
function meshport.Mesh:insert_faces(faces)
for _, face in ipairs(faces.faces) do
self:insert_face(face)
end
end
function meshport.Mesh:write_obj(path)
local objFile = io.open(path .. "/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(string.format("v %s\n", vert))
end
-- Write texture coordinates.
for _, texCoord in ipairs(self.tex_coords) do
objFile:write(string.format("vt %s\n", texCoord))
end
-- Write vertex normals.
for _, vertNorm in ipairs(self.vert_norms) do
objFile:write(string.format("vn %s\n", 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(string.format("f %s\n", face))
end
end
objFile:close()
end
function meshport.Mesh:write_mtl(path, playerName)
local matFile = io.open(path .. "/materials.mtl", "w")
matFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n")
-- Write material information.
for mat, _ in pairs(self.faces) do
matFile:write(string.format("\nnewmtl %s\n", mat))
-- Attempt to get the base texture, ignoring texture modifiers.
local texName = string.match(mat, "[%w%s%-_%.]+%.png") or mat
if meshport.texture_paths[texName] then
if texName ~= mat then
meshport.log(playerName, "warning", string.format("Ignoring texture modifers in material %q.", mat))
end
matFile:write(string.format("map_Kd %s\n", meshport.texture_paths[texName]))
else
meshport.log(playerName, "warning",
string.format("Could not find texture %q. Using a dummy material instead.", texName))
matFile:write(string.format("Kd %f %f %f\n", math.random(), math.random(), math.random()))
end
end
matFile:close()
end

View File

@ -1,4 +1,24 @@
meshport.side_box_names = {
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
This file is part of Meshport.
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
local SIDE_BOX_NAMES = {
"top", -- Y+
"bottom", -- Y-
"right", -- X+
@ -7,7 +27,8 @@ meshport.side_box_names = {
"front", -- Z-
}
function meshport.sort_box(box)
local function sort_box(box)
return {
math.min(box[1], box[4]),
math.min(box[2], box[5]),
@ -18,15 +39,41 @@ function meshport.sort_box(box)
}
end
local function 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
-- A list of node boxes, in the format used by Minetest:
-- {a.x, a.y, a.z, b.x, b.y, b.z}
-- Individual boxes inside the `boxes` array are not mutated.
meshport.Boxes = {}
function meshport.Boxes:new(boxes)
local o = {}
if type(boxes) ~= "table" or type(boxes[1]) == "number" then
o.boxes = {boxes}
if type(boxes) == "table" and type(boxes[1]) == "table" then
-- Copy boxes individually to avoid mutating the argument.
o.boxes = {}
for i, box in ipairs(boxes) do
o.boxes[i] = box
end
else
o.boxes = boxes
o.boxes = {boxes}
end
setmetatable(o, self)
@ -34,9 +81,13 @@ function meshport.Boxes:new(boxes)
return o
end
function meshport.Boxes:insert_box(box)
table.insert(self.boxes, box)
end
function meshport.Boxes:insert_all(boxes)
for _, box in ipairs(boxes.boxes) do
table.insert(self.boxes, table.copy(box))
table.insert(self.boxes, box)
end
end
@ -63,62 +114,66 @@ function meshport.Boxes:rotate_by_facedir(facedir)
end
function meshport.Boxes:get_leveled(level)
local newBoxes = meshport.Boxes:new(table.copy(self.boxes))
local newBoxes = meshport.Boxes:new()
for i, box in ipairs(newBoxes.boxes) do
box = meshport.sort_box(box)
box[5] = level / 64 - 0.5
newBoxes.boxes[i] = box
for i, box in ipairs(self.boxes) do
local newBox = sort_box(box)
newBox[5] = level / 64 - 0.5
newBoxes.boxes[i] = newBox
end
return newBoxes
end
function meshport.Boxes:to_faces(nodeTiles, pos, facedir)
function meshport.Boxes:to_faces(nodeDef, pos, facedir, tileIdx, useSpecial)
local tiles = useSpecial and nodeDef.special_tiles or nodeDef.tiles
local vec = vector.new
local faces = meshport.Faces:new()
for _, b in ipairs(self.boxes) do
b = meshport.sort_box(b)
for _, box in ipairs(self.boxes) do
local b = sort_box(box)
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-
{vec(b[1], b[5], b[3]), vec(b[4], b[5], b[3]), vec(b[4], b[5], b[6]), vec(b[1], b[5], b[6])}, -- Y+
{vec(b[1], b[2], b[6]), vec(b[4], b[2], b[6]), vec(b[4], b[2], b[3]), vec(b[1], b[2], b[3])}, -- Y-
{vec(b[4], b[2], b[3]), vec(b[4], b[2], b[6]), vec(b[4], b[5], b[6]), vec(b[4], b[5], b[3])}, -- X+
{vec(b[1], b[2], b[6]), vec(b[1], b[2], b[3]), vec(b[1], b[5], b[3]), vec(b[1], b[5], b[6])}, -- X-
{vec(b[4], b[2], b[6]), vec(b[1], b[2], b[6]), vec(b[1], b[5], b[6]), vec(b[4], b[5], b[6])}, -- Z+
{vec(b[1], b[2], b[3]), vec(b[4], b[2], b[3]), vec(b[4], b[5], b[3]), vec(b[1], b[5], b[3])}, -- Z-
}
local t = {}
for i = 1, 6 do
t[i] = b[i] + 0.5 -- Texture coordinates range from 0 to 1
end
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-
{{x = t[1], y = t[3]}, {x = t[4], y = t[3]}, {x = t[4], y = t[6]}, {x = t[1], y = t[6]}}, -- Y+
{{x = t[1], y =1-t[6]}, {x = t[4], y =1-t[6]}, {x = t[4], y =1-t[3]}, {x = t[1], y =1-t[3]}}, -- Y-
{{x = t[3], y = t[2]}, {x = t[6], y = t[2]}, {x = t[6], y = t[5]}, {x = t[3], y = t[5]}}, -- X+
{{x =1-t[6], y = t[2]}, {x =1-t[3], y = t[2]}, {x =1-t[3], y = t[5]}, {x =1-t[6], y = t[5]}}, -- X-
{{x =1-t[4], y = t[2]}, {x =1-t[1], y = t[2]}, {x =1-t[1], y = t[5]}, {x =1-t[4], y = t[5]}}, -- Z+
{{x = t[1], y = t[2]}, {x = t[4], y = t[2]}, {x = t[4], y = t[5]}, {x = t[1], y = t[5]}}, -- Z-
}
local 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]
local norm = 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))
vert_norms = {norm, norm, norm, norm},
tile_idx = tileIdx,
use_special_tiles = useSpecial,
}, tiles, pos, facedir, i))
end
end
return faces
end
function meshport.prepare_nodebox(nodebox)
local prepNodebox = {}
prepNodebox.type = nodebox.type
@ -132,7 +187,7 @@ function meshport.prepare_nodebox(nodebox)
prepNodebox.connected = {}
prepNodebox.disconnected = {}
for i, name in ipairs(meshport.side_box_names) do
for i, name in ipairs(SIDE_BOX_NAMES) do
prepNodebox.connected[i] = meshport.Boxes:new(nodebox["connect_" .. name])
prepNodebox.disconnected[i] = meshport.Boxes:new(nodebox["disconnected_" .. name])
end
@ -145,20 +200,21 @@ function meshport.prepare_nodebox(nodebox)
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)
prepNodebox.wall_top:transform(function(v) return vector.new(-v.x, -v.y, v.z) end)
prepNodebox.wall_side:transform(function(v) return vector.new(-v.z, v.x, v.y) end)
end
return prepNodebox
end
function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors)
function meshport.collect_boxes(prepNodebox, nodeDef, param2, facedir, 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))
local level = nodeDef.paramtype2 == "leveled" and param2 or nodeDef.leveled or 0
boxes:insert_all(prepNodebox.fixed:get_leveled(level))
else
boxes:insert_all(prepNodebox.fixed)
end
@ -170,7 +226,7 @@ function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors
for i = 1, 6 do
neighborName = minetest.get_name_from_content_id(neighbors[i])
if meshport.node_connects_to(neighborName, nodeDef.connects_to) then
if node_connects_to(neighborName, nodeDef.connects_to) then
boxes:insert_all(prepNodebox.connected[i])
else
boxes:insert_all(prepNodebox.disconnected[i])

View File

@ -1,31 +1,50 @@
function meshport.parse_vector_element(elementStr)
local elementType
local vec = {}
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
-- 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%.%-]*)")
This file is part of Meshport.
for k, v in pairs(vec) do
vec[k] = tonumber(v)
end
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
-- 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
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
-- See the OBJ file specification: http://www.martinreddy.net/gfx/3d/OBJ.spec
-- Also, the Irrlicht implementation: irrlicht/source/Irrlicht/COBJMeshFileLoader.cpp
local function parse_vector_element(elementType, elementStr)
if elementType == "v" or elementType == "vn" then
-- Note that there may be an optional weight value after z, which is ignored.
local xs, ys, zs = string.match(elementStr, "^([%d%.%-]+)%s+([%d%.%-]+)%s+([%d%.%-]+)")
-- The X axis of vectors is inverted to match the Minetest coordinate system.
local vec = vector.new(-tonumber(xs), tonumber(ys), tonumber(zs))
if elementType == "v" then
return "verts", vec
else
return "vert_norms", vec
end
elseif elementType == "vt" then
return "tex_coords", vec
elseif elementType == "vn" then
vec.x = -vec.x
return "vert_norms", vec
local xs, ys = string.match(elementStr, "^([%d%.%-]+)%s+([%d%.%-]+)")
local coords = {x = tonumber(xs), y = tonumber(ys)}
assert(coords.x and coords.y, "Invalid texture coordinate element")
return "tex_coords", coords
end
end
function meshport.parse_face_element(elements, elementStr)
local function parse_face_element(elements, faceStr)
-- 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 vertStrs = string.split(faceStr, " ")
local face = {
verts = {},
@ -34,58 +53,77 @@ function meshport.parse_face_element(elements, elementStr)
}
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)
-- Split the string into indices for vertex, texture coordinate, and/or vertex normal elements.
local vs, vts, vns = string.match(vertStr, "^(%d*)/?(%d*)/?(%d*)$")
local vi, vti, vni = tonumber(vs), tonumber(vts), tonumber(vns)
assert(vi, "Invalid face element")
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]
-- Set the position, texture coordinate, and vertex normal of the vertex.
-- Note that vti or vni are allowed to be nil
face.verts[i] = elements.verts[vi]
face.tex_coords[i] = elements.tex_coords[vti]
face.vert_norms[i] = elements.vert_norms[vni]
end
return face
end
local function handle_group(groups, elementStr)
-- Note: Minetest ignores usemtl; see `OBJ_LOADER_IGNORE_MATERIAL_FILES`.
-- The format allows multiple group names; get only the first one.
local groupName = string.match(elementStr, "^(%S+)")
if not groupName then
-- "default" is the default group name if no name is specified.
groupName = "default"
end
local groupIdx = table.indexof(groups, groupName)
-- If this group has not been used yet, add it to the list.
if groupIdx < 0 then
table.insert(groups, groupName)
groupIdx = #groups
end
return groupIdx
end
function meshport.parse_obj(path)
local faces = meshport.Faces:new()
local file = io.open(path, "r")
local faces = meshport.Faces:new()
local elements = {
verts = {},
tex_coords = {},
vert_norms = {},
}
-- Tiles are assigned according to groups, in the order in which groups are defined.
local groups = {}
local curGroup
local elementType
local currentTileIdx
for line in file:lines() do
elementType = string.sub(line, 1, 1)
-- elementStr may be an empty string, e.g. "g" with no group name.
local elementType, elementStr = string.match(line, "^(%a+)%s*(.*)")
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)
if elementType == "v" or elementType == "vt" or elementType == "vn" then
local dest, value = parse_vector_element(elementType, elementStr)
table.insert(elements[dest], value)
elseif elementType == "f" then
-- If the face is not part of any group, use the placeholder group `0`.
if not curGroup then
if not currentTileIdx then
table.insert(groups, 0)
curGroup = table.indexof(groups, 0)
currentTileIdx = #groups
end
-- Parse the face element.
local face = meshport.parse_face_element(elements, line)
local face = parse_face_element(elements, elementStr)
-- Assign materials according to the group.
face.tile_idx = curGroup
face.tile_idx = currentTileIdx
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(.+)"))
currentTileIdx = handle_group(groups, elementStr)
end
end

View File

@ -1,13 +1,47 @@
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-
--[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
This file is part of Meshport.
Meshport is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]
meshport.NEIGHBOR_DIRS = {
-- face neighbors
vector.new( 0, 1, 0), -- 1
vector.new( 0,-1, 0),
vector.new( 1, 0, 0),
vector.new(-1, 0, 0),
vector.new( 0, 0, 1),
vector.new( 0, 0,-1),
-- edge neighbors
vector.new(-1, 1, 0), -- 7
vector.new( 1, 1, 0),
vector.new( 0, 1, 1),
vector.new( 0, 1,-1),
vector.new(-1, 0, 1),
vector.new( 1, 0, 1),
vector.new(-1, 0,-1),
vector.new( 1, 0,-1),
vector.new(-1,-1, 0),
vector.new( 1,-1, 0),
vector.new( 0,-1, 1),
vector.new( 0,-1,-1),
}
meshport.facedir_to_tile_indices = {
local FACEDIR_TO_TILE_INDICES = {
[0] =
{1, 2, 3, 4, 5, 6},
{1, 2, 5, 6, 4, 3},
@ -35,42 +69,43 @@ meshport.facedir_to_tile_indices = {
{2, 1, 5, 6, 3, 4},
}
meshport.facedir_to_tile_rotations = {
local 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},
{2, 2, 0, 0, 0, 0},
{3, 1, 0, 0, 0, 0},
{0, 2, 1, 3, 2, 0},
{0, 2, 1, 3, 3, 3},
{0, 2, 1, 3, 0, 2},
{0, 2, 1, 3, 1, 1},
{2, 0, 3, 1, 2, 0},
{2, 0, 3, 1, 1, 1},
{2, 0, 3, 1, 0, 2},
{2, 0, 3, 1, 3, 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},
{3, 3, 3, 3, 1, 3},
{3, 3, 2, 0, 1, 3},
{3, 3, 1, 1, 1, 3},
{3, 3, 0, 2, 1, 3},
{2, 2, 2, 2, 2, 2},
{3, 1, 2, 2, 2, 2},
{0, 0, 2, 2, 2, 2},
{1, 3, 2, 2, 2, 2},
{0, 0, 2, 2, 2, 2},
{3, 1, 2, 2, 2, 2},
}
meshport.wallmounted_to_facedir = {[0] = 20, 0, 17, 15, 8, 6}
local WALLMOUNTED_TO_FACEDIR = {[0] = 20, 0, 17, 15, 8, 6}
meshport.drawtype_aliases = {
local DRAWTYPE_ALIASES = {
allfaces_optional = "allfaces",
glasslike_framed_optional = "glasslike_framed",
glasslike_framed_optional = "glasslike",
}
function meshport.print(name, level, s)
function meshport.log(name, level, s)
local message
if level == "info" then
@ -84,27 +119,6 @@ function meshport.print(name, level, s)
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)
@ -135,36 +149,68 @@ function meshport.rotate_vector_by_facedir(vec, facedir)
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
function meshport.translate_texture_coordinates(texCoords, offset)
if offset.x == 0 and offset.y == 0 then
return texCoords
end
local newTexCoords = {}
for _, texCoord in ipairs(texCoords) do
table.insert(newTexCoords, meshport.rotate_texture_coordinate(texCoord, rot))
for _, tc in ipairs(texCoords) do
table.insert(newTexCoords, {x = tc.x + offset.x, y = tc.y + offset.y})
end
return newTexCoords
end
function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scale)
function meshport.rotate_texture_coordinates_rad(texCoords, rad)
if rad == 0 then
return texCoords
end
local sinRad = math.sin(rad)
local cosRad = math.cos(rad)
local newTexCoords = {}
for _, texCoord in ipairs(texCoords) do
-- Coordinates are rotated around (0.5, 0.5).
local x = texCoord.x - 0.5
local y = texCoord.y - 0.5
table.insert(newTexCoords, {
x = x * cosRad - y * sinRad + 0.5,
y = x * sinRad + y * cosRad + 0.5
})
end
return newTexCoords
end
local function rotate_texture_coordinates(texCoords, rot)
if rot == 0 then
return
end
for i, tc in ipairs(texCoords) do
local x, y
-- Rotate the vector. Values of components range from 0 to 1, so adding 1 when inverting is necessary.
if rot == 1 then
x, y = 1 - tc.y, tc.x -- 90 degrees counterclockwise
elseif rot == 2 then
x, y = 1 - tc.x, 1 - tc.y -- 180 degrees counterclockwise
elseif rot == 3 then
x, y = tc.y, 1 - tc.x -- 270 degrees counterclockwise
end
texCoords[i] = {x = x, y = y}
end
end
local function 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 = {}
@ -189,57 +235,77 @@ function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scal
end
-- Scale and move the texture coordinates.
local newTexCoords = {}
for _, texCoord in ipairs(texCoords) do
table.insert(newTexCoords, {
for i, texCoord in ipairs(texCoords) do
texCoords[i] = {
x = (texCoord.x + texPos.x) / scale,
y = (texCoord.y + texPos.y) / scale,
})
}
end
return newTexCoords
end
-- WARNING: This function mutates tables!
-- Please follow the table rules used by Faces.
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]
face.tile_idx = face.tile_idx or 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)
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])
rotate_texture_coordinates(face.tex_coords, FACEDIR_TO_TILE_ROTATIONS[facedir][sideIdx])
end
return face
end
function meshport.get_content_id_or_nil(nodeName)
if minetest.registered_nodes[nodeName] then
return minetest.get_content_id(nodeName)
end
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
return DRAWTYPE_ALIASES[drawtype or ""] or drawtype
end
function meshport.get_facedir(param2, type)
function meshport.get_facedir(type, param2)
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
return WALLMOUNTED_TO_FACEDIR[param2 % 8] or 0
else
return 0
end
end
function meshport.get_degrotate(type, param2)
if type == "degrotate" then
return 1.5 * (param2 % 240)
elseif type == "colordegrotate" then
return 15 * ((param2 % 32) % 24)
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)
@ -247,37 +313,65 @@ function meshport.get_node_neighbors(array, area, idx)
-- 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]))]
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
if type(tiles) == "table" and #tiles > 0 then
return tiles[n] or tiles[#tiles]
else
return "unknown"
end
end
local function get_png_dimensions(path)
-- Luckily, reading the dimensions of a PNG file is a trivial task
local file = io.open(path, "rb")
if not file then
return
end
file:seek("set", 1)
if file:read(3) ~= "PNG" then -- Verify it's a PNG file
return
end
file:seek("set", 16)
local function read_u32(b)
return (b:byte(1) * 0x1000000 +
b:byte(2) * 0x10000 +
b:byte(3) * 0x100 +
b:byte(4))
end
local w = read_u32(file:read(4))
local h = read_u32(file:read(4))
file:close()
return w, h
end
-- In case of failure, this should return nil, nil
function meshport.get_texture_dimensions(textureName)
local dims = meshport.texture_dimension_cache[textureName]
if dims then
return dims[1], dims[2]
end
local path = meshport.texture_paths[textureName]
if path then
local w, h = get_png_dimensions(path)
meshport.texture_dimension_cache[path] = {w, h} -- Will be an empty table if the file isn't found.
return w, h
end
end
function meshport.get_asset_paths(assetFolderName, extension)
local modAssetPath
local assets = {}
@ -288,7 +382,7 @@ function meshport.get_asset_paths(assetFolderName, extension)
-- 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.
-- Add files to the table. If an extension 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