Виртуальный полигон для дрона
Автор
#!/usr/bin/env julia
#
# obj2bin.jl — упаковка OBJ-меша в компактный бинарный файл для WebGL.
#
# Формат DRN1 (little-endian):
# 0..3 "DRN1" сигнатура
# 4..7 UInt32 nVerts число вершин
# 8..11 UInt32 nIndices число индексов (3 * число треугольников)
# 12 UInt8 indexWidth 2 => UInt16, 4 => UInt32
# 13..15 3 байта паддинг (чтобы Float32 был выровнен на 4)
# 16.. Float32 positions[3*nVerts] x,y,z,x,y,z,...
# ... UInt16|UInt32 indices[nIndices]
#
# Запуск:
# julia obj2bin.jl drone_model.obj # -> drone_model.bin
# julia obj2bin.jl drone_model.obj public/mesh.bin
# julia obj2bin.jl drone_model.obj public/mesh.bin --dedup
#
# Чтение в браузере (без JSON.parse):
# const buf = await (await fetch('/mesh.bin')).arrayBuffer();
# const dv = new DataView(buf);
# const nV = dv.getUint32(4, true), nI = dv.getUint32(8, true);
# const iw = dv.getUint8(12);
# const pos = new Float32Array(buf, 16, nV * 3);
# const tris = (iw === 2) ? new Uint16Array(buf, 16 + nV * 12, nI)
# : new Uint32Array(buf, 16 + nV * 12, nI);
# // pos -> gl.bufferData(ARRAY_BUFFER, pos) ; tris -> ELEMENT_ARRAY_BUFFER
#
# Поддерживается: v, f (v / v/vt / v//vn / v/vt/vn), отрицательные индексы,
# многоугольники (триангуляция веером), CRLF, комментарии.
ENDIAN_BOM == 0x04030201 || error("obj2bin: нужен little-endian процессор")
"Читает OBJ, возвращает (verts::Vector{Float32}, faces::Vector{NTuple{3,Int}})."
function parse_obj(path::String; dedup::Bool = false)
verts = Float32[] # плоский список x,y,z
faces = NTuple{3,Int}[] # уже триангулированные
for raw in eachline(path)
s = strip(raw)
(isempty(s) || s[1] == '#') && continue
p = split(s)
key = p[1]
if key == "v"
length(p) >= 4 || continue
push!(verts, parse(Float32, p[2]), parse(Float32, p[3]), parse(Float32, p[4]))
elseif key == "f"
n = length(verts) ÷ 3
idx = Int[]
for tok in p[2:end]
j = parse(Int, split(tok, '/')[1])
push!(idx, j < 0 ? n + 1 + j : j) # отрицательный индекс — от конца списка
end
length(idx) < 3 && continue
for t in 2:length(idx)-1 # веер: (1, t, t+1)
push!(faces, (idx[1], idx[t], idx[t+1]))
end
end
end
(length(verts) ÷ 3) == 0 && error("в файле нет вершин: " * path)
isempty(faces) && error("в файле нет граней: " * path)
if dedup
vmap = Dict{NTuple{3,Float32},Int}()
newv = Float32[]
newf = Vector{NTuple{3,Int}}(undef, length(faces))
for (i, f) in enumerate(faces)
nf = ntuple(3) do k
o = 3 * (f[k] - 1)
key = (verts[o+1], verts[o+2], verts[o+3])
get!(vmap, key) do
push!(newv, key...)
length(newv) ÷ 3
end
end
newf[i] = nf
end
verts = newv
faces = newf
end
return verts, faces
end
"Пишет бинарный меш DRN1, возвращает (nVerts, nIndices, indexWidth)."
function write_bin(path::String, verts::Vector{Float32}, faces::Vector{NTuple{3,Int}})
nv = length(verts) ÷ 3
ni = 3 * length(faces)
flat = Vector{UInt32}(undef, ni)
for (i, f) in enumerate(faces)
flat[3i-2] = UInt32(f[1] - 1)
flat[3i-1] = UInt32(f[2] - 1)
flat[3i] = UInt32(f[3] - 1)
end
if nv <= 65535
idx = UInt16.(flat)
iw = 2
else
idx = flat
iw = 4
end
open(path, "w") do io
write(io, codeunits("DRN1"))
write(io, UInt32(nv))
write(io, UInt32(ni))
write(io, UInt8(iw))
write(io, UInt8[0x00, 0x00, 0x00])
write(io, verts)
write(io, idx)
end
return nv, ni, iw
end
function main(args)
isempty(args) && (println("usage: julia obj2bin.jl model.obj [out.bin] [--dedup]"); return)
dedup = "--dedup" in args
pos = [a for a in args if !startswith(a, "--")]
src = pos[1]
dst = length(pos) >= 2 ? pos[2] :
(endswith(lowercase(src), ".obj") ? src[1:end-4] * ".bin" : src * ".bin")
isfile(src) || error("нет файла: " * src)
verts, faces = parse_obj(src; dedup = dedup)
nv, ni, iw = write_bin(dst, verts, faces)
# «до»: так меш лежал в HTML у старой версии (JSON-текст в двух div-ах)
longest = [v for f in faces for v in f]
json_v = "[" * join(string.(verts), ",") * "]"
json_i = "[" * join(string.(longest), ",") * "]"
before = sizeof(json_v) + sizeof(json_i)
after = filesize(dst)
println("OBJ : ", src, " (", filesize(src), " Б)")
println("вершин : ", nv, " треугольников: ", length(faces))
println("индексы : UInt", 8 * iw, iw == 2 ? " (вершин <= 65535)" : " (нужен OES_element_index_uint)")
println("JSON в HTML: ", before, " Б (VERTEX_JSON ", sizeof(json_v), " + INDEX_JSON ", sizeof(json_i), ")")
println("mesh.bin : ", dst, " (", after, " Б)")
println("сжатие : ", round(before / after, digits = 2), "x меньше JSON, ",
round(100 * (1 - after / before), digits = 1), "% экономии")
println("плотность : ", round(after / nv, digits = 1), " Б на вершину (Float32 поз. + индекс)")
end
main(ARGS)