Skip to content

Instantly share code, notes, and snippets.

@hhrhhr
Last active April 26, 2021 19:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hhrhhr/ee63fbcd280f6110cc2f8184da857964 to your computer and use it in GitHub Desktop.
Save hhrhhr/ee63fbcd280f6110cc2f8184da857964 to your computer and use it in GitHub Desktop.
Subnautica Below Zero map maker
@echo off
rem ==== start of user settings ====
rem minimum level (0...24)
set LEVEL=16
rem input raw files from AssetStudio (https://github.com/Perfare/AssetStudio)
set MESH_DIR=h:\raw
rem output directories
set OBJ_DIR=h:\obj
set LEVEL_DIR=h:\level
set LUA=lua54_static.exe
rem ==== end of user settings ====
set LUA_DIR=%~dp0%
rem goto :merge
:convert
if not exist "%OBJ_DIR%" mkdir "%OBJ_DIR%"
for /r "%MESH_DIR%" %%i in (*.dat) do (
"%LUA%" "%LUA_DIR%\mesh2obj_bz.lua" "%%i" "%OBJ_DIR%" %LEVEL%
if ERRORLEVEL 1 goto eof
)
:merge
if not exist "%LEVEL_DIR%" mkdir "%LEVEL_DIR%"
pushd "%LEVEL_DIR%"
for /l %%i in (%LEVEL% 1 24) do (
echo merge level %%i
copy /y "%OBJ_DIR%\*-%%i-*.obj" level-%%i.obj >NUL 2>&1
)
popd
:eof
pause
local FN = arg[1] or [[h:\sub_raw\unnamed asset-resources.assets-10072.dat]]
local DIR = arg[2] or [[h:\sub_obj]]
local SKIP = tonumber(arg[3]) or 0
print(FN)
if "\\" ~= DIR:sub(-1) then
DIR = DIR .. "\\"
end
local function writeObj(obj)
local withNormal = obj.normal[1] and true or false
local n = withNormal and "_wn" or ""
local fn = DIR .. obj.name:sub(7) .. n .. ".obj"
local w = assert(io.open(fn, "w+b"))
local fmt = {
head = "#o n_%s\n# %d points, %d faces\n",
point = "v %f %f %f\n",
normal = "vn %f %f %f\n",
group = "g g_%s\ns 1\n",
face_n = "f -%d//-%d -%d//-%d -%d//-%d\n",
face = "f -%d -%d -%d\n"
}
w:write(fmt.head:format(obj.name, #obj.point, #obj.face))
for i = 1, #obj.point do
local p = obj.point[i]
w:write(fmt.point:format(p[1], p[2], p[3]))
end
if withNormal then
for i = 1, #obj.normal do
local n = obj.normal[i]
w:write(fmt.normal:format(n[1], n[2], n[3]))
end
w:write(fmt.group:format(obj.name))
for i = 1, #obj.face do
local f = obj.face[i]
w:write(fmt.face_n:format(f[1], f[1], f[2], f[2], f[3], f[3]))
end
else
w:write(fmt.group:format(obj.name))
for i = 1, #obj.face do
local f = obj.face[i]
w:write(fmt.face:format(f[1], f[2], f[3]))
end
end
w:close()
end
local function convert()
local m = require("mesh_utils_bz")
local obj
if SKIP >= 0 then
local level = m.open(FN) -- get fileSize, name, offset
if level >= SKIP then
obj = m.processMesh()
writeObj(obj)
end
else
m.just_open(FN)
obj = m.processMesh()
writeObj(obj)
end
end
convert()
local m = {}
local obj = {}
local r
local su = string.unpack
local function uint8()
local res, _ = su("B", r:read(1))
return res
end
local function uint16()
local res, _ = su("H", r:read(2))
return res
end
local function uint32()
local res, _ = su("I4", r:read(4))
return res
end
local function str(len)
return r:read(len)
end
local function float()
local res, _ = su("f", r:read(4))
if res ~= res then res = 0.0 end
return res
end
local function seek(n) r:seek("cur", n) end
local function padding()
local pos = r:seek()
pos = pos % 4
if pos > 0 then
seek(4 - pos)
end
return r:seek()
end
local function pos() print("!!! pos:" .. r:seek()) end
local function string()
local sz = uint32()
local name = str(sz)
padding()
return name
end
local function AABB()
local t = {}
t.center = {float(), float(), float()}
t.extent = {float()*2, float()*2, float()*2}
-- print(("center: %s\nextent: %s"):format(
-- table.concat(t.center, ", "), table.concat(t.extent, ", ")))
return t
end
local function SubMeshes() -- vector
local sz = uint32()
assert(1 == sz, sz)
seek(4) -- firstByte
obj.indexCount = uint32()
seek(4*3) -- topology, baseVertex, firstVertex
obj.points = uint32() -- vertexCount
obj.AABB = AABB()
print(("%s:\t%d points, %d faces"):format(obj.name, obj.points, obj.indexCount//3))
-- print(("center: %s\nextent: %s"):format(
-- table.concat(obj.AABB.center, ", "), table.concat(obj.AABB.extent, ", ")))
end
local function BlendShapeData()
-- // BlendShapeData Shapes
assert(0 == uint32()) -- vector vertices
assert(0 == uint32()) -- vector shape
assert(0 == uint32()) -- vector channels
assert(0 == uint32()) -- vector fullWeights
-- \\ BlendShapeData Shapes
end
local function vars()
assert(0 == uint32()) -- vector BindPose
assert(0 == uint32()) -- vector BoneNameHashes
seek(4*1) -- unsigned int RootBoneNameHash
assert(0 == uint32()) -- vector BonesAABB
assert(0 == uint32()) -- VariableBoneCountWeights VariableBoneCountWeights
obj.MeshCompression = uint8() -- UInt8 MeshCompression
seek(3*1 + 4) -- bool IsReadable, KeepVertices, KeepIndices, int IndexFormat
end
local function other()
local localAABB = AABB()
seek(4) -- MeshUsageFlags
assert(0 == uint32()) -- vector BakedConvexCollisionMesh
assert(0 == uint32()) -- vector BakedTriangleCollisionMesh
seek(4) -- float MeshMetrics[0]
seek(4) -- float MeshMetrics[1]
-- StreamingInfo StreamData
seek(4) -- unsigned int offset
seek(4) -- unsigned int size
local sz = uint32()
seek(sz) -- string path
end
local function bitUnpacker(t)
local result = {}
local idx = 1
local sz = 0
local buf = 0
local mask = (1 << t.bitSize) - 1
-- local num = 0
-- while num < t.count do
for _ = 1, t.count do
local buf_add
while sz < t.bitSize do
buf_add, idx = su("B", t.data, idx)
buf = (buf_add << sz) + buf
sz = sz + 8
end
local res = buf & mask
table.insert(result, res)
-- num = num + 1
buf = buf >> t.bitSize
sz = sz - t.bitSize
--print("num", num, "res", res, "buf", buf, "sz", sz)
end
return result
end
local function packedBitVector(str, withOpt)
local t = {}
local p1 = r:seek()
t.count = uint32()
if withOpt then
t.range = float()
t.start = float()
end
t.sz = uint32()
t.data = r:read(t.sz)
padding()
t.bitSize = uint8()
local p2 = padding()
-- print(("%12s count:%6d, %10f %10f, sz:%6d, b:%2d, %d-%d"):format(
-- str, t.count, t.range or 0, t.start or 0, t.sz, t.bitSize, p1, p2))
return t
end
local function processVertices(v)
local point = {}
local scale = v.range -- t.start -- CHECK
local o = obj.offset
if v.bitSize == 16 then
local h, pos = 0, 1
for i = 1, v.count, 3 do
local pp = {}
for j = 1, 3 do
h, pos = su("H", v.data, pos)
h = (h * scale / 65535.0) + v.start
table.insert(pp, h)
end
-- fix coord
pp[1] = o.x + pp[1]
pp[2] = o.y + pp[2]
pp[3] = o.z - pp[3]
table.insert(point, pp)
end
else
assert(false, "not implemented")
end
return point
end
local function processNormals(n, s)
local sign = {}
local tmp = bitUnpacker(s)
for i = 1, #tmp do
sign[i] = tmp[i] == 1 and 1 or -1
end
local norm = {}
tmp = bitUnpacker(n)
local scale = n.range -- t.start -- CHECK
for i = 1, #tmp, 2 do
local n1 = tmp[i] * scale / 255.0 + n.start
local n2 = tmp[i+1] * scale / 255.0 + n.start
local n3s = sign[(i+1)/2]
local n3 = (1.0 - (n1^2 + n2^2))
n3 = math.abs(n3)^0.5 * n3s
table.insert(norm, {n1, n2, -n3})
end
return norm
end
local function processFaces(t)
-- for i = 1, faces do
-- local f1, f2, f3 = points-uint16(), points-uint16(), points-uint16()
-- table.insert(face, { f1, f3, f2 })
-- end
local face = {}
local tmp = bitUnpacker(t)
--assert(t.count == #tmp, t.count .. " ~= " .. #tmp)
local p = obj.points
for i = 1, t.count, 3 do
local ff = {p - tmp[i], p - tmp[i+2], p - tmp[i+1]}
table.insert(face, ff)
end
--assert(t.count // 3 == #f)
return face
end
local function compressedMesh()
local t = {}
assert(0 == uint32()) -- vector IndexBuffer
-- // VertexData VertexData
assert(0 == uint32()) -- unsigned int VertexCount
sz = uint32() -- vector m_Channels
seek(sz * 4) -- ChannelInfo data, 4 * uint8
assert(0 == uint32()) -- TypelessData DataSize
-- \\ VertexData VertexData
-- CompressedMesh CompressedMesh
local v = packedBitVector("Vertices", true)
packedBitVector("UV", true)
local n = packedBitVector("Normals", true, true)
packedBitVector("Tangents", true)
packedBitVector("Weights")
local s = packedBitVector("NormalSigns")
packedBitVector("TangentSigns")
packedBitVector("FloatColors")
packedBitVector("BoneIndices")
seek(8) -- ???
local f = packedBitVector("Triangles")
seek(4*1) -- unsigned int UVInfo
obj.point = processVertices(v)
obj.normal = {} --processNormals(n, s)
obj.face = processFaces(f)
end
local function uncompressedMesh()
obj.face = {}
local p = obj.points
-- // vector IndexBuffer
local sz = uint32()
for i = 1, sz // 6 do
local f1, f2, f3 = p-uint16(), p-uint16(), p-uint16()
table.insert(obj.face, { f1, f3, f2 })
end
-- \\ vector IndexBuffer
-- // VertexData VertexData
sz = uint32()
-- assert(points == sz)
sz = uint32() -- vector m_Channels
seek(sz * 4) -- ChannelInfo data, 4 * uint8
sz = uint32()
-- assert(points == sz // 24)
obj.point = {}
obj.normal = {}
local o = obj.offset
for _ = 1, obj.points do
local x, y, z = float() + o.x, float() + o.y, o.z - float()
table.insert(obj.point, { x, y, z })
-- table.insert(obj.normal, { float(), float(), -float() })
seek(4*3)
end
-- \\ VertexData VertexData
-- CompressedMesh CompressedMesh
packedBitVector("Vertices", true)
packedBitVector("UV", true)
packedBitVector("Normals", true)
packedBitVector("Tangents", true)
packedBitVector("Weights")
packedBitVector("NormalSigns")
packedBitVector("TangentSigns")
packedBitVector("FloatColors")
packedBitVector("BoneIndices")
seek(8) -- ???
packedBitVector("Triangles")
seek(4*1) -- unsigned int UVInfo
end
function m.open(fn)
r = assert(io.open(fn, "rb"))
obj.fileSize = r:seek("end")
r:seek("set")
obj.name = string()
for x, y, z in obj.name:gmatch("-(.+)-(.+)-(.+)") do
obj.offset = {x = x * 32.0, y = y * 32.0, z = z * -32.0}
end
return obj.offset.y // 32
end
function m.just_open(fn)
r = assert(io.open(fn, "rb"))
obj.fileSize = r:seek("end")
r:seek("set")
obj.name = string()
obj.offset = {x = 0.0, y = 0.0, z = 0.0}
end
function m.processMesh()
SubMeshes()
BlendShapeData()
vars()
if 0 == obj.MeshCompression then uncompressedMesh()
elseif 2 == obj.MeshCompression then compressedMesh()
else assert(false, obj.MeshCompression)
end
other()
local pos = r:seek()
assert(obj.fileSize == pos, obj.fileSize .. " ~= " .. pos)
return obj
end
return m
@hhrhhr
Copy link
Author

hhrhhr commented Apr 26, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment