pax_global_header 0000666 0000000 0000000 00000000064 14163517455 0014525 g ustar 00root root 0000000 0000000 52 comment=b8a53fa65b2569bf5ce7b1e26b1ef4fd2fb520a5
meshport-0.2.2/ 0000775 0000000 0000000 00000000000 14163517455 0013367 5 ustar 00root root 0000000 0000000 meshport-0.2.2/.github/ 0000775 0000000 0000000 00000000000 14163517455 0014727 5 ustar 00root root 0000000 0000000 meshport-0.2.2/.github/workflows/ 0000775 0000000 0000000 00000000000 14163517455 0016764 5 ustar 00root root 0000000 0000000 meshport-0.2.2/.github/workflows/build.yml 0000664 0000000 0000000 00000000364 14163517455 0020611 0 ustar 00root root 0000000 0000000 on: [push, pull_request]
name: build
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: lint
uses: Roang-zero1/factorio-mod-luacheck@master
with:
luacheckrc_url: ""
meshport-0.2.2/.luacheckrc 0000664 0000000 0000000 00000000431 14163517455 0015472 0 ustar 00root root 0000000 0000000 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",
"PseudoRandom",
"vector",
"VoxelArea",
}
meshport-0.2.2/LICENSE 0000664 0000000 0000000 00000016744 14163517455 0014410 0 ustar 00root root 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
meshport-0.2.2/README.md 0000664 0000000 0000000 00000011174 14163517455 0014652 0 ustar 00root root 0000000 0000000 # Meshport (Minetest Mesh Exporter)
[](https://github.com/random-geek/meshport/actions)
[](https://content.minetest.net/packages/random_geek/meshport/)
[](https://www.gnu.org/licenses/lgpl-3.0.en.html)

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 beta phase; certain texturing features and node
drawtypes are not yet supported.
## Usage
Only players with the `meshport` privilege are allowed to select areas and
export meshes. This privilege is granted to singleplayer/admin players by
default.
To export a mesh, first select the area you want to export. There are two ways
to do this:
- Use the **Meshport Area Selector** tool. Left- or right-click on a node or
object to select either corner of the area. Hold sneak while clicking a node
to select the node in front of the face you clicked on.
- Or, use the `/mesh1` and `/mesh2` commands to set either corner. You can
specify a position (e.g. `/mesh1 -24 0 24`) or leave the argument blank to
use your current position (e.g. `/mesh1`).
After selecting an area, use `/meshport [filename]` to export the mesh
(filename is optional).
The `/meshrst` command can be used to clear the current
selection.
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 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
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.
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.
#### Other fixes
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
The following node drawtypes are currently supported:
- 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`
Meshport also supports many of Minetest's relevant features, including:
- Most `paramtype2`s (note that color is ignored for colored types)
- `visual_scale`
- World-aligned textures
- Animated textures (only one frame is used)
Some special rendering features are unsupported, including texture modifiers,
overlay textures, and node coloring.
### Notes on cubic nodes
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
Textures are licensed under [CC BY 4.0][2]. Everything else (including source code)
is licensed under the GNU LGPL v3.0.
[2]: https://creativecommons.org/licenses/by/4.0/
meshport-0.2.2/export.lua 0000664 0000000 0000000 00000063124 14163517455 0015421 0 ustar 00root root 0000000 0000000 --[[
Copyright (C) 2021 random-geek (https://github.com/random-geek)
Minetest: Copyright (C) 2010-2021 celeron55, Perttu Ahola
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 .
]]
-- Much of the mesh generation code in this file is derived from Minetest's
-- MapblockMeshGenerator class. See minetest/src/client/content_mapblock.cpp.
local S = meshport.S
local vec = vector.new -- Makes defining tables of vertices a little less painful.
--[[
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 = 2,
liquid = 3,
normal = 4,
plantlike_rooted = 4, -- base of plantlike_rooted is equivalent to `normal`.
}
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 i = 1, 6 do
local drawFace
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 = 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
-- 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 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, pos, facedir)
end
local function create_mesh_node(nodeDef, param2, 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.log(playerName, "warning", S("Mesh \"@1\" is not supported.", meshName))
else
meshport.log(playerName, "warning", S("Mesh \"@1\" 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
-- 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
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
-- Plant rotation is slightly different from normal wallmounted rotation.
local PLANTLIKE_WALLMOUNTED_TO_FACEDIR = {[0] = 20, 0, 16, 14, 11, 5}
local function create_plantlike_node(pos, param2, nodeDef)
local isRooted = nodeDef.drawtype == "plantlike_rooted"
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
local faces = meshport.Faces:new()
if isRooted then
-- Place plant above the center node.
offset.y = 1
end
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
if height == 0 then
-- No height, no plant!
-- But seriously, zero-area faces cause problems with Blender.
return faces
end
end
local function create_plantlike_quad(faceRotation, topOffset, bottomOffset)
-- Use Faces, even though it's just one face.
local face = meshport.Faces:new()
local plantHeight = 2.0 * scale * height
local norm = vector.normalize(vector.new(0, bottomOffset - topOffset, plantHeight))
face: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 = 1}, {x = 1, y = 1}, {x = 1, y = 1 - height}, {x = 0, y = 1 - height}},
vert_norms = {norm, norm, norm, norm},
tile_idx = 0,
use_special_tiles = isRooted,
})
face:rotate_xz_degrees(faceRotation + rotation)
local faceOffset = vector.new(offset)
if randomOffsetY then
local seed = faceNum + (pos.x % 0xFF) * 0x10000 + (pos.z % 0xFF) * 0x100 + (pos.y % 0xFF) * 0x1000000
local yRng = PseudoRandom(seed)
faceOffset.y = faceOffset.y - ((yRng:next() % 16) / 16 * 0.125)
faceNum = faceNum + 1
end
face:translate(faceOffset)
return face
end
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
-- TODO: Support facedir if added.
if nodeDef.paramtype2 == "wallmounted" or nodeDef.paramtype2 == "colorwallmounted" then
local facedir = PLANTLIKE_WALLMOUNTED_TO_FACEDIR[param2 % 8] or 0
faces:rotate_by_facedir(facedir)
end
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(vContent[idx])
if nodeDef.drawtype == "airlike" then
return
end
local pos = area:position(idx)
local nodeDrawtype = meshport.get_aliased_drawtype(nodeDef.drawtype)
local neighbors, faces
if CUBIC_FACE_PRIORITY[nodeDrawtype] or nodeDrawtype == "nodebox" then
neighbors = meshport.get_node_neighbors(vContent, area, idx)
end
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(plantPos, vParam2[idx], nodeDef)
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 = create_nodebox_node(pos, vContent[idx], vParam2[idx], neighbors)
elseif nodeDrawtype == "mesh" then
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
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.log(playerName, "info", S("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 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()
-- Loop through all positions in the desired area.
for idx in vArea:iterp(p1, p2) do
-- Generate a mesh for the node.
local faces = create_node(idx, vArea, vContent, vParam2, playerName)
if faces then
-- Move the node to its proper position.
faces:translate(vector.subtract(vArea:position(idx), meshOrigin))
-- Add faces to our final mesh.
mesh:insert_faces(faces)
end
end
minetest.mkdir(path)
mesh:write_obj(path)
mesh:write_mtl(path, playerName)
cleanup_resources()
meshport.log(playerName, "info", S("Finished. Saved to @1", path))
end
meshport-0.2.2/init.lua 0000664 0000000 0000000 00000020707 14163517455 0015043 0 ustar 00root root 0000000 0000000 --[[
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 .
]]
meshport = {
player_data = {},
S = minetest.get_translator("meshport"),
}
modpath = minetest.get_modpath("meshport")
dofile(modpath .. "/utils.lua")
dofile(modpath .. "/mesh.lua")
dofile(modpath .. "/parse_obj.lua")
dofile(modpath .. "/nodebox.lua")
dofile(modpath .. "/export.lua")
local S = meshport.S
local vec = vector.new
minetest.register_privilege("meshport", S("Can save meshes with Meshport."))
minetest.register_on_leaveplayer(function(player, timed_out)
local name = player:get_player_name()
meshport.player_data[name] = nil
end)
for n = 1, 2 do
local tex = "meshport_corner_" .. n .. ".png"
minetest.register_entity("meshport:corner_" .. n, {
initial_properties = {
physical = false,
visual = "cube",
visual_size = {x = 1.04, y = 1.04, z = 1.04},
selectionbox = {-0.52, -0.52, -0.52, 0.52, 0.52, 0.52},
textures = {tex, tex, tex, tex, tex, tex},
static_save = false,
glow = minetest.LIGHT_MAX,
},
on_punch = function(self, hitter)
self.object:remove()
end,
})
end
minetest.register_entity("meshport:border", {
initial_properties = {
physical = false,
visual = "upright_sprite",
textures = {
"meshport_border.png",
"meshport_border.png^[transformFX",
},
static_save = false,
glow = minetest.LIGHT_MAX,
},
on_punch = function(self, hitter)
if not hitter then
return
end
local playerName = hitter:get_player_name()
if not playerName then
return
end
local borders = meshport.player_data[playerName].borders
for i = 1, 6 do -- Remove all borders at once.
if borders[i] then
borders[i]:remove()
borders[i] = nil
end
end
end,
})
local SIDE_ROTATIONS = {
vec(0.5 * math.pi, 0, 0), -- Y+
vec(1.5 * math.pi, 0, 0), -- Y-
vec(0, 1.5 * math.pi, 0), -- X+
vec(0, 0.5 * math.pi, 0), -- X-
vec(0, 0, 0), -- Z+
vec(0, math.pi, 0), -- Z-
}
local function mark_borders(playerData)
local pos1, pos2 = vector.sort(playerData.pos[1], playerData.pos[2])
local center = vector.multiply(vector.add(pos1, pos2), 0.5)
-- Add 0.01 to avoid z-fighting with blocks or corner markers.
local c1, c2 = vector.subtract(pos1, 0.5 + 0.01), vector.add(pos2, 0.5 + 0.01)
local sideCenters = {
vec(center.x, c2.y, center.z), -- Y+
vec(center.x, c1.y, center.z), -- Y-
vec(c2.x, center.y, center.z), -- X+
vec(c1.x, center.y, center.z), -- X-
vec(center.x, center.y, c2.z), -- Z+
vec(center.x, center.y, c1.z), -- Z-
}
local size = vector.subtract(c2, c1)
local sideSizes = {
{x = size.x, y = size.z}, -- Y+
{x = size.x, y = size.z}, -- Y-
{x = size.z, y = size.y}, -- X+
{x = size.z, y = size.y}, -- X-
{x = size.x, y = size.y}, -- Z+
{x = size.x, y = size.y}, -- Z-
}
local half = vector.multiply(size, 0.5)
local selectionBoxes = {
{-half.x, -0.02, -half.z, half.x, 0, half.z}, -- Y+
{-half.x, 0, -half.z, half.x, 0.02, half.z}, -- Y-
{-0.02, -half.y, -half.z, 0, half.y, half.z}, -- X+
{0, -half.y, -half.z, 0.02, half.y, half.z}, -- X-
{-half.x, -half.y, -0.02, half.x, half.y, 0}, -- Z+
{-half.x, -half.y, 0, half.x, half.y, 0.02}, -- Z-
}
for i = 1, 6 do
local entity = minetest.add_entity(sideCenters[i], "meshport:border")
entity:set_properties({
visual_size = sideSizes[i],
selectionbox = selectionBoxes[i],
})
entity:set_rotation(SIDE_ROTATIONS[i])
playerData.borders[i] = entity
end
end
local function set_position(playerName, n, pos)
if not meshport.player_data[playerName] then
meshport.player_data[playerName] = {
pos = {},
corners = {},
borders = {},
}
end
local data = meshport.player_data[playerName]
data.pos[n] = pos
if data.corners[n] then
data.corners[n]:remove()
end
data.corners[n] = minetest.add_entity(pos, "meshport:corner_" .. n)
for i = 1, 6 do
if data.borders[i] then
data.borders[i]:remove()
data.borders[i] = nil
end
end
if data.pos[1] and data.pos[2] then
mark_borders(data)
end
meshport.log(playerName, "info", S("Position @1 set to @2.", n, minetest.pos_to_string(pos)))
end
for n = 1, 2 do
minetest.register_chatcommand("mesh" .. n, {
params = "[pos]",
description = S(
"Set position @1 for Meshport. Player's position is used if no other position is specified.", n),
privs = {meshport = true},
func = function(playerName, param)
local pos
if param == "" then
pos = minetest.get_player_by_name(playerName):get_pos()
else
pos = minetest.string_to_pos(param)
end
if not pos then
meshport.log(playerName, "error", S("Not a valid position."))
return
end
pos = vector.round(pos)
set_position(playerName, n, pos)
end,
})
end
local function on_wand_click(itemstack, player, pointedThing, n)
if not player or pointedThing.type == "nothing" then
return
end
local playerName = player:get_player_name()
if not minetest.check_player_privs(playerName, "meshport") then
meshport.log(playerName, "error", S("You must have the meshport privilege to use this tool."))
return
end
local pos
if pointedThing.type == "node" then
if player:get_player_control().sneak then
pos = pointedThing.above
else
pos = pointedThing.under
end
elseif pointedThing.type == "object" then
local entity = pointedThing.ref:get_luaentity()
if entity.name == "meshport:border" then
return
end
pos = vector.round(pointedThing.ref:get_pos())
else
return -- In case another pointed_thing.type is added
end
set_position(playerName, n, pos)
end
minetest.register_tool("meshport:wand", {
description = S("Meshport Area Selector\nLeft-click to set 1st corner, right-click to set 2nd corner."),
short_description = S("Meshport Area Selector"),
inventory_image = "meshport_wand.png",
on_use = function(itemstack, placer, pointedThing) -- Left-click
on_wand_click(itemstack, placer, pointedThing, 1)
end,
on_place = function(itemstack, placer, pointedThing) -- Right-click
on_wand_click(itemstack, placer, pointedThing, 2)
return itemstack -- Required by on_place
end,
on_secondary_use = function(itemstack, placer, pointedThing) -- Right-click on non-node
on_wand_click(itemstack, placer, pointedThing, 2)
end,
})
minetest.register_chatcommand("meshrst", {
description = S("Clear the current Meshport area."),
privs = {meshport = true},
func = function(playerName, param)
local data = meshport.player_data[playerName]
if data then
for n = 1, 2 do
data.pos[n] = nil
if data.corners[n] then
data.corners[n]:remove()
data.corners[n] = nil
end
end
for i = 1, 6 do
if data.borders[i] then
data.borders[i]:remove()
data.borders[i] = nil
end
end
end
meshport.log(playerName, "info", S("Cleared the current area."))
end,
})
minetest.register_chatcommand("meshport", {
params = "[filename]",
description = S("Save a mesh of the selected area (filename optional)."),
privs = {meshport = true},
func = function(playerName, filename)
local playerData = meshport.player_data[playerName] or {}
if not (playerData.pos and playerData.pos[1] and playerData.pos[2]) then
meshport.log(playerName, "error",
S("No area selected. Use the Meshport Area Selector or /mesh1 and /mesh2 to select an area."))
return
end
if filename:find("[^%w-_]") then
meshport.log(playerName, "error",
S("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")
end
local mpPath = minetest.get_worldpath() .. "/" .. "meshport"
local folderName = playerName .. "_" .. filename
if table.indexof(minetest.get_dir_list(mpPath, true), folderName) > 0 then
meshport.log(playerName, "error",
S("Folder \"@1\" already exists. Try using a different name.", folderName))
return
end
local path = mpPath .. "/" .. folderName
meshport.create_mesh(playerName, playerData.pos[1], playerData.pos[2], path)
end,
})
meshport-0.2.2/locale/ 0000775 0000000 0000000 00000000000 14163517455 0014626 5 ustar 00root root 0000000 0000000 meshport-0.2.2/locale/template.txt 0000664 0000000 0000000 00000001616 14163517455 0017206 0 ustar 00root root 0000000 0000000 # textdomain:meshport
Warning: @1
Error: @1
Can save meshes with Meshport.
Position @1 set to @2.
Set position @1 for Meshport. Player's position is used if no other position is specified.
Not a valid position.
You must have the meshport privilege to use this tool.
Meshport Area Selector@\nLeft-click to set 1st corner, right-click to set 2nd corner.
Meshport Area Selector
Clear the current Meshport area.
Cleared the current area.
Save a mesh of the selected area (filename optional).
No area selected. Use the Meshport Area Selector or /mesh1 and /mesh2 to select an area.
Invalid name supplied. Please use valid characters: [A-Z][a-z][0-9][-_]
Folder "@1" already exists. Try using a different name.
Mesh "@1" is not supported.
Mesh "@1" could not be found.
Generating mesh...
Finished. Saved to @1
Ignoring texture modifers in material "@1".
Could not find texture "@1". Using a dummy material instead.
meshport-0.2.2/materials.py 0000664 0000000 0000000 00000003442 14163517455 0015725 0 ustar 00root root 0000000 0000000 # Original script: random-geek
# Contributors: GreenXenith, sbrl, VorTechnix
### Use better materials and support alpha ###
# Usage: Open or copy script in Blender
# Run script WHILE OBJECT IS SELECTED
import bpy
targetMats = []
for obj in bpy.context.selected_objects:
for slot in obj.material_slots:
mat = slot.material
if mat not in targetMats:
targetMats.append(mat)
for mat in targetMats:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
# Remove all nodes except texture
for node in nodes:
if node.bl_idname != "ShaderNodeTexImage":
nodes.remove(node)
# Get texture node
try:
tex = nodes["Image Texture"]
except KeyError:
print(f"[materials.py] Skipped material '{mat.name}': Image texture not found.")
continue
# Change image interpolation
tex.interpolation = "Closest"
tex.location = 0, 0
# Create texture coordinate node
coord = nodes.new("ShaderNodeTexCoord")
coord.location = -400, 0
# Create mapping node
map = nodes.new("ShaderNodeMapping")
map.location = -200, 0
# Create principled shader
prin = nodes.new("ShaderNodeBsdfPrincipled")
prin.location = 300, 0
prin.inputs["Specular"].default_value = 0
# Create output
out = nodes.new("ShaderNodeOutputMaterial")
out.location = 600, 0
# Link everything
links.new(coord.outputs["UV"], map.inputs["Vector"])
links.new(map.outputs["Vector"], tex.inputs["Vector"])
links.new(tex.outputs["Color"], prin.inputs["Base Color"])
links.new(tex.outputs["Alpha"], prin.inputs["Alpha"])
links.new(prin.outputs["BSDF"], out.inputs["Surface"])
# Deselect all
for node in nodes:
node.select = False
# Set blend mode
mat.blend_method = "CLIP"
meshport-0.2.2/mesh.lua 0000664 0000000 0000000 00000022011 14163517455 0015022 0 ustar 00root root 0000000 0000000 --[[
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 .
]]
local S = meshport.S
--[[
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", S("Ignoring texture modifers in material \"@1\".", mat))
end
matFile:write(string.format("map_Kd %s\n", meshport.texture_paths[texName]))
else
meshport.log(playerName, "warning",
S("Could not find texture \"@1\". 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
meshport-0.2.2/mod.conf 0000664 0000000 0000000 00000000132 14163517455 0015011 0 ustar 00root root 0000000 0000000 name = meshport
description = Easily export areas in Minetest to meshes for 3D rendering.
meshport-0.2.2/nodebox.lua 0000664 0000000 0000000 00000016667 14163517455 0015550 0 ustar 00root root 0000000 0000000 --[[
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 .
]]
local SIDE_BOX_NAMES = {
"top", -- Y+
"bottom", -- Y-
"right", -- X+
"left", -- X-
"back", -- Z+
"front", -- Z-
}
local function 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
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" 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}
end
setmetatable(o, self)
self.__index = self
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, 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()
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(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 _, box in ipairs(self.boxes) do
local b = sort_box(box)
local sideFaces = {
{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 = 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-
}
for i = 1, 6 do
local norm = meshport.NEIGHBOR_DIRS[i]
faces:insert_face(meshport.prepare_cuboid_face({
verts = sideFaces[i],
tex_coords = sideTexCoords[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
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(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 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, param2, facedir, neighbors)
local boxes = meshport.Boxes:new()
if prepNodebox.fixed then
if prepNodebox.type == "leveled" then
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
end
if prepNodebox.type == "connected" then
local neighborName
for i = 1, 6 do
neighborName = minetest.get_name_from_content_id(neighbors[i])
if 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
meshport-0.2.2/parse_obj.lua 0000664 0000000 0000000 00000010342 14163517455 0016036 0 ustar 00root root 0000000 0000000 --[[
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 .
]]
-- 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
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
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(faceStr, " ")
local face = {
verts = {},
tex_coords = {},
vert_norms = {},
}
for i, vertStr in ipairs(vertStrs) do
-- 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")
-- 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 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 currentTileIdx
for line in file:lines() do
-- elementStr may be an empty string, e.g. "g" with no group name.
local elementType, elementStr = string.match(line, "^(%a+)%s*(.*)")
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 currentTileIdx then
table.insert(groups, 0)
currentTileIdx = #groups
end
-- Parse the face element.
local face = parse_face_element(elements, elementStr)
-- Assign materials according to the group.
face.tile_idx = currentTileIdx
faces:insert_face(face)
elseif elementType == "g" then
currentTileIdx = handle_group(groups, elementStr)
end
end
return faces
end
meshport-0.2.2/screenshot.png 0000664 0000000 0000000 00002152105 14163517455 0016260 0 ustar 00root root 0000000 0000000 PNG
IHDR th pHYs IDATxĽK4۲#̪{cq%;n .=hk4h!!t`{w?oʜ1hdfUfc̬YzTeeG#"FD( |/~Z}XM\5ڷ +G8o1;֦c2l|
g =; /-X =̋Fz{flLWg+n;؝Ϭ_nl4f<&k=J&hcڦ'EGIKƇOeWy}wpn\1Vϼ+]'HBtbKbTyts{~<Ӷ`Vd+(Eמ)FnLUxb.>3nmYFCC'[c7y,Dftszم/돉X+\oٹal\ etP:/NXD[pKr=mo:^G5f=gDM*يtm-=rǜw`xG|f&4w<=ݿ WU2f
aH(Xp]ˣ:Ȏ{-l16/ppvlWS:iܵ-7w6m}
dKE}CGϭhwmD52Wxy5XN$'Cܰ 3Ȼ1Ƽ$:
ǖ10ss,;Kc=9&{}rvP%X_Xhr0X}#,{Gb݄LQ8`Հ)ڭJJlGڲ
ʽHY89@x#q9lxRX.<8=|~ )Փ#"czoD$i