.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # gsbox
7 |
8 | A cross-platform command-line tool for 3D Gaussian Splatting, focusing on format conversion and optimization.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Features
19 | - [x] Conversion between file formats, supporting `.ply`, `.splat`, `.spx`, and `.spz` formats for 3DGS.
20 | - [x] Viewing file header information for `.ply`, `.spx`, and `.spz` files, or simple information of `.splat`.
21 | - [x] Supports data transformation (Rotation, Scale, Translation).
22 | - [x] Supports merging multiple model files into one.
23 |
24 |
25 | ## `.spz`
26 | - The `.spz` format is an open 3DGS model format. Its encoding algorithm is highly commendable, and combined with gzip compression, it can significantly reduce the size of model files without any noticeable loss in visual quality.
27 | - The official open-source repository for the `.spz` format is available at [spz](https://github.com/nianticlabs/spz). This format is about 10x smaller than the equivalent PLY format and is offered as open source by Niantic Labs. More details can be found at [scaniverse](https://scaniverse.com/spz)
28 | - For rendering and viewing `.spz` format models, you can refer to [GaussianSplats3D](https://github.com/mkkellogg/GaussianSplats3D) or [Reall3dViewer](https://github.com/reall3d-com/Reall3dViewer)
29 |
30 | ## `.spx`
31 | - The `.spx` format is flexible, expandable, and supports proprietary data protection. It incorporates encoding methods from both `.splat` and `.spz` formats and adds block compression processing. It supports progressive loading and is suitable for large file models.
32 | - For detailed information about the `.spx` format, please refer to [SPX Specification](https://github.com/reall3d-com/Reall3dViewer/blob/main/SPX_EN.md)
33 | - To render and view models in the `.spx` format, you can use [Reall3dViewer](https://github.com/reall3d-com/Reall3dViewer). This viewer is built on Three.js and supports features such as marking, measurements, and text watermarks.
34 |
35 | ## Usage
36 | ```shell
37 | Usage:
38 | gsbox [options]
39 |
40 | Options:
41 | p2s, ply2splat convert ply to splat
42 | p2x, ply2spx convert ply to spx
43 | p2z, ply2spz convert ply to spz
44 | p2p, ply2ply convert ply to ply
45 | s2p, splat2ply convert splat to ply
46 | s2x, splat2spx convert splat to spx
47 | s2z, splat2spz convert splat to spz
48 | s2s, splat2splat convert splat to splat
49 | x2p, spx2ply convert spx to ply
50 | x2s, spx2splat convert spx to splat
51 | x2z, spx2spz convert spx to spz
52 | x2x, spx2spx convert spx to spx
53 | z2p, spz2ply convert spz to ply
54 | z2s, spz2splat convert spz to splat
55 | z2x, spz2spx convert spz to spx
56 | z2z, spz2spz convert spz to spz
57 | join join the input model files into a single output file
58 | info display the model file information
59 | -i, --input specify the input file
60 | -o, --output specify the output file
61 | -c, --comment specify the comment for ply/spx output
62 | -bs, --block-size specify the block size for spx output (default 20480)
63 | -sh, --shDegree specify the SH degree for ply/spx/spz output
64 | -f1, --flag1 specify the header flag1 for spx output
65 | -f2, --flag2 specify the header flag2 for spx output
66 | -f3, --flag3 specify the header flag3 for spx output
67 | -rx, --rotateX specify the rotation angle in degrees about the x-axis for transform
68 | -ry, --rotateY specify the rotation angle in degrees about the y-axis for transform
69 | -rz, --rotateZ specify the rotation angle in degrees about the z-axis for transform
70 | -s, --scale specify a uniform scaling factor(0.01~100.0) for transform
71 | -tx, --translateX specify the translation value about the x-axis for transform
72 | -ty, --translateY specify the translation value about the y-axis for transform
73 | -tz, --translateZ specify the translation value about the z-axis for transform
74 | -to, --transform-order specify the transform order (RST/RTS/SRT/STR/TRS/TSR), default is RST
75 | -v, --version display version information
76 | -h, --help display help information
77 |
78 | Examples:
79 | gsbox ply2splat -i /path/to/input.ply -o /path/to/output.splat
80 | gsbox s2x -i /path/to/input.splat -o /path/to/output.spx -c "your comment" -bs 10240
81 | gsbox x2z -i /path/to/input.spx -o /path/to/output.spz -sh 0 -rz 90 -s 0.9 -tx 0.1 -to TRS
82 | gsbox z2p -i /path/to/input.spz -o /path/to/output.ply -c "your comment"
83 | gsbox join -i a.ply -i b.splat -i c.spx -i d.spz -o output.spx
84 | gsbox info -i /path/to/file.spx
85 |
86 |
87 | # Convert the ply to spx without saving SH coefficients and add custom comments.
88 | gsbox p2x -i /path/to/input.ply -o /path/to/output.spx -c "your comment here" -sh 0
89 |
90 | # Inspect the header information of the spx file
91 | gsbox info -i /path/to/file.spx
92 | ```
93 |
94 |
95 | ## Update History & binary files
96 | https://github.com/gotoeasy/gsbox/releases
97 |
--------------------------------------------------------------------------------
/cmn/common.go:
--------------------------------------------------------------------------------
1 | package cmn
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "log"
11 | "math"
12 | "net/http"
13 | "os"
14 | "path/filepath"
15 | "strconv"
16 | "strings"
17 | "time"
18 | )
19 |
20 | const lastestVerUrl = "https://reall3d.com/gsbox/open-lastest.json?v=" + VER
21 |
22 | var NewVersionMessage = ""
23 |
24 | const COLOR_SCALE = 0.15
25 | const SH_C0 float64 = 0.28209479177387814
26 |
27 | const DEG2RAD = math.Pi / 180
28 | const RAD2DEG = 180 / math.Pi
29 |
30 | // 去重
31 | func UniqueStrings(slice []string) []string {
32 | seen := make(map[string]bool)
33 | var result []string
34 |
35 | for _, v := range slice {
36 | if !seen[v] {
37 | seen[v] = true
38 | result = append(result, v)
39 | }
40 | }
41 | return result
42 | }
43 |
44 | // DegToRad :
45 | func DegToRad(degrees float64) float64 {
46 | return degrees * DEG2RAD
47 | }
48 |
49 | // RadToDeg :
50 | func RadToDeg(radians float64) float64 {
51 | return radians * RAD2DEG
52 | }
53 |
54 | func StringToFloat32(s string, defaultVal ...float32) float32 {
55 | v, err := strconv.ParseFloat(s, 64)
56 | if err != nil {
57 | var defaultValue float32
58 | if len(defaultVal) > 0 {
59 | defaultValue = defaultVal[0]
60 | }
61 | return defaultValue
62 | }
63 | return ClipFloat32(v)
64 | }
65 |
66 | func Trim(str string) string {
67 | return strings.TrimSpace(str)
68 | }
69 |
70 | // 字符串切割
71 | func Split(str string, sep string) []string {
72 | return strings.Split(str, sep)
73 | }
74 |
75 | // []byte 转 string
76 | func BytesToString(b []byte) string {
77 | return string(b)
78 | }
79 |
80 | // 判断是否包含(区分大小写)
81 | func Contains(str string, substr string) bool {
82 | return strings.Contains(str, substr)
83 | }
84 |
85 | // 判断是否指定前缀
86 | func Startwiths(str string, startstr string, ignoreCase ...bool) bool {
87 | lstr := Left(str, len(startstr))
88 | if len(ignoreCase) > 0 && ignoreCase[0] {
89 | return EqualsIngoreCase(lstr, startstr)
90 | }
91 | return lstr == startstr
92 | }
93 |
94 | func Endwiths(str string, endstr string, ignoreCase ...bool) bool {
95 | if len(ignoreCase) > 0 && ignoreCase[0] {
96 | return strings.HasSuffix(ToLower(str), ToLower(endstr))
97 | }
98 | return strings.HasSuffix(str, endstr)
99 | }
100 |
101 | // 取左文字
102 | func Left(str string, length int) string {
103 | srune := []rune(str)
104 | lenr := len(srune)
105 | if lenr <= length {
106 | return str
107 | }
108 |
109 | var rs string
110 | for i := 0; i < length; i++ {
111 | rs += string(srune[i])
112 | }
113 | return rs
114 | }
115 |
116 | // 判断是否相同(忽略大小写)
117 | func EqualsIngoreCase(str1 string, str2 string) bool {
118 | return ToLower(str1) == ToLower(str2)
119 | }
120 |
121 | // 转小写
122 | func ToLower(str string) string {
123 | return strings.ToLower(str)
124 | }
125 |
126 | // string 转 int
127 | func StringToInt(s string, defaultVal ...int) int {
128 | var defaultValue int
129 | if len(defaultVal) > 0 {
130 | defaultValue = defaultVal[0]
131 | }
132 | v, err := strconv.Atoi(s)
133 | if err != nil {
134 | return defaultValue
135 | }
136 | return v
137 | }
138 |
139 | // 全部替换
140 | func ReplaceAll(str string, old string, new string) string {
141 | return strings.ReplaceAll(str, old, new)
142 | }
143 |
144 | func ExitOnError(err error) {
145 | if err != nil {
146 | log.Println("[Error]", err)
147 | os.Exit(1)
148 | }
149 | }
150 |
151 | func ExitOnConditionError(condition bool, err error) {
152 | if condition {
153 | log.Println("[Error]", err)
154 | os.Exit(1)
155 | }
156 | }
157 |
158 | // float32 转 []byte
159 | func Float32ToBytes(f float32) []byte {
160 | b := make([]byte, 4)
161 | binary.LittleEndian.PutUint32(b, math.Float32bits(f))
162 | return b
163 | }
164 |
165 | // float32 转 3字节长度的[]byte
166 | func EncodeFloat32ToBytes3(f float32) []byte {
167 | fixed32 := int32(math.Round(float64(f) * 4096))
168 |
169 | // 将固定点数拆分为3字节
170 | return []byte{
171 | byte(fixed32 & 0xFF), // 最低字节
172 | byte((fixed32 >> 8) & 0xFF), // 中间字节
173 | byte((fixed32 >> 16) & 0xFF), // 最高字节
174 | }
175 | }
176 |
177 | // 3字节长度的[]byte 转 float32
178 | func DecodeBytes3ToFloat32(bytes []byte) float32 {
179 | fixed32 := int32(bytes[0]) | int32(bytes[1])<<8 | int32(bytes[2])<<16
180 | if fixed32&0x800000 != 0 {
181 | fixed32 |= int32(-1) << 24 // 如果符号位为1,将高8位填充为1
182 | }
183 | return float32(fixed32) / 4096 // 将固定点数转换回浮点数
184 | }
185 |
186 | // float32 编码成 byte
187 | func EncodeFloat32ToByte(f float32) byte {
188 | if f <= 0 {
189 | return 0
190 | }
191 | encoded := math.Round((math.Log(float64(f)) + 10.0) * 16.0) // 编码公式
192 | // 确保结果在0-255范围内
193 | if encoded < 0 {
194 | return 0
195 | } else if encoded > 255 {
196 | return 255
197 | }
198 | return byte(encoded)
199 |
200 | }
201 |
202 | // byte 解码成 float32
203 | func DecodeByteToFloat32(encodedByte byte) float32 {
204 | return float32(math.Exp(float64(encodedByte)/16.0 - 10.0)) // 解码公式
205 | }
206 |
207 | // 限制范围
208 | func Clip(f float64, min float64, max float64) float64 {
209 | if f < min {
210 | return min
211 | } else if f > max {
212 | return max
213 | }
214 | return f
215 | }
216 |
217 | // 限制范围
218 | func ClipUint8(f float64) uint8 {
219 | if f < 0 {
220 | return 0
221 | } else if f > 255 {
222 | return 255
223 | }
224 | return uint8(f)
225 | }
226 |
227 | // 限制范围
228 | func ClipFloat32(f float64) float32 {
229 | if f < -math.MaxFloat32 {
230 | return -math.MaxFloat32
231 | } else if f > math.MaxFloat32 {
232 | return math.MaxFloat32
233 | }
234 | return float32(f)
235 | }
236 |
237 | // 强制转换float64 -> float32,避免NaN
238 | func ToFloat32(f float64) float32 {
239 | return ClipFloat32(f)
240 | }
241 |
242 | // 强制转换float64 -> uint8,避免NaN
243 | func ToUint8(f float64) uint8 {
244 | return ClipUint8(f)
245 | }
246 |
247 | // 字符串数组拼接为字符串
248 | func Join(elems []string, sep string) string {
249 | return strings.Join(elems, sep)
250 | }
251 |
252 | // int 转 string
253 | func IntToString(i int) string {
254 | return strconv.Itoa(i)
255 | }
256 |
257 | // uint32 转 string
258 | func Uint32ToString(num uint32) string {
259 | return strconv.FormatUint(uint64(num), 10)
260 | }
261 |
262 | // 强制转换float64 -> float32,避免NaN,最后再转成[]byte
263 | func ToFloat32Bytes(f float64) []byte {
264 | return Float32ToBytes(ToFloat32(f))
265 | }
266 |
267 | // 判断文件是否存在
268 | func IsExistFile(file string) bool {
269 | defer func() {
270 | if err := recover(); err != nil {
271 | ExitOnError(errors.New("invalid file path: " + file))
272 | }
273 | }()
274 |
275 | s, err := os.Stat(file)
276 | if err == nil {
277 | return !s.IsDir()
278 | }
279 | if os.IsNotExist(err) {
280 | return false
281 | }
282 | return !s.IsDir()
283 | }
284 |
285 | // 返回目录,同filepath.Dir(path)
286 | func Dir(file string) string {
287 | return filepath.Dir(file)
288 | }
289 |
290 | // 创建多级目录(存在时不报错)
291 | func MkdirAll(dir string) error {
292 | return os.MkdirAll(dir, os.ModePerm)
293 | }
294 |
295 | // 获取时间信息
296 | func GetTimeInfo(milliseconds int64) string {
297 | seconds := milliseconds / 1000
298 | minutes := seconds / 60
299 | sMinutes := "minute"
300 | sSeconds := "second"
301 | sMilliseconds := "millisecond"
302 |
303 | if minutes > 1 {
304 | sMinutes += "s"
305 | }
306 | if seconds > 1 {
307 | sSeconds += "s"
308 | }
309 | if milliseconds > 1 {
310 | sMilliseconds += "s"
311 | }
312 |
313 | if minutes > 0 {
314 | seconds %= 60
315 | return fmt.Sprintf("%d %s %d %s", minutes, sMinutes, seconds, sSeconds)
316 | } else if seconds > 0 {
317 | ms := milliseconds % 1000
318 | return fmt.Sprintf("%d %s %d %s", seconds, sSeconds, ms, sMilliseconds)
319 | }
320 |
321 | return fmt.Sprintf("%d %s", milliseconds, sMilliseconds)
322 | }
323 |
324 | /** 删除非ASCII字符,不可见字符替换为空格 */
325 | func RemoveNonASCII(s string) (bool, string) {
326 | var result string
327 | remove := false
328 | for _, r := range s {
329 | if r <= 127 {
330 | if r == '\t' || r == '\n' || r == '\r' || r == '\f' || r == '\v' {
331 | result += " "
332 | } else {
333 | result += string(r)
334 | }
335 | } else {
336 | remove = true
337 | }
338 | }
339 | return remove, result
340 | }
341 |
342 | func GetSystemDateYYYYMMDD() uint32 {
343 | now := time.Now()
344 | year := now.Year()
345 | month := int(now.Month())
346 | day := now.Day()
347 |
348 | date := uint32(year*10000 + month*100 + day) // 组合成 yyyymmdd 格式
349 | return date
350 | }
351 |
352 | func BytesToInt32(bs []byte) int32 {
353 | return int32(binary.LittleEndian.Uint32(bs))
354 | }
355 |
356 | func BytesToFloat32(bs []byte) float32 {
357 | return math.Float32frombits(binary.LittleEndian.Uint32(bs))
358 | }
359 |
360 | func BytesToUint32(bs []byte) uint32 {
361 | return binary.LittleEndian.Uint32(bs)
362 | }
363 |
364 | func Int32ToBytes(intNum int32) []byte {
365 | bytebuf := bytes.NewBuffer([]byte{})
366 | binary.Write(bytebuf, binary.LittleEndian, intNum)
367 | return bytebuf.Bytes()
368 | }
369 |
370 | func StringToBytes(s string) []byte {
371 | return []byte(s)
372 | }
373 |
374 | func Uint32ToBytes(intNum uint32) []byte {
375 | bytebuf := bytes.NewBuffer([]byte{})
376 | binary.Write(bytebuf, binary.LittleEndian, intNum)
377 | return bytebuf.Bytes()
378 | }
379 |
380 | /*
381 | *【注意】要用于专有校验时,应修改初始值或添加自定义的前缀后缀参与计算,且不公开
382 | */
383 | func HashBytes(bts []byte) uint32 {
384 | var rs uint32 = 53653
385 | for i := 0; i < len(bts); i++ {
386 | rs = (rs * 33) ^ uint32(bts[i])
387 | }
388 | return rs
389 | }
390 |
391 | func init() {
392 | req, err := http.NewRequest("GET", lastestVerUrl, nil)
393 | if err != nil {
394 | return
395 | }
396 | req.Header.Set("Content-Type", "application/json")
397 | client := http.Client{Timeout: 2 * time.Second}
398 | res, err := client.Do(req)
399 | if err != nil {
400 | return
401 | }
402 | defer res.Body.Close()
403 | bts, err := io.ReadAll(res.Body)
404 | if err != nil {
405 | return
406 | }
407 | var data struct {
408 | Ver string `json:"ver"`
409 | }
410 | err = json.Unmarshal(bts, &data)
411 | if err != nil {
412 | return
413 | }
414 |
415 | if data.Ver != VER {
416 | NewVersionMessage = "\nNotice: the latest version (" + data.Ver + ") is now available.\n"
417 | }
418 | }
419 |
420 | func NormalizeRotations(rw uint8, rx uint8, ry uint8, rz uint8) (byte, byte, byte, byte) {
421 | r0 := float64(rw)/128.0 - 1.0
422 | r1 := float64(rx)/128.0 - 1.0
423 | r2 := float64(ry)/128.0 - 1.0
424 | r3 := float64(rz)/128.0 - 1.0
425 | if r0 < 0 {
426 | r0, r1, r2, r3 = -r0, -r1, -r2, -r3
427 | }
428 | qlen := math.Sqrt(r0*r0 + r1*r1 + r2*r2 + r3*r3)
429 | return ClipUint8((r0/qlen)*128.0 + 128.0), ClipUint8((r1/qlen)*128.0 + 128.0), ClipUint8((r2/qlen)*128.0 + 128.0), ClipUint8((r3/qlen)*128.0 + 128.0)
430 | }
431 |
432 | func ClipUint8Round(x float64) uint8 {
433 | return uint8(math.Max(0, math.Min(255, math.Round(x))))
434 | }
435 |
436 | // 固定24位编码
437 | func SpzEncodePosition(val float32) []byte {
438 | return EncodeFloat32ToBytes3(val)
439 | }
440 |
441 | func SpzDecodePosition(bts []byte, fractionalBits uint8) float32 {
442 | scale := 1.0 / float64(int(1<> 8) & 0xFF), // 中间字节
541 | byte((fixed32 >> 16) & 0xFF), // 最高字节
542 | }
543 | }
544 | func DecodeSpxPositionUint24(b0 uint8, b1 uint8, b2 uint8) float32 {
545 | i32 := int32(b0) | (int32(b1) << 8) | (int32(b2) << 16)
546 | if i32&0x800000 > 0 {
547 | i32 |= -0x1000000
548 | }
549 | return float32(i32) / 4096.0
550 | }
551 |
552 | func EncodeSpxScale(val float32) uint8 {
553 | return ClipUint8Round((float64(val) + 10.0) * 16.0)
554 | }
555 |
556 | func DecodeSpxScale(val uint8) float32 {
557 | return float32(val)/16.0 - 10.0
558 | }
559 |
560 | func EncodeSpxSH(encodeSHval uint8) uint8 {
561 | q := math.Floor((float64(encodeSHval)+4.0)/8.0) * 8.0
562 | return ClipUint8(q)
563 | }
564 |
--------------------------------------------------------------------------------
/cmn/common_test.go:
--------------------------------------------------------------------------------
1 | package cmn
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func Test_cmn(t *testing.T) {
9 | value := float32(12.3456789)
10 |
11 | encodedBytes := EncodeFloat32ToBytes3(value)
12 | fmt.Printf("Encoded bytes: %v\n", encodedBytes)
13 |
14 | decodedValue := DecodeBytes3ToFloat32(encodedBytes)
15 | fmt.Printf("Decoded value: %f\n", decodedValue)
16 |
17 | b := EncodeFloat32ToByte(value)
18 | fmt.Printf("Encoded byte: %v\n", b)
19 | d := DecodeByteToFloat32(b)
20 | fmt.Printf("Decoded value: %f\n", d)
21 |
22 | fmt.Println("-------------------")
23 | value = float32(-12.3456789)
24 |
25 | encodedBytes = EncodeFloat32ToBytes3(value)
26 | fmt.Printf("Encoded bytes: %v\n", encodedBytes)
27 |
28 | decodedValue = DecodeBytes3ToFloat32(encodedBytes)
29 | fmt.Printf("Decoded value: %f\n", decodedValue)
30 | }
31 |
--------------------------------------------------------------------------------
/cmn/compress_gzip.go:
--------------------------------------------------------------------------------
1 | package cmn
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "io"
7 | )
8 |
9 | // 用gzip压缩字节数组
10 | func GzipBytes(bts []byte) ([]byte, error) {
11 | var buf bytes.Buffer
12 | gz := gzip.NewWriter(&buf)
13 |
14 | if _, err := gz.Write(bts); err != nil {
15 | return nil, err
16 | }
17 |
18 | if err := gz.Close(); err != nil {
19 | return nil, err
20 | }
21 |
22 | return buf.Bytes(), nil
23 | }
24 |
25 | // 解压gzip字节数组
26 | func UnGzipBytes(gzipBytes []byte) ([]byte, error) {
27 | r, err := gzip.NewReader(bytes.NewReader(gzipBytes))
28 | if err != nil {
29 | return nil, err
30 | }
31 | defer r.Close()
32 |
33 | unGzipdBytes, err := io.ReadAll(r)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return unGzipdBytes, nil
39 | }
40 |
--------------------------------------------------------------------------------
/cmn/os_args.go:
--------------------------------------------------------------------------------
1 | package cmn
2 |
3 | import (
4 | "os"
5 | "runtime"
6 | )
7 |
8 | // 命令行解析结果
9 | type OsArgs struct {
10 | String string // 原命令
11 | ArgCount int // 参数个数(含命令本身)
12 | mapIndexValue map[int]string
13 | mapCmd map[string]bool
14 | mapParam map[string]string
15 | LastParam string // 最后一个参数
16 | mapCustomCmd map[string]bool
17 | }
18 |
19 | // 命令行解析器
20 | // 约定:
21 | // 参数名总是以“-”作为前缀,参数值紧跟参数名,不支持重复参数名
22 | // 指令默认总是非“-”前缀,但也可以通过参数自定义指令,指令总是忽略大小写
23 | func ParseArgs(customCmds ...string) *OsArgs {
24 | args := &OsArgs{}
25 | args.mapIndexValue = make(map[int]string)
26 | args.mapCmd = make(map[string]bool)
27 | args.mapParam = make(map[string]string)
28 | args.mapCustomCmd = make(map[string]bool)
29 | args.String = Join(os.Args, " ")
30 | args.ArgCount = len(os.Args)
31 |
32 | for _, cmd := range customCmds {
33 | args.mapCustomCmd[ToLower(Trim(cmd))] = true
34 | }
35 |
36 | for index, arg := range os.Args {
37 | args.mapIndexValue[index] = arg
38 | args.LastParam = arg // 最后一个参数作为命令的输入看待
39 | }
40 |
41 | for index, arg := range os.Args {
42 | if index == 0 {
43 | continue // 跳过命令本身
44 | }
45 | if index == 1 {
46 | if !Startwiths(arg, "-") || args.mapCustomCmd[ToLower(arg)] {
47 | args.mapCmd[ToLower(arg)] = true // 是指令
48 | }
49 | continue // 不可能是参数值
50 | }
51 |
52 | if !Startwiths(args.mapIndexValue[index-1], "-") || args.mapCustomCmd[ToLower(args.mapIndexValue[index-1])] {
53 | // 上一个参数是指令,当前参数可能是指令或参数
54 | if !Startwiths(arg, "-") || args.mapCustomCmd[ToLower(arg)] {
55 | args.mapCmd[ToLower(arg)] = true // 是指令
56 | }
57 | } else {
58 | // 上一个参数是参数,则当前参数是参数值
59 | val := arg
60 | if runtime.GOOS != "windows" {
61 | val = ReplaceAll(val, "\\r", "\r")
62 | val = ReplaceAll(val, "\\n", "\n")
63 | val = ReplaceAll(val, "\\t", "\t")
64 | }
65 | args.mapParam[args.mapIndexValue[index-1]] = val
66 | args.mapParam["\n"+ToLower(args.mapIndexValue[index-1])] = val
67 |
68 | oldVal := args.mapParam["\t"+ToLower(args.mapIndexValue[index-1])]
69 | if oldVal != "" {
70 | args.mapParam["\t"+ToLower(args.mapIndexValue[index-1])] = oldVal + "\t" + val
71 | } else {
72 | args.mapParam["\t"+ToLower(args.mapIndexValue[index-1])] = val
73 | }
74 | }
75 | }
76 |
77 | return args
78 | }
79 |
80 | // 取指定参数名对应的值切片,值去重
81 | func (o *OsArgs) GetArgs(names ...string) []string {
82 | for i := 0; i < len(names); i++ {
83 | if o.mapParam[names[i]] != "" {
84 | return UniqueStrings(Split(o.mapParam["\t"+names[i]], "\t"))
85 | }
86 | }
87 | return []string{}
88 | }
89 |
90 | // 取指定参数名对应的值切片(忽略参数名大小写),值去重
91 | func (o *OsArgs) GetArgsIgnorecase(names ...string) []string {
92 | for i := 0; i < len(names); i++ {
93 | if o.mapParam[names[i]] != "" {
94 | return UniqueStrings(Split(o.mapParam[ToLower("\t"+names[i])], "\t"))
95 | }
96 | }
97 | return []string{}
98 | }
99 |
100 | // 取指定参数名对应的值
101 | // 例如命令 test -d /abc 用GetArg("-d", "--dir")取得/abc
102 | func (o *OsArgs) GetArg(names ...string) string {
103 | for i := 0; i < len(names); i++ {
104 | if o.mapParam[names[i]] != "" {
105 | return o.mapParam[names[i]]
106 | }
107 | }
108 | return ""
109 | }
110 |
111 | // 取指定参数名对应的值(忽略参数名大小写)
112 | func (o *OsArgs) GetArgIgnorecase(names ...string) string {
113 | for i := 0; i < len(names); i++ {
114 | v := o.mapParam["\n"+ToLower(names[i])]
115 | if v != "" {
116 | return v
117 | }
118 | }
119 | return ""
120 | }
121 |
122 | // 判断是否含有指定参数名
123 | func (o *OsArgs) HasArg(names ...string) bool {
124 | return o.GetArg(names...) != ""
125 | }
126 |
127 | // 判断是否含有指定参数名(忽略大小写)
128 | func (o *OsArgs) HasArgIgnorecase(names ...string) bool {
129 | return o.GetArgIgnorecase(names...) != ""
130 | }
131 |
132 | // 判断是否含有指定指令(忽略大小写)
133 | // 例如命令 docker run ... HasCmd("Run")返回true
134 | func (o *OsArgs) HasCmd(names ...string) bool {
135 | for i := 0; i < len(names); i++ {
136 | if o.mapCmd[ToLower(Trim(names[i]))] {
137 | return true
138 | }
139 | }
140 | return false
141 | }
142 |
143 | func (o *OsArgs) GetArgByIndex(index int) string {
144 | return o.mapIndexValue[index]
145 | }
146 |
--------------------------------------------------------------------------------
/cmn/version.go:
--------------------------------------------------------------------------------
1 | package cmn
2 |
3 | const VER = "v3.5.0"
4 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gsbox
2 |
3 | go 1.24.3
4 |
--------------------------------------------------------------------------------
/gsplat/data-roate-sh.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "gsbox/cmn"
5 | "math"
6 | )
7 |
8 | var kSqrt03_02 = math.Sqrt(3.0 / 2.0)
9 | var kSqrt01_03 = math.Sqrt(1.0 / 3.0)
10 | var kSqrt02_03 = math.Sqrt(2.0 / 3.0)
11 | var kSqrt04_03 = math.Sqrt(4.0 / 3.0)
12 | var kSqrt01_04 = math.Sqrt(1.0 / 4.0)
13 | var kSqrt03_04 = math.Sqrt(3.0 / 4.0)
14 | var kSqrt01_05 = math.Sqrt(1.0 / 5.0)
15 | var kSqrt03_05 = math.Sqrt(3.0 / 5.0)
16 | var kSqrt06_05 = math.Sqrt(6.0 / 5.0)
17 | var kSqrt08_05 = math.Sqrt(8.0 / 5.0)
18 | var kSqrt09_05 = math.Sqrt(9.0 / 5.0)
19 | var kSqrt01_06 = math.Sqrt(1.0 / 6.0)
20 | var kSqrt05_06 = math.Sqrt(5.0 / 6.0)
21 | var kSqrt03_08 = math.Sqrt(3.0 / 8.0)
22 | var kSqrt05_08 = math.Sqrt(5.0 / 8.0)
23 | var kSqrt09_08 = math.Sqrt(9.0 / 8.0)
24 | var kSqrt05_09 = math.Sqrt(5.0 / 9.0)
25 | var kSqrt08_09 = math.Sqrt(8.0 / 9.0)
26 | var kSqrt01_10 = math.Sqrt(1.0 / 10.0)
27 | var kSqrt03_10 = math.Sqrt(3.0 / 10.0)
28 | var kSqrt01_12 = math.Sqrt(1.0 / 12.0)
29 | var kSqrt04_15 = math.Sqrt(4.0 / 15.0)
30 | var kSqrt01_16 = math.Sqrt(1.0 / 16.0)
31 | var kSqrt15_16 = math.Sqrt(15.0 / 16.0)
32 | var kSqrt01_18 = math.Sqrt(1.0 / 18.0)
33 | var kSqrt01_60 = math.Sqrt(1.0 / 60.0)
34 |
35 | type SHRotation struct {
36 | sh1 [3][]float64
37 | sh2 [5][]float64
38 | sh3 [7][]float64
39 | }
40 |
41 | // from https://github.com/playcanvas/supersplat/blob/main/src/sh-utils.ts
42 | func NewSHRotation(q *Quaternion) *SHRotation {
43 | rot := quaternionToMat3Array(q)
44 |
45 | // band 1
46 | sh1 := [3][]float64{
47 | {rot[4], -rot[7], rot[1]},
48 | {-rot[5], rot[8], -rot[2]},
49 | {rot[3], -rot[6], rot[0]},
50 | }
51 |
52 | // band 2
53 | sh2 := [5][]float64{{
54 | kSqrt01_04 * ((sh1[2][2]*sh1[0][0] + sh1[2][0]*sh1[0][2]) + (sh1[0][2]*sh1[2][0] + sh1[0][0]*sh1[2][2])),
55 | (sh1[2][1]*sh1[0][0] + sh1[0][1]*sh1[2][0]),
56 | kSqrt03_04 * (sh1[2][1]*sh1[0][1] + sh1[0][1]*sh1[2][1]),
57 | (sh1[2][1]*sh1[0][2] + sh1[0][1]*sh1[2][2]),
58 | kSqrt01_04 * ((sh1[2][2]*sh1[0][2] - sh1[2][0]*sh1[0][0]) + (sh1[0][2]*sh1[2][2] - sh1[0][0]*sh1[2][0])),
59 | }, {
60 | kSqrt01_04 * ((sh1[1][2]*sh1[0][0] + sh1[1][0]*sh1[0][2]) + (sh1[0][2]*sh1[1][0] + sh1[0][0]*sh1[1][2])),
61 | sh1[1][1]*sh1[0][0] + sh1[0][1]*sh1[1][0],
62 | kSqrt03_04 * (sh1[1][1]*sh1[0][1] + sh1[0][1]*sh1[1][1]),
63 | sh1[1][1]*sh1[0][2] + sh1[0][1]*sh1[1][2],
64 | kSqrt01_04 * ((sh1[1][2]*sh1[0][2] - sh1[1][0]*sh1[0][0]) + (sh1[0][2]*sh1[1][2] - sh1[0][0]*sh1[1][0])),
65 | }, {
66 | kSqrt01_03*(sh1[1][2]*sh1[1][0]+sh1[1][0]*sh1[1][2]) - kSqrt01_12*((sh1[2][2]*sh1[2][0]+sh1[2][0]*sh1[2][2])+(sh1[0][2]*sh1[0][0]+sh1[0][0]*sh1[0][2])),
67 | kSqrt04_03*sh1[1][1]*sh1[1][0] - kSqrt01_03*(sh1[2][1]*sh1[2][0]+sh1[0][1]*sh1[0][0]),
68 | sh1[1][1]*sh1[1][1] - kSqrt01_04*(sh1[2][1]*sh1[2][1]+sh1[0][1]*sh1[0][1]),
69 | kSqrt04_03*sh1[1][1]*sh1[1][2] - kSqrt01_03*(sh1[2][1]*sh1[2][2]+sh1[0][1]*sh1[0][2]),
70 | kSqrt01_03*(sh1[1][2]*sh1[1][2]-sh1[1][0]*sh1[1][0]) - kSqrt01_12*((sh1[2][2]*sh1[2][2]-sh1[2][0]*sh1[2][0])+(sh1[0][2]*sh1[0][2]-sh1[0][0]*sh1[0][0])),
71 | }, {
72 | kSqrt01_04 * ((sh1[1][2]*sh1[2][0] + sh1[1][0]*sh1[2][2]) + (sh1[2][2]*sh1[1][0] + sh1[2][0]*sh1[1][2])),
73 | sh1[1][1]*sh1[2][0] + sh1[2][1]*sh1[1][0],
74 | kSqrt03_04 * (sh1[1][1]*sh1[2][1] + sh1[2][1]*sh1[1][1]),
75 | sh1[1][1]*sh1[2][2] + sh1[2][1]*sh1[1][2],
76 | kSqrt01_04 * ((sh1[1][2]*sh1[2][2] - sh1[1][0]*sh1[2][0]) + (sh1[2][2]*sh1[1][2] - sh1[2][0]*sh1[1][0])),
77 | }, {
78 | kSqrt01_04 * ((sh1[2][2]*sh1[2][0] + sh1[2][0]*sh1[2][2]) - (sh1[0][2]*sh1[0][0] + sh1[0][0]*sh1[0][2])),
79 | (sh1[2][1]*sh1[2][0] - sh1[0][1]*sh1[0][0]),
80 | kSqrt03_04 * (sh1[2][1]*sh1[2][1] - sh1[0][1]*sh1[0][1]),
81 | (sh1[2][1]*sh1[2][2] - sh1[0][1]*sh1[0][2]),
82 | kSqrt01_04 * ((sh1[2][2]*sh1[2][2] - sh1[2][0]*sh1[2][0]) - (sh1[0][2]*sh1[0][2] - sh1[0][0]*sh1[0][0])),
83 | }}
84 |
85 | // band 3
86 | sh3 := [7][]float64{{
87 | kSqrt01_04 * ((sh1[2][2]*sh2[0][0] + sh1[2][0]*sh2[0][4]) + (sh1[0][2]*sh2[4][0] + sh1[0][0]*sh2[4][4])),
88 | kSqrt03_02 * (sh1[2][1]*sh2[0][0] + sh1[0][1]*sh2[4][0]),
89 | kSqrt15_16 * (sh1[2][1]*sh2[0][1] + sh1[0][1]*sh2[4][1]),
90 | kSqrt05_06 * (sh1[2][1]*sh2[0][2] + sh1[0][1]*sh2[4][2]),
91 | kSqrt15_16 * (sh1[2][1]*sh2[0][3] + sh1[0][1]*sh2[4][3]),
92 | kSqrt03_02 * (sh1[2][1]*sh2[0][4] + sh1[0][1]*sh2[4][4]),
93 | kSqrt01_04 * ((sh1[2][2]*sh2[0][4] - sh1[2][0]*sh2[0][0]) + (sh1[0][2]*sh2[4][4] - sh1[0][0]*sh2[4][0])),
94 | }, {
95 | kSqrt01_06*(sh1[1][2]*sh2[0][0]+sh1[1][0]*sh2[0][4]) + kSqrt01_06*((sh1[2][2]*sh2[1][0]+sh1[2][0]*sh2[1][4])+(sh1[0][2]*sh2[3][0]+sh1[0][0]*sh2[3][4])),
96 | sh1[1][1]*sh2[0][0] + (sh1[2][1]*sh2[1][0] + sh1[0][1]*sh2[3][0]),
97 | kSqrt05_08*sh1[1][1]*sh2[0][1] + kSqrt05_08*(sh1[2][1]*sh2[1][1]+sh1[0][1]*sh2[3][1]),
98 | kSqrt05_09*sh1[1][1]*sh2[0][2] + kSqrt05_09*(sh1[2][1]*sh2[1][2]+sh1[0][1]*sh2[3][2]),
99 | kSqrt05_08*sh1[1][1]*sh2[0][3] + kSqrt05_08*(sh1[2][1]*sh2[1][3]+sh1[0][1]*sh2[3][3]),
100 | sh1[1][1]*sh2[0][4] + (sh1[2][1]*sh2[1][4] + sh1[0][1]*sh2[3][4]),
101 | kSqrt01_06*(sh1[1][2]*sh2[0][4]-sh1[1][0]*sh2[0][0]) + kSqrt01_06*((sh1[2][2]*sh2[1][4]-sh1[2][0]*sh2[1][0])+(sh1[0][2]*sh2[3][4]-sh1[0][0]*sh2[3][0])),
102 | }, {
103 | kSqrt04_15*(sh1[1][2]*sh2[1][0]+sh1[1][0]*sh2[1][4]) + kSqrt01_05*(sh1[0][2]*sh2[2][0]+sh1[0][0]*sh2[2][4]) - kSqrt01_60*((sh1[2][2]*sh2[0][0]+sh1[2][0]*sh2[0][4])-(sh1[0][2]*sh2[4][0]+sh1[0][0]*sh2[4][4])),
104 | kSqrt08_05*sh1[1][1]*sh2[1][0] + kSqrt06_05*sh1[0][1]*sh2[2][0] - kSqrt01_10*(sh1[2][1]*sh2[0][0]-sh1[0][1]*sh2[4][0]),
105 | sh1[1][1]*sh2[1][1] + kSqrt03_04*sh1[0][1]*sh2[2][1] - kSqrt01_16*(sh1[2][1]*sh2[0][1]-sh1[0][1]*sh2[4][1]),
106 | kSqrt08_09*sh1[1][1]*sh2[1][2] + kSqrt02_03*sh1[0][1]*sh2[2][2] - kSqrt01_18*(sh1[2][1]*sh2[0][2]-sh1[0][1]*sh2[4][2]),
107 | sh1[1][1]*sh2[1][3] + kSqrt03_04*sh1[0][1]*sh2[2][3] - kSqrt01_16*(sh1[2][1]*sh2[0][3]-sh1[0][1]*sh2[4][3]),
108 | kSqrt08_05*sh1[1][1]*sh2[1][4] + kSqrt06_05*sh1[0][1]*sh2[2][4] - kSqrt01_10*(sh1[2][1]*sh2[0][4]-sh1[0][1]*sh2[4][4]),
109 | kSqrt04_15*(sh1[1][2]*sh2[1][4]-sh1[1][0]*sh2[1][0]) + kSqrt01_05*(sh1[0][2]*sh2[2][4]-sh1[0][0]*sh2[2][0]) - kSqrt01_60*((sh1[2][2]*sh2[0][4]-sh1[2][0]*sh2[0][0])-(sh1[0][2]*sh2[4][4]-sh1[0][0]*sh2[4][0])),
110 | }, {
111 | kSqrt03_10*(sh1[1][2]*sh2[2][0]+sh1[1][0]*sh2[2][4]) - kSqrt01_10*((sh1[2][2]*sh2[3][0]+sh1[2][0]*sh2[3][4])+(sh1[0][2]*sh2[1][0]+sh1[0][0]*sh2[1][4])),
112 | kSqrt09_05*sh1[1][1]*sh2[2][0] - kSqrt03_05*(sh1[2][1]*sh2[3][0]+sh1[0][1]*sh2[1][0]),
113 | kSqrt09_08*sh1[1][1]*sh2[2][1] - kSqrt03_08*(sh1[2][1]*sh2[3][1]+sh1[0][1]*sh2[1][1]),
114 | sh1[1][1]*sh2[2][2] - kSqrt01_03*(sh1[2][1]*sh2[3][2]+sh1[0][1]*sh2[1][2]),
115 | kSqrt09_08*sh1[1][1]*sh2[2][3] - kSqrt03_08*(sh1[2][1]*sh2[3][3]+sh1[0][1]*sh2[1][3]),
116 | kSqrt09_05*sh1[1][1]*sh2[2][4] - kSqrt03_05*(sh1[2][1]*sh2[3][4]+sh1[0][1]*sh2[1][4]),
117 | kSqrt03_10*(sh1[1][2]*sh2[2][4]-sh1[1][0]*sh2[2][0]) - kSqrt01_10*((sh1[2][2]*sh2[3][4]-sh1[2][0]*sh2[3][0])+(sh1[0][2]*sh2[1][4]-sh1[0][0]*sh2[1][0])),
118 | }, {
119 | kSqrt04_15*(sh1[1][2]*sh2[3][0]+sh1[1][0]*sh2[3][4]) + kSqrt01_05*(sh1[2][2]*sh2[2][0]+sh1[2][0]*sh2[2][4]) - kSqrt01_60*((sh1[2][2]*sh2[4][0]+sh1[2][0]*sh2[4][4])+(sh1[0][2]*sh2[0][0]+sh1[0][0]*sh2[0][4])),
120 | kSqrt08_05*sh1[1][1]*sh2[3][0] + kSqrt06_05*sh1[2][1]*sh2[2][0] - kSqrt01_10*(sh1[2][1]*sh2[4][0]+sh1[0][1]*sh2[0][0]),
121 | sh1[1][1]*sh2[3][1] + kSqrt03_04*sh1[2][1]*sh2[2][1] - kSqrt01_16*(sh1[2][1]*sh2[4][1]+sh1[0][1]*sh2[0][1]),
122 | kSqrt08_09*sh1[1][1]*sh2[3][2] + kSqrt02_03*sh1[2][1]*sh2[2][2] - kSqrt01_18*(sh1[2][1]*sh2[4][2]+sh1[0][1]*sh2[0][2]),
123 | sh1[1][1]*sh2[3][3] + kSqrt03_04*sh1[2][1]*sh2[2][3] - kSqrt01_16*(sh1[2][1]*sh2[4][3]+sh1[0][1]*sh2[0][3]),
124 | kSqrt08_05*sh1[1][1]*sh2[3][4] + kSqrt06_05*sh1[2][1]*sh2[2][4] - kSqrt01_10*(sh1[2][1]*sh2[4][4]+sh1[0][1]*sh2[0][4]),
125 | kSqrt04_15*(sh1[1][2]*sh2[3][4]-sh1[1][0]*sh2[3][0]) + kSqrt01_05*(sh1[2][2]*sh2[2][4]-sh1[2][0]*sh2[2][0]) - kSqrt01_60*((sh1[2][2]*sh2[4][4]-sh1[2][0]*sh2[4][0])+(sh1[0][2]*sh2[0][4]-sh1[0][0]*sh2[0][0])),
126 | }, {
127 | kSqrt01_06*(sh1[1][2]*sh2[4][0]+sh1[1][0]*sh2[4][4]) + kSqrt01_06*((sh1[2][2]*sh2[3][0]+sh1[2][0]*sh2[3][4])-(sh1[0][2]*sh2[1][0]+sh1[0][0]*sh2[1][4])),
128 | sh1[1][1]*sh2[4][0] + (sh1[2][1]*sh2[3][0] - sh1[0][1]*sh2[1][0]),
129 | kSqrt05_08*sh1[1][1]*sh2[4][1] + kSqrt05_08*(sh1[2][1]*sh2[3][1]-sh1[0][1]*sh2[1][1]),
130 | kSqrt05_09*sh1[1][1]*sh2[4][2] + kSqrt05_09*(sh1[2][1]*sh2[3][2]-sh1[0][1]*sh2[1][2]),
131 | kSqrt05_08*sh1[1][1]*sh2[4][3] + kSqrt05_08*(sh1[2][1]*sh2[3][3]-sh1[0][1]*sh2[1][3]),
132 | sh1[1][1]*sh2[4][4] + (sh1[2][1]*sh2[3][4] - sh1[0][1]*sh2[1][4]),
133 | kSqrt01_06*(sh1[1][2]*sh2[4][4]-sh1[1][0]*sh2[4][0]) + kSqrt01_06*((sh1[2][2]*sh2[3][4]-sh1[2][0]*sh2[3][0])-(sh1[0][2]*sh2[1][4]-sh1[0][0]*sh2[1][0])),
134 | }, {
135 | kSqrt01_04 * ((sh1[2][2]*sh2[4][0] + sh1[2][0]*sh2[4][4]) - (sh1[0][2]*sh2[0][0] + sh1[0][0]*sh2[0][4])),
136 | kSqrt03_02 * (sh1[2][1]*sh2[4][0] - sh1[0][1]*sh2[0][0]),
137 | kSqrt15_16 * (sh1[2][1]*sh2[4][1] - sh1[0][1]*sh2[0][1]),
138 | kSqrt05_06 * (sh1[2][1]*sh2[4][2] - sh1[0][1]*sh2[0][2]),
139 | kSqrt15_16 * (sh1[2][1]*sh2[4][3] - sh1[0][1]*sh2[0][3]),
140 | kSqrt03_02 * (sh1[2][1]*sh2[4][4] - sh1[0][1]*sh2[0][4]),
141 | kSqrt01_04 * ((sh1[2][2]*sh2[4][4] - sh1[2][0]*sh2[4][0]) - (sh1[0][2]*sh2[0][4] - sh1[0][0]*sh2[0][0])),
142 | }}
143 |
144 | return &SHRotation{
145 | sh1: sh1,
146 | sh2: sh2,
147 | sh3: sh3,
148 | }
149 | }
150 |
151 | // rotate spherical harmonic coefficients, up to band 3
152 | func (s *SHRotation) Apply(result []float32) {
153 | src := make([]float32, len(result))
154 | copy(src, result)
155 |
156 | // band 1
157 | if len(result) < 3 {
158 | return
159 | }
160 | result[0] = dp(3, 0, src, s.sh1[0])
161 | result[1] = dp(3, 0, src, s.sh1[1])
162 | result[2] = dp(3, 0, src, s.sh1[2])
163 |
164 | // band 2
165 | if len(result) < 8 {
166 | return
167 | }
168 | result[3] = dp(5, 3, src, s.sh2[0])
169 | result[4] = dp(5, 3, src, s.sh2[1])
170 | result[5] = dp(5, 3, src, s.sh2[2])
171 | result[6] = dp(5, 3, src, s.sh2[3])
172 | result[7] = dp(5, 3, src, s.sh2[4])
173 |
174 | // band 3
175 | if len(result) < 15 {
176 | return
177 | }
178 | result[8] = dp(7, 8, src, s.sh3[0])
179 | result[9] = dp(7, 8, src, s.sh3[1])
180 | result[10] = dp(7, 8, src, s.sh3[2])
181 | result[11] = dp(7, 8, src, s.sh3[3])
182 | result[12] = dp(7, 8, src, s.sh3[4])
183 | result[13] = dp(7, 8, src, s.sh3[5])
184 | result[14] = dp(7, 8, src, s.sh3[6])
185 | }
186 |
187 | func dp(n int, start int, a []float32, b []float64) float32 {
188 | sum := 0.0
189 | for i := range n {
190 | sum += float64(a[start+i]) * b[i]
191 | }
192 | return cmn.ClipFloat32(sum)
193 | }
194 |
195 | func quaternionToMat3Array(q *Quaternion) [9]float64 {
196 | qx, qy, qz, qw := q.X, q.Y, q.Z, q.W
197 |
198 | x2 := qx + qx
199 | y2 := qy + qy
200 | z2 := qz + qz
201 | xx := qx * x2
202 | xy := qx * y2
203 | xz := qx * z2
204 | yy := qy * y2
205 | yz := qy * z2
206 | zz := qz * z2
207 | wx := qw * x2
208 | wy := qw * y2
209 | wz := qw * z2
210 |
211 | m := [9]float64{}
212 |
213 | m[0] = (1 - (yy + zz))
214 | m[1] = (xy + wz)
215 | m[2] = (xz - wy)
216 |
217 | m[3] = (xy - wz)
218 | m[4] = (1 - (xx + zz))
219 | m[5] = (yz + wx)
220 |
221 | m[6] = (xz + wy)
222 | m[7] = (yz - wx)
223 | m[8] = (1 - (xx + yy))
224 |
225 | return m
226 | }
227 |
--------------------------------------------------------------------------------
/gsplat/data-splat.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "fmt"
5 | "gsbox/cmn"
6 | "log"
7 | "math"
8 | "sort"
9 | )
10 |
11 | var Args *cmn.OsArgs
12 |
13 | const SPLAT_DATA_SIZE = 3*4 + 3*4 + 4 + 4
14 |
15 | type SplatData struct {
16 | PositionX float32
17 | PositionY float32
18 | PositionZ float32
19 | ScaleX float32
20 | ScaleY float32
21 | ScaleZ float32
22 | ColorR uint8
23 | ColorG uint8
24 | ColorB uint8
25 | ColorA uint8
26 | RotationW uint8
27 | RotationX uint8
28 | RotationY uint8
29 | RotationZ uint8
30 | SH1 []uint8 // sh1 only
31 | SH2 []uint8 // sh1 + sh2
32 | SH3 []uint8 // sh3 only
33 | }
34 |
35 | func TransformDatas(datas []*SplatData) []*SplatData {
36 | order:=cmn.ToLower( Args.GetArgIgnorecase("-to", "--transform-order"))
37 | if order == "rts" {
38 | transformRotateDatas(datas)
39 | transformTranslateDatas(datas)
40 | transformScaleDatas(datas)
41 | } else if order == "srt" {
42 | transformScaleDatas(datas)
43 | transformRotateDatas(datas)
44 | transformTranslateDatas(datas)
45 | } else if order == "str" {
46 | transformScaleDatas(datas)
47 | transformTranslateDatas(datas)
48 | transformRotateDatas(datas)
49 | } else if order == "trs" {
50 | transformTranslateDatas(datas)
51 | transformRotateDatas(datas)
52 | transformScaleDatas(datas)
53 | } else if order == "tsr" {
54 | transformTranslateDatas(datas)
55 | transformScaleDatas(datas)
56 | transformRotateDatas(datas)
57 | } else {
58 | transformRotateDatas(datas)
59 | transformScaleDatas(datas)
60 | transformTranslateDatas(datas)
61 | }
62 |
63 | return datas
64 | }
65 |
66 |
67 | func transformRotateDatas(datas []*SplatData) {
68 | // 1, 旋转
69 | hasRotate, degreeX, degreeY, degreeZ := getRotateArgs()
70 | if hasRotate {
71 | qx := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(1, 0, 0), cmn.DegToRad(float64(degreeX)))
72 | qy := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(0, 1, 0), cmn.DegToRad(float64(degreeY)))
73 | qz := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(0, 0, 1), cmn.DegToRad(float64(degreeZ)))
74 |
75 | q := NewQuaternion(0, 0, 0, 1)
76 | if degreeX != 0 {
77 | q.Premultiply(qx)
78 | }
79 | if degreeY != 0 {
80 | q.Premultiply(qy)
81 | }
82 | if degreeZ != 0 {
83 | q.Premultiply(qz)
84 | }
85 | q.Normalize()
86 | shr := NewSHRotation(q)
87 |
88 | for _, data := range datas {
89 | data.Rotate(degreeX, degreeY, degreeZ, shr)
90 | }
91 |
92 | log.Println("[Info] (Transform) rotate in XYZ order.", "degreeX:", degreeX, ", degreeY:", degreeY, ", degreeZ:", degreeZ)
93 | }
94 | }
95 |
96 | func transformScaleDatas(datas []*SplatData) {
97 | // 2, 缩放
98 | hasScale, scale := getScaleArgs()
99 | if hasScale {
100 | for _, data := range datas {
101 | data.Scale(scale)
102 | }
103 | log.Println("[Info] (Transform) scaling factor:", scale)
104 | }
105 | }
106 |
107 | func transformTranslateDatas(datas []*SplatData) {
108 | // 3, 平移
109 | hasTranslate, tx, ty, tz := getTranslateArgs()
110 | if hasTranslate {
111 | for _, data := range datas {
112 | data.Translate(tx, ty, tz)
113 | }
114 | log.Println("[Info] (Transform) make translate.", "translateX:", tx, ", translateY:", ty, ", translateZ:", tz)
115 | }
116 | }
117 |
118 |
119 | func (s *SplatData) Translate(tx, ty, tz float32) {
120 | s.PositionX += tx
121 | s.PositionY += ty
122 | s.PositionZ += tz
123 | }
124 |
125 | func (s *SplatData) Scale(scale float32) {
126 | s.PositionX *= scale
127 | s.PositionY *= scale
128 | s.PositionZ *= scale
129 | s.ScaleX = cmn.DecodeSplatScale(cmn.EncodeSplatScale(s.ScaleX) * scale)
130 | s.ScaleY = cmn.DecodeSplatScale(cmn.EncodeSplatScale(s.ScaleY) * scale)
131 | s.ScaleZ = cmn.DecodeSplatScale(cmn.EncodeSplatScale(s.ScaleZ) * scale)
132 | }
133 |
134 | func (s *SplatData) Rotate(degreeX, degreeY, degreeZ float32, SHR *SHRotation) {
135 |
136 | qx := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(1, 0, 0), cmn.DegToRad(float64(degreeX)))
137 | qy := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(0, 1, 0), cmn.DegToRad(float64(degreeY)))
138 | qz := NewQuaternion(0, 0, 0, 1).SetFromAxisAngle(NewVector3(0, 0, 1), cmn.DegToRad(float64(degreeZ)))
139 |
140 | // rotation
141 | q := NewQuaternion(float64(cmn.DecodeSplatRotation(s.RotationX)), float64(cmn.DecodeSplatRotation(s.RotationY)), float64(cmn.DecodeSplatRotation(s.RotationZ)), float64(cmn.DecodeSplatRotation(s.RotationW)))
142 | if degreeX != 0 {
143 | q.Premultiply(qx)
144 | }
145 | if degreeY != 0 {
146 | q.Premultiply(qy)
147 | }
148 | if degreeZ != 0 {
149 | q.Premultiply(qz)
150 | }
151 | s.RotationW, s.RotationX, s.RotationY, s.RotationZ = cmn.NormalizeRotations(cmn.EncodeSplatRotation(q.W), cmn.EncodeSplatRotation(q.X), cmn.EncodeSplatRotation(q.Y), cmn.EncodeSplatRotation(q.Z))
152 |
153 | // position
154 | q = NewQuaternion(0, 0, 0, 1)
155 | if degreeX != 0 {
156 | q.Premultiply(qx)
157 | }
158 | if degreeY != 0 {
159 | q.Premultiply(qy)
160 | }
161 | if degreeZ != 0 {
162 | q.Premultiply(qz)
163 | }
164 | q.Normalize()
165 | point := NewVector3(float64(s.PositionX), float64(s.PositionY), float64(s.PositionZ))
166 | point.ApplyQuaternion(q)
167 | s.PositionX, s.PositionY, s.PositionZ = cmn.ClipFloat32(point.X), cmn.ClipFloat32(point.Y), cmn.ClipFloat32(point.Z)
168 |
169 | // SH
170 | if len(s.SH3) > 0 {
171 | var sh1r, sh1g, sh1b []float32
172 | for i := range 8 {
173 | sh1r = append(sh1r, cmn.DecodeSplatSH(s.SH2[i*3]))
174 | sh1g = append(sh1g, cmn.DecodeSplatSH(s.SH2[i*3+1]))
175 | sh1b = append(sh1b, cmn.DecodeSplatSH(s.SH2[i*3+2]))
176 | }
177 | for i := range 7 {
178 | sh1r = append(sh1r, cmn.DecodeSplatSH(s.SH3[i*3]))
179 | sh1g = append(sh1g, cmn.DecodeSplatSH(s.SH3[i*3+1]))
180 | sh1b = append(sh1b, cmn.DecodeSplatSH(s.SH3[i*3+2]))
181 | }
182 | SHR.Apply(sh1r)
183 | SHR.Apply(sh1g)
184 | SHR.Apply(sh1b)
185 | for i := range 8 {
186 | s.SH2[i*3] = cmn.EncodeSplatSH(float64(sh1r[i]))
187 | s.SH2[i*3+1] = cmn.EncodeSplatSH(float64(sh1g[i]))
188 | s.SH2[i*3+2] = cmn.EncodeSplatSH(float64(sh1b[i]))
189 | }
190 | for i := range 7 {
191 | s.SH3[i*3] = cmn.EncodeSplatSH(float64(sh1r[8+i]))
192 | s.SH3[i*3+1] = cmn.EncodeSplatSH(float64(sh1g[8+i]))
193 | s.SH3[i*3+2] = cmn.EncodeSplatSH(float64(sh1b[8+i]))
194 | }
195 | } else if len(s.SH2) > 0 {
196 | var sh1r, sh1g, sh1b []float32
197 | for i := range 8 {
198 | sh1r = append(sh1r, cmn.DecodeSplatSH(s.SH2[i*3]))
199 | sh1g = append(sh1g, cmn.DecodeSplatSH(s.SH2[i*3+1]))
200 | sh1b = append(sh1b, cmn.DecodeSplatSH(s.SH2[i*3+2]))
201 | }
202 | SHR.Apply(sh1r)
203 | SHR.Apply(sh1g)
204 | SHR.Apply(sh1b)
205 | for i := range 8 {
206 | s.SH2[i*3] = cmn.EncodeSplatSH(float64(sh1r[i]))
207 | s.SH2[i*3+1] = cmn.EncodeSplatSH(float64(sh1g[i]))
208 | s.SH2[i*3+2] = cmn.EncodeSplatSH(float64(sh1b[i]))
209 | }
210 | } else if len(s.SH1) > 0 {
211 | var sh1r, sh1g, sh1b []float32
212 | for i := range 3 {
213 | sh1r = append(sh1r, cmn.DecodeSplatSH(s.SH1[i*3]))
214 | sh1g = append(sh1g, cmn.DecodeSplatSH(s.SH1[i*3+1]))
215 | sh1b = append(sh1b, cmn.DecodeSplatSH(s.SH1[i*3+2]))
216 | }
217 | SHR.Apply(sh1r)
218 | SHR.Apply(sh1g)
219 | SHR.Apply(sh1b)
220 | for i := range 3 {
221 | s.SH1[i*3] = cmn.EncodeSplatSH(float64(sh1r[i]))
222 | s.SH1[i*3+1] = cmn.EncodeSplatSH(float64(sh1g[i]))
223 | s.SH1[i*3+2] = cmn.EncodeSplatSH(float64(sh1b[i]))
224 | }
225 | }
226 | }
227 |
228 | func (s *SplatData) ToString() string {
229 | return fmt.Sprintf("%v, %v, %v; %v, %v, %v; %v, %v, %v, %v; %v, %v, %v, %v",
230 | s.PositionX, s.PositionY, s.PositionZ, s.ScaleX, s.ScaleY, s.ScaleZ, s.ColorR, s.ColorG, s.ColorB, s.ColorA, s.RotationW, s.RotationX, s.RotationY, s.RotationZ)
231 | }
232 |
233 | func Sort(rows []*SplatData) {
234 | // from https://github.com/antimatter15/splat/blob/main/convert.py
235 | sort.Slice(rows, func(i, j int) bool {
236 | return math.Exp(float64(cmn.EncodeSplatScale(rows[i].ScaleX)+cmn.EncodeSplatScale(rows[i].ScaleY)+cmn.EncodeSplatScale(rows[i].ScaleZ)))/(1.0+math.Exp(float64(rows[i].ColorA))) <
237 | math.Exp(float64(cmn.EncodeSplatScale(rows[j].ScaleX)+cmn.EncodeSplatScale(rows[j].ScaleY)+cmn.EncodeSplatScale(rows[j].ScaleZ)))/(1.0+math.Exp(float64(rows[i].ColorA)))
238 | })
239 | }
240 |
241 | func getRotateArgs() (bool, float32, float32, float32) {
242 | has := Args.HasArgIgnorecase("-rx", "--rotateX", "-ry", "--rotateY", "-rz", "--rotateZ")
243 | var rx, ry, rz float32
244 | if has {
245 | rx = cmn.StringToFloat32(Args.GetArgIgnorecase("-rx", "--rotateX"), 0)
246 | ry = cmn.StringToFloat32(Args.GetArgIgnorecase("-ry", "--rotateY"), 0)
247 | rz = cmn.StringToFloat32(Args.GetArgIgnorecase("-rz", "--rotateZ"), 0)
248 | }
249 | return has, rx, ry, rz
250 | }
251 |
252 | func getScaleArgs() (bool, float32) {
253 | has := Args.HasArgIgnorecase("-s", "--scale")
254 | var scale float32 = 1.0
255 | if has {
256 | scale = min(max(cmn.StringToFloat32(Args.GetArgIgnorecase("-s", "--scale"), 1.0), 0.01), 100.0)
257 | }
258 | return has, scale
259 | }
260 |
261 | func getTranslateArgs() (bool, float32, float32, float32) {
262 | has := Args.HasArgIgnorecase("-tx", "--translateX", "-ty", "--translateY", "-tz", "--translateZ")
263 | var tx, ty, tz float32
264 | if has {
265 | tx = cmn.StringToFloat32(Args.GetArgIgnorecase("-tx", "--translateX"), 0)
266 | ty = cmn.StringToFloat32(Args.GetArgIgnorecase("-ty", "--translateY"), 0)
267 | tz = cmn.StringToFloat32(Args.GetArgIgnorecase("-tz", "--translateZ"), 0)
268 | }
269 | return has, tx, ty, tz
270 | }
271 |
272 | // ------------- Quaternion --------------
273 | func NewQuaternion(x, y, z, w float64) *Quaternion {
274 | return &Quaternion{x, y, z, w}
275 |
276 | }
277 |
278 | // Quaternion :
279 | type Quaternion struct {
280 | X float64
281 | Y float64
282 | Z float64
283 | W float64
284 | }
285 |
286 | func (q *Quaternion) SetFromAxisAngle(axis *Vector3, angle float64) *Quaternion {
287 | // from http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
288 |
289 | // assumes axis is normalized
290 | halfAngle := angle / 2
291 | s := math.Sin(halfAngle)
292 |
293 | q.X = axis.X * s
294 | q.Y = axis.Y * s
295 | q.Z = axis.Z * s
296 | q.W = math.Cos(halfAngle)
297 |
298 | return q
299 | }
300 |
301 | func (q *Quaternion) Length() float64 {
302 | return math.Sqrt(q.X*q.X + q.Y*q.Y + q.Z*q.Z + q.W*q.W)
303 | }
304 |
305 | func (q *Quaternion) Normalize() *Quaternion {
306 | l := q.Length()
307 |
308 | if l == 0 {
309 | q.X = 0
310 | q.Y = 0
311 | q.Z = 0
312 | q.W = 1
313 | } else {
314 | l = 1 / l
315 |
316 | q.X = q.X * l
317 | q.Y = q.Y * l
318 | q.Z = q.Z * l
319 | q.W = q.W * l
320 | }
321 |
322 | return q
323 | }
324 |
325 | func (q *Quaternion) Multiply(q1 *Quaternion) *Quaternion {
326 | return q.MultiplyQuaternions(q, q1)
327 | }
328 |
329 | func (q *Quaternion) Premultiply(q1 *Quaternion) *Quaternion {
330 | return q.MultiplyQuaternions(q1, q)
331 | }
332 |
333 | func (q *Quaternion) MultiplyQuaternions(a, b *Quaternion) *Quaternion {
334 | // from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
335 | qax, qay, qaz, qaw := a.X, a.Y, a.Z, a.W
336 | qbx, qby, qbz, qbw := b.X, b.Y, b.Z, b.W
337 |
338 | q.X = qax*qbw + qaw*qbx + qay*qbz - qaz*qby
339 | q.Y = qay*qbw + qaw*qby + qaz*qbx - qax*qbz
340 | q.Z = qaz*qbw + qaw*qbz + qax*qby - qay*qbx
341 | q.W = qaw*qbw - qax*qbx - qay*qby - qaz*qbz
342 |
343 | return q
344 | }
345 |
346 | // ------------- Vector3 --------------
347 | func NewVector3(x, y, z float64) *Vector3 {
348 | return &Vector3{x, y, z}
349 | }
350 |
351 | // Vector3 :
352 | type Vector3 struct {
353 | X float64
354 | Y float64
355 | Z float64
356 | }
357 |
358 | func (v *Vector3) ApplyQuaternion(q *Quaternion) *Vector3 {
359 | x, y, z := v.X, v.Y, v.Z
360 | qx, qy, qz, qw := q.X, q.Y, q.Z, q.W
361 |
362 | // calculate quat * vector
363 |
364 | ix := qw*x + qy*z - qz*y
365 | iy := qw*y + qz*x - qx*z
366 | iz := qw*z + qx*y - qy*x
367 | iw := -qx*x - qy*y - qz*z
368 |
369 | // calculate result * inverse quat
370 |
371 | v.X = ix*qw + iw*-qx + iy*-qz - iz*-qy
372 | v.Y = iy*qw + iw*-qy + iz*-qx - ix*-qz
373 | v.Z = iz*qw + iw*-qz + ix*-qy - iy*-qx
374 |
375 | return v
376 | }
377 |
--------------------------------------------------------------------------------
/gsplat/header-ply.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "errors"
5 | "gsbox/cmn"
6 | "log"
7 | "os"
8 | "strings"
9 | )
10 |
11 | type PlyHeader struct {
12 | Declare string
13 | Format string
14 | Comment string
15 | VertexCount int
16 | HeaderLength int
17 | RowLength int
18 | text string
19 | mapOffset map[string]int
20 | mapType map[string]string
21 | }
22 |
23 | func ReadPlyHeaderString(plyFile string, readLen int) (string, error) {
24 | file, err := os.Open(plyFile)
25 | cmn.ExitOnError(err)
26 | defer file.Close()
27 |
28 | bs := make([]byte, readLen)
29 | _, err = file.Read(bs)
30 | if err != nil {
31 | return "", err
32 | }
33 |
34 | str := cmn.BytesToString(bs)
35 | if !cmn.Contains(str, "end_header\n") {
36 | if readLen > 1024*10 {
37 | return "", errors.New("ply header not found")
38 | }
39 | return ReadPlyHeaderString(plyFile, readLen+1024)
40 | }
41 |
42 | header := cmn.Split(str, "end_header\n")[0] + "end_header\n"
43 | return header, nil
44 | }
45 |
46 | func getPlyHeader(file *os.File, readLen int) (*PlyHeader, error) {
47 | bs := make([]byte, readLen)
48 | _, err := file.Read(bs)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | str := cmn.BytesToString(bs)
54 | if !cmn.Contains(str, "end_header\n") {
55 | if readLen > 1024*10 {
56 | return nil, errors.New("ply header not found")
57 | }
58 | return getPlyHeader(file, readLen+1024)
59 | }
60 |
61 | mapOffset := make(map[string]int)
62 | mapType := make(map[string]string)
63 |
64 | header := cmn.Split(str, "end_header\n")[0] + "end_header\n"
65 | lines := cmn.Split(strings.TrimRight(header, "\n"), "\n")
66 | vertexCount := -1
67 | declare := ""
68 | format := ""
69 | comment := ""
70 | offset := 0
71 | for i := 0; i < len(lines); i++ {
72 | if i == 0 {
73 | declare = lines[i]
74 | } else if cmn.Startwiths(lines[i], "property ") {
75 | ary := cmn.Split(lines[i], " ")
76 | mapOffset[ary[2]] = offset
77 | mapType[ary[2]] = ary[1]
78 | offset += getTypeSize(ary[1])
79 | } else if cmn.Startwiths(lines[i], "format ") {
80 | format = cmn.ReplaceAll(lines[i], "format ", "")
81 | } else if cmn.Startwiths(lines[i], "element vertex ") {
82 | vertexCount = cmn.StringToInt(cmn.ReplaceAll(lines[i], "element vertex ", ""))
83 | } else if cmn.Startwiths(lines[i], "comment ") {
84 | comment = cmn.ReplaceAll(lines[i], "comment ", "")
85 | }
86 | }
87 |
88 | plyHeader := &PlyHeader{
89 | text: header,
90 | Declare: declare,
91 | Format: format,
92 | Comment: comment,
93 | VertexCount: vertexCount,
94 | HeaderLength: len(header),
95 | RowLength: offset,
96 | mapType: mapType,
97 | mapOffset: mapOffset,
98 | }
99 | return plyHeader, nil
100 | }
101 |
102 | func getTypeSize(name string) int {
103 | if name == "float" {
104 | return 4
105 | } else if name == "double" {
106 | return 8
107 | } else if name == "int" {
108 | return 4
109 | } else if name == "uint" {
110 | return 4
111 | } else if name == "short" {
112 | return 2
113 | } else if name == "ushort" {
114 | return 2
115 | } else if name == "uchar" {
116 | return 1
117 | }
118 |
119 | log.Println("[Error] unsupported property type:", name)
120 | os.Exit(1)
121 | return 0 // unknown
122 | }
123 |
124 | func (p *PlyHeader) Property(property string) (int, string) {
125 | return p.mapOffset[property], p.mapType[property]
126 | }
127 |
128 | func (p *PlyHeader) MaxShDegree() int {
129 | if p.mapType["f_rest_44"] != "" {
130 | return 3
131 | } else if p.mapType["f_rest_23"] != "" {
132 | return 2
133 | } else if p.mapType["f_rest_8"] != "" {
134 | return 1
135 | }
136 | return 0
137 | }
138 |
139 | func (p *PlyHeader) IsPly() bool {
140 | return cmn.Startwiths(p.text, "ply\n")
141 | }
142 |
143 | func (p *PlyHeader) GetComment() string {
144 | return p.Comment
145 | }
146 |
147 | func (p *PlyHeader) GetFormat() string {
148 | return p.Format
149 | }
150 |
151 | func (p *PlyHeader) ToString() string {
152 | return p.text
153 | }
154 |
155 | // 是否3dgs官方格式ply
156 | func (p *PlyHeader) IsOfficialPly() bool {
157 | return p.Declare == "ply" &&
158 | p.Format == "binary_little_endian 1.0" &&
159 | p.VertexCount > 0 &&
160 | p.mapType["x"] != "" && p.mapType["y"] != "" && p.mapType["z"] != "" &&
161 | p.mapType["f_dc_0"] != "" && p.mapType["f_dc_1"] != "" && p.mapType["f_dc_2"] != "" &&
162 | p.mapType["opacity"] != "" && p.mapType["scale_0"] != "" && p.mapType["scale_1"] != "" && p.mapType["scale_2"] != "" &&
163 | p.mapType["rot_0"] != "" && p.mapType["rot_1"] != "" && p.mapType["rot_2"] != "" && p.mapType["rot_3"] != ""
164 | }
165 |
--------------------------------------------------------------------------------
/gsplat/header-spx.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "gsbox/cmn"
7 | "os"
8 | "strings"
9 | )
10 |
11 | /** 创建者ID */
12 | const ID1202056903 uint32 = 1202056903
13 |
14 | /** spx文件头长度 */
15 | const HeaderSizeSpx = 128
16 |
17 | /** Spx format file header */
18 | type SpxHeader struct {
19 | /** Fixed string, fixed to spx */
20 | Fixed string
21 | /** Spx version number, fixed to 1 */
22 | Version uint8
23 | /** Number of Gaussian primitives, must be specified */
24 | SplatCount int32
25 | /** Model bounding box vertices */
26 | MinX float32
27 | /** Model bounding box vertices */
28 | MaxX float32
29 | /** Model bounding box vertices */
30 | MinY float32
31 | /** Model bounding box vertices */
32 | MaxY float32
33 | /** Model bounding box vertices */
34 | MinZ float32
35 | /** Model bounding box vertices */
36 | MaxZ float32
37 | /** Min Center height */
38 | MinTopY float32
39 | /** Max Center height */
40 | MaxTopY float32
41 | /** Creation date (YYYYMMDD) */
42 | CreateDate uint32
43 | /** Creator identifier, 0 represents official tools */
44 | CreaterId uint32
45 | /** Exclusive format identifier, 0 represents official open format, can be customized, must be specified */
46 | ExclusiveId uint32
47 | /** spherical harmonics degree(1/2/3, others mean 0) */
48 | ShDegree uint8
49 | /** gaussian splat data type (default 0) */
50 | Flag1 uint8
51 | Flag2 uint8
52 | Flag3 uint8
53 | /** Reserved fields */
54 | Reserve1 uint32
55 | /** Reserved fields */
56 | Reserve2 uint32
57 | /** Comments (only supports ASCII characters) */
58 | Comment string
59 | /** Hash */
60 | Hash uint32
61 |
62 | checkHash bool
63 | }
64 |
65 | func (h *SpxHeader) IsValid() bool {
66 | return h.checkHash
67 | }
68 |
69 | func (h *SpxHeader) ToBytes() []byte {
70 | bts := make([]byte, 0)
71 | bts = append(bts, h.Fixed...)
72 | bts = append(bts, h.Version)
73 | bts = append(bts, cmn.Int32ToBytes(h.SplatCount)...)
74 | bts = append(bts, cmn.Float32ToBytes(h.MinX)...)
75 | bts = append(bts, cmn.Float32ToBytes(h.MaxX)...)
76 | bts = append(bts, cmn.Float32ToBytes(h.MinY)...)
77 | bts = append(bts, cmn.Float32ToBytes(h.MaxY)...)
78 | bts = append(bts, cmn.Float32ToBytes(h.MinZ)...)
79 | bts = append(bts, cmn.Float32ToBytes(h.MaxZ)...)
80 | bts = append(bts, cmn.Float32ToBytes(h.MinTopY)...)
81 | bts = append(bts, cmn.Float32ToBytes(h.MaxTopY)...)
82 | bts = append(bts, cmn.Uint32ToBytes(h.CreateDate)...)
83 | bts = append(bts, cmn.Uint32ToBytes(h.CreaterId)...)
84 | bts = append(bts, cmn.Uint32ToBytes(h.ExclusiveId)...)
85 | bts = append(bts, h.ShDegree)
86 | bts = append(bts, h.Flag1)
87 | bts = append(bts, h.Flag2)
88 | bts = append(bts, h.Flag3)
89 | bts = append(bts, cmn.Uint32ToBytes(h.Reserve1)...)
90 | bts = append(bts, cmn.Uint32ToBytes(h.Reserve2)...)
91 | bts = append(bts, cmn.StringToBytes(cmn.Left(cmn.Trim(h.Comment)+strings.Repeat(" ", 60), 60))...) // 右边补足空格取60个accsii字符
92 | bts = append(bts, cmn.Uint32ToBytes(cmn.HashBytes(bts[0:124]))...)
93 | return bts
94 | }
95 |
96 | func ParseSpxHeader(spxFile string) *SpxHeader {
97 | file, err := os.Open(spxFile)
98 | cmn.ExitOnError(err)
99 | defer file.Close()
100 |
101 | bs := make([]byte, HeaderSizeSpx)
102 | _, err = file.Read(bs)
103 | if err != nil {
104 | cmn.ExitOnError(err)
105 | }
106 |
107 | if bs[0] == 's' && bs[1] == 'p' && bs[2] == 'x' && bs[3] == 1 {
108 | header := readSpxHeader(bs)
109 | if header.ExclusiveId != 0 && !Args.HasCmd("info") {
110 | cmn.ExitOnError(errors.New("unknown exclusive id: " + cmn.Uint32ToString(header.ExclusiveId))) // 内含不识别的专属格式时,退出
111 | }
112 | return header
113 | }
114 |
115 | cmn.ExitOnError(errors.New("unknown format: " + spxFile))
116 | return nil
117 | }
118 |
119 | func readSpxHeader(bts []byte) *SpxHeader {
120 | header := &SpxHeader{
121 | Fixed: "spx",
122 | Version: bts[3],
123 | SplatCount: cmn.BytesToInt32(bts[4:8]),
124 | MinX: cmn.BytesToFloat32(bts[8:12]),
125 | MaxX: cmn.BytesToFloat32(bts[12:16]),
126 | MinY: cmn.BytesToFloat32(bts[16:20]),
127 | MaxY: cmn.BytesToFloat32(bts[20:24]),
128 | MinZ: cmn.BytesToFloat32(bts[24:28]),
129 | MaxZ: cmn.BytesToFloat32(bts[28:32]),
130 | MinTopY: cmn.BytesToFloat32(bts[32:36]),
131 | MaxTopY: cmn.BytesToFloat32(bts[36:40]),
132 | CreateDate: cmn.BytesToUint32(bts[40:44]),
133 | CreaterId: cmn.BytesToUint32(bts[44:48]),
134 | ExclusiveId: cmn.BytesToUint32(bts[48:52]),
135 | ShDegree: bts[52],
136 | Flag1: bts[53],
137 | Flag2: bts[54],
138 | Flag3: bts[55],
139 | Reserve1: cmn.BytesToUint32(bts[56:60]),
140 | Reserve2: cmn.BytesToUint32(bts[60:64]),
141 | Hash: cmn.BytesToUint32(bts[124:128]),
142 | checkHash: cmn.HashBytes(bts[0:124]) == cmn.BytesToUint32(bts[124:128]),
143 | }
144 | if header.ShDegree != 1 && header.ShDegree != 2 && header.ShDegree != 3 {
145 | header.ShDegree = 0
146 | }
147 |
148 | comment := ""
149 | for i := 64; i < 124; i++ {
150 | comment += cmn.BytesToString(bts[i : i+1])
151 | }
152 | header.Comment = cmn.Trim(comment)
153 |
154 | return header
155 | }
156 |
157 | func (h *SpxHeader) ToStringSpx() string {
158 | return fmt.Sprintf("3DGS model format spx\nSpx version : %v\nSplatCount : %v\nMinX, MaxX : %v, %v\nMinY, MaxY : %v, %v\nMinZ, MaxZ : %v, %v\nMinTopY : %v\nMaxTopY : %v\nCreateDate : %v\nCreaterId : %v\nExclusiveId : %v\nShDegree : %v\nFlag1 : %v\nFlag2 : %v\nFlag3 : %v\nComment : %v\nHash : %v (%v)",
159 | h.Version, h.SplatCount, h.MinX, h.MaxX, h.MinY, h.MaxY, h.MinZ, h.MaxZ, h.MinTopY, h.MaxTopY, h.CreateDate, h.CreaterId, h.ExclusiveId, h.ShDegree, h.Flag1, h.Flag2, h.Flag3, h.Comment, h.Hash, h.checkHash)
160 | }
161 |
--------------------------------------------------------------------------------
/gsplat/header-spz.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "gsbox/cmn"
7 | )
8 |
9 | const HeaderSizeSpz = 16
10 | const SPZ_MAGIC = 0x5053474e // NGSP = Niantic gaussian splat
11 |
12 | type SpzHeader struct {
13 | /** 1347635022 */
14 | Magic uint32
15 | /** 2 */
16 | Version uint32
17 | /** Number of Gaussian primitives, must be specified */
18 | NumPoints uint32
19 | /** 0,1,2,3 */
20 | ShDegree uint8
21 | /** Reserved fields */
22 | FractionalBits uint8
23 | /** Reserved fields */
24 | Flags uint8
25 | /** 0 */
26 | Reserved uint8
27 | }
28 |
29 | func (h *SpzHeader) ToBytes() []byte {
30 | bts := make([]byte, 0)
31 | bts = append(bts, cmn.Uint32ToBytes(h.Magic)...)
32 | bts = append(bts, cmn.Uint32ToBytes(h.Version)...)
33 | bts = append(bts, cmn.Uint32ToBytes(h.NumPoints)...)
34 | bts = append(bts, h.ShDegree)
35 | bts = append(bts, h.FractionalBits)
36 | bts = append(bts, h.Flags)
37 | bts = append(bts, h.Reserved)
38 | return bts
39 | }
40 |
41 | func ParseSpzHeader(bts []byte) *SpzHeader {
42 | header := &SpzHeader{
43 | Magic: cmn.BytesToUint32(bts[0:4]),
44 | Version: cmn.BytesToUint32(bts[4:8]),
45 | NumPoints: cmn.BytesToUint32(bts[8:12]),
46 | ShDegree: bts[12],
47 | FractionalBits: bts[13],
48 | Flags: bts[14],
49 | Reserved: bts[15],
50 | }
51 |
52 | if header.Magic != SPZ_MAGIC {
53 | cmn.ExitOnError(errors.New("[SPZ ERROR] deserializePackedGaussians: header not found"))
54 | }
55 | if header.Version != 2 {
56 | cmn.ExitOnError(errors.New("[SPZ ERROR] deserializePackedGaussians: version not supported: " + cmn.Uint32ToString(header.Version)))
57 | }
58 | if header.ShDegree > 3 {
59 | cmn.ExitOnError(errors.New("[SPZ ERROR] deserializePackedGaussians: Unsupported SH degree: " + cmn.IntToString(int(header.ShDegree))))
60 | }
61 | if header.FractionalBits != 12 {
62 | // 仅支持这一种编码方式(坐标24位整数编码)
63 | cmn.ExitOnError(errors.New("[SPZ ERROR] deserializePackedGaussians: Unsupported FractionalBits: " + cmn.IntToString(int(header.FractionalBits))))
64 | }
65 |
66 | return header
67 | }
68 |
69 | func (h *SpzHeader) ToString() string {
70 | return fmt.Sprintf("3DGS model format spz\nMagic : %v\nVersion : %v\nNumPoints : %v\nShDegree : %v\nFractionalBits : %v\nFlags : %v\nReserved : %v",
71 | h.Magic, h.Version, h.NumPoints, h.ShDegree, h.FractionalBits, h.Flags, h.Reserved)
72 | }
73 |
--------------------------------------------------------------------------------
/gsplat/read-gs-ply.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "gsbox/cmn"
8 | "log"
9 | "os"
10 | )
11 |
12 | const SH_C0 float64 = 0.28209479177387814
13 |
14 | func ReadPlyHeader(plyFile string) (*PlyHeader, error) {
15 | file, err := os.Open(plyFile)
16 | cmn.ExitOnError(err)
17 | defer file.Close()
18 | return getPlyHeader(file, 2048)
19 | }
20 |
21 | func ReadPly(plyFile string, plyTypes ...string) (*PlyHeader, []*SplatData) {
22 | file, err := os.Open(plyFile)
23 | cmn.ExitOnError(err)
24 | defer file.Close()
25 |
26 | header, err := getPlyHeader(file, 2048)
27 | cmn.ExitOnError(err)
28 |
29 | if len(plyTypes) > 0 && cmn.EqualsIngoreCase(plyTypes[0], "ply-3dgs") {
30 | if !header.IsOfficialPly() {
31 | cmn.ExitOnError(errors.New("unsupported ply file: " + plyFile))
32 | }
33 | }
34 |
35 | datas := make([]*SplatData, header.VertexCount)
36 | for i := 0; i < header.VertexCount; i++ {
37 | dataBytes := make([]byte, header.RowLength)
38 | _, err := file.ReadAt(dataBytes, int64(header.HeaderLength+i*header.RowLength))
39 | cmn.ExitOnError(err)
40 |
41 | data := &SplatData{}
42 | data.PositionX = cmn.ClipFloat32(readValue(header, "x", dataBytes))
43 | data.PositionY = cmn.ClipFloat32(readValue(header, "y", dataBytes))
44 | data.PositionZ = cmn.ClipFloat32(readValue(header, "z", dataBytes))
45 | data.ScaleX = cmn.ClipFloat32(readValue(header, "scale_0", dataBytes))
46 | data.ScaleY = cmn.ClipFloat32(readValue(header, "scale_1", dataBytes))
47 | data.ScaleZ = cmn.ClipFloat32(readValue(header, "scale_2", dataBytes))
48 | data.ColorR = cmn.EncodeSplatColor(readValue(header, "f_dc_0", dataBytes))
49 | data.ColorG = cmn.EncodeSplatColor(readValue(header, "f_dc_1", dataBytes))
50 | data.ColorB = cmn.EncodeSplatColor(readValue(header, "f_dc_2", dataBytes))
51 | data.ColorA = cmn.EncodeSplatOpacity(readValue(header, "opacity", dataBytes))
52 | data.RotationW = cmn.EncodeSplatRotation(readValue(header, "rot_0", dataBytes))
53 | data.RotationX = cmn.EncodeSplatRotation(readValue(header, "rot_1", dataBytes))
54 | data.RotationY = cmn.EncodeSplatRotation(readValue(header, "rot_2", dataBytes))
55 | data.RotationZ = cmn.EncodeSplatRotation(readValue(header, "rot_3", dataBytes))
56 |
57 | datas[i] = data
58 |
59 | shDim := 0
60 | maxShDegree := header.MaxShDegree()
61 | if maxShDegree == 1 {
62 | shDim = 3
63 | } else if maxShDegree == 2 {
64 | shDim = 8
65 | } else if maxShDegree == 3 {
66 | shDim = 15
67 | }
68 |
69 | shs := make([]byte, 45)
70 | n := 0
71 | for j := range shDim {
72 | for c := range 3 {
73 | shs[n] = cmn.EncodeSplatSH(readValue(header, "f_rest_"+cmn.IntToString(j+c*shDim), dataBytes))
74 | n++
75 | }
76 | }
77 | for ; n < 45; n++ {
78 | shs[n] = cmn.EncodeSplatSH(0)
79 | }
80 |
81 | if maxShDegree == 3 {
82 | data.SH2 = shs[:24]
83 | data.SH3 = shs[24:]
84 | } else if maxShDegree == 2 {
85 | data.SH2 = shs[:24]
86 | } else if maxShDegree == 1 {
87 | data.SH1 = shs[:9]
88 | }
89 | }
90 |
91 | return header, datas
92 | }
93 |
94 | func readValue(header *PlyHeader, property string, splatDataBytes []byte) float64 {
95 | offset, typename := header.Property(property)
96 | if typename == "float" {
97 | var v float32
98 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+4]), binary.LittleEndian, &v))
99 | return float64(v)
100 | } else if typename == "double" {
101 | var v float64
102 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+8]), binary.LittleEndian, &v))
103 | return v
104 | } else if typename == "int" {
105 | var v int32
106 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+4]), binary.LittleEndian, &v))
107 | return float64(v)
108 | } else if typename == "uint" {
109 | var v uint32
110 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+4]), binary.LittleEndian, &v))
111 | return float64(v)
112 | } else if typename == "short" {
113 | var v int16
114 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+2]), binary.LittleEndian, &v))
115 | return float64(v)
116 | } else if typename == "ushort" {
117 | var v uint16
118 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+2]), binary.LittleEndian, &v))
119 | return float64(v)
120 | } else if typename == "uchar" {
121 | var v uint8
122 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatDataBytes[offset:offset+1]), binary.LittleEndian, &v))
123 | return float64(v)
124 | }
125 |
126 | if cmn.Startwiths(property, "f_rest_") {
127 | return 0 // 球谐系数读取不到时,默认为0
128 | }
129 |
130 | log.Println("[Error] unsupported property:", "property", typename, property)
131 | os.Exit(1)
132 | return 0
133 | }
134 |
--------------------------------------------------------------------------------
/gsplat/read-gs-splat.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "gsbox/cmn"
7 | "os"
8 | )
9 |
10 | func ReadSplat(splatFile string) []*SplatData {
11 |
12 | file, err := os.Open(splatFile)
13 | cmn.ExitOnError(err)
14 | defer file.Close()
15 |
16 | fileInfo, err := file.Stat()
17 | cmn.ExitOnError(err)
18 | fileSize := fileInfo.Size()
19 | count := fileSize / SPLAT_DATA_SIZE
20 |
21 | var i int64 = 0
22 | datas := make([]*SplatData, count)
23 | for ; i < count; i++ {
24 | splatBytes := make([]byte, SPLAT_DATA_SIZE)
25 | _, err = file.ReadAt(splatBytes, i*SPLAT_DATA_SIZE)
26 | cmn.ExitOnError(err)
27 |
28 | splatData := &SplatData{}
29 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[0:4]), binary.LittleEndian, &splatData.PositionX))
30 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[4:8]), binary.LittleEndian, &splatData.PositionY))
31 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[8:12]), binary.LittleEndian, &splatData.PositionZ))
32 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[12:16]), binary.LittleEndian, &splatData.ScaleX))
33 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[16:20]), binary.LittleEndian, &splatData.ScaleY))
34 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[20:24]), binary.LittleEndian, &splatData.ScaleZ))
35 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[24:25]), binary.LittleEndian, &splatData.ColorR))
36 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[25:26]), binary.LittleEndian, &splatData.ColorG))
37 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[26:27]), binary.LittleEndian, &splatData.ColorB))
38 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[27:28]), binary.LittleEndian, &splatData.ColorA))
39 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[28:29]), binary.LittleEndian, &splatData.RotationW))
40 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[29:30]), binary.LittleEndian, &splatData.RotationX))
41 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[30:31]), binary.LittleEndian, &splatData.RotationY))
42 | cmn.ExitOnError(binary.Read(bytes.NewReader(splatBytes[31:32]), binary.LittleEndian, &splatData.RotationZ))
43 |
44 | splatData.ScaleX = cmn.DecodeSplatScale(splatData.ScaleX)
45 | splatData.ScaleY = cmn.DecodeSplatScale(splatData.ScaleY)
46 | splatData.ScaleZ = cmn.DecodeSplatScale(splatData.ScaleZ)
47 | datas[i] = splatData
48 | }
49 |
50 | return datas
51 | }
52 |
--------------------------------------------------------------------------------
/gsplat/read-gs-spx.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "errors"
5 | "gsbox/cmn"
6 | "log"
7 | "math"
8 | "os"
9 | )
10 |
11 | func ReadSpx(spxFile string) (*SpxHeader, []*SplatData) {
12 |
13 | header := ParseSpxHeader(spxFile)
14 | if !header.IsValid() && header.CreaterId == ID1202056903 && header.ExclusiveId == 0 {
15 | log.Println("[Warn] hash check failed! CreaterId:" + cmn.Uint32ToString(header.CreaterId) + ", ExclusiveId:" + cmn.Uint32ToString(header.ExclusiveId))
16 | }
17 |
18 | file, err := os.Open(spxFile)
19 | cmn.ExitOnError(err)
20 | defer file.Close()
21 |
22 | datas := make([]*SplatData, 0)
23 | offset := int64(HeaderSizeSpx)
24 | var n1, n2, n3 int
25 | for {
26 | // 块数据长度、是否压缩
27 | bts := make([]byte, 4)
28 | _, err = file.ReadAt(bts, offset)
29 | if err != nil {
30 | break
31 | }
32 |
33 | blockSize := int64(math.Abs(float64(cmn.BytesToInt32(bts[0:]))))
34 | isGzip := cmn.BytesToInt32(bts[0:]) < 0
35 |
36 | // 块数据读取
37 | offset += 4
38 | blockBytes := make([]byte, blockSize)
39 | _, err = file.ReadAt(blockBytes, offset)
40 | cmn.ExitOnError(err)
41 | offset += blockSize
42 |
43 | // 块数据解压
44 | var blockBts []byte
45 | if isGzip {
46 | blockBts, err = cmn.UnGzipBytes(blockBytes)
47 | cmn.ExitOnError(err)
48 | } else {
49 | blockBts = blockBytes
50 | }
51 |
52 | // 块数据格式
53 | i32BlockSplatCount := int32(cmn.BytesToUint32(blockBts[0:4]))
54 | blkSplatCnt := int(i32BlockSplatCount) // 数量
55 | formatId := cmn.BytesToUint32(blockBts[4:8]) // 格式ID
56 | if formatId == 20 {
57 | // splat20
58 | bts := blockBts[8:] // 除去前8字节(数量,格式)
59 | for n := range blkSplatCnt {
60 | data := &SplatData{}
61 | data.PositionX = cmn.DecodeSpxPositionUint24(bts[n*3], bts[n*3+1], bts[n*3+2])
62 | data.PositionY = cmn.DecodeSpxPositionUint24(bts[blkSplatCnt*3+n*3], bts[blkSplatCnt*3+n*3+1], bts[blkSplatCnt*3+n*3+2])
63 | data.PositionZ = cmn.DecodeSpxPositionUint24(bts[blkSplatCnt*6+n*3], bts[blkSplatCnt*6+n*3+1], bts[blkSplatCnt*6+n*3+2])
64 | data.ScaleX = cmn.DecodeSpxScale(bts[blkSplatCnt*9+n])
65 | data.ScaleY = cmn.DecodeSpxScale(bts[blkSplatCnt*10+n])
66 | data.ScaleZ = cmn.DecodeSpxScale(bts[blkSplatCnt*11+n])
67 | data.ColorR = bts[blkSplatCnt*12+n]
68 | data.ColorG = bts[blkSplatCnt*13+n]
69 | data.ColorB = bts[blkSplatCnt*14+n]
70 | data.ColorA = bts[blkSplatCnt*15+n]
71 | data.RotationW, data.RotationX, data.RotationY, data.RotationZ = cmn.NormalizeRotations(bts[blkSplatCnt*16+n], bts[blkSplatCnt*17+n], bts[blkSplatCnt*18+n], bts[blkSplatCnt*19+n])
72 | datas = append(datas, data)
73 | }
74 |
75 | } else if formatId == 1 {
76 | // SH1
77 | dataBytes := blockBts[8:] // 除去前8字节(数量,格式)
78 | for n := range blkSplatCnt {
79 | splatData := datas[n1+n]
80 | splatData.SH1 = dataBytes[n*9 : n*9+9]
81 | }
82 | n1 += blkSplatCnt
83 | } else if formatId == 2 {
84 | // SH2
85 | dataBytes := blockBts[8:] // 除去前8字节(数量,格式)
86 | for n := range blkSplatCnt {
87 | splatData := datas[n2+n]
88 | splatData.SH2 = dataBytes[n*24 : n*24+24]
89 | }
90 | n2 += blkSplatCnt
91 | } else if formatId == 3 {
92 | // SH3
93 | dataBytes := blockBts[8:] // 除去前8字节(数量,格式)
94 | for n := range blkSplatCnt {
95 | splatData := datas[n3+n]
96 | splatData.SH3 = dataBytes[n*21 : n*21+21]
97 | }
98 | n3 += blkSplatCnt
99 | } else {
100 | // 存在无法识别读取的专有格式数据
101 | cmn.ExitOnError(errors.New("unknow block data format exists: " + cmn.Uint32ToString(formatId)))
102 | }
103 |
104 | }
105 |
106 | return header, datas
107 | }
108 |
--------------------------------------------------------------------------------
/gsplat/read-gs-spz.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "errors"
5 | "gsbox/cmn"
6 | "io"
7 | "os"
8 | )
9 |
10 | func ReadSpz(spzFile string, readHeadOnly bool) (*SpzHeader, []*SplatData) {
11 |
12 | file, err := os.Open(spzFile)
13 | cmn.ExitOnConditionError(err != nil, errors.New("[SPZ ERROR] File open failed"))
14 | defer file.Close()
15 |
16 | gzipDatas, err := io.ReadAll(file)
17 | cmn.ExitOnConditionError(err != nil, errors.New("[SPZ ERROR] File read failed"))
18 |
19 | ungzipDatas, err := cmn.UnGzipBytes(gzipDatas)
20 | cmn.ExitOnConditionError(err != nil, errors.New("[SPZ ERROR] UnGzip failed"))
21 | cmn.ExitOnConditionError(len(ungzipDatas) < HeaderSizeSpz, errors.New("[SPZ ERROR] Invalid spz header"))
22 |
23 | header := ParseSpzHeader(ungzipDatas[0:HeaderSizeSpz])
24 | if readHeadOnly {
25 | return header, nil
26 | }
27 |
28 | datas := readSpzDatas(ungzipDatas[HeaderSizeSpz:], header)
29 | return header, datas
30 | }
31 |
32 | func readSpzDatas(datas []byte, h *SpzHeader) []*SplatData {
33 | positionSize := int(h.NumPoints) * 9
34 | alphaSize := int(h.NumPoints)
35 | colorSize := int(h.NumPoints) * 3
36 | scaleSize := int(h.NumPoints) * 3
37 | rotationSize := int(h.NumPoints) * 3
38 |
39 | offsetpositions := 0
40 | offsetAlphas := offsetpositions + positionSize
41 | offsetColors := offsetAlphas + alphaSize
42 | offsetScales := offsetColors + colorSize
43 | offsetRotations := offsetScales + scaleSize
44 | offsetShs := offsetRotations + rotationSize
45 |
46 | shDim := 0
47 | if h.ShDegree == 1 {
48 | shDim = int(h.NumPoints) * 9
49 | } else if h.ShDegree == 2 {
50 | shDim = int(h.NumPoints) * 24
51 | } else if h.ShDegree == 3 {
52 | shDim = int(h.NumPoints) * 45
53 | }
54 | cmn.ExitOnConditionError(len(datas) != int(h.NumPoints)*19+shDim, errors.New("[SPZ ERROR] Invalid spz data"))
55 |
56 | positions := datas[0:positionSize]
57 | scales := datas[offsetScales : offsetScales+scaleSize]
58 | rotations := datas[offsetRotations : offsetRotations+rotationSize]
59 | alphas := datas[offsetAlphas : offsetAlphas+alphaSize]
60 | colors := datas[offsetColors : offsetColors+colorSize]
61 | shs := datas[offsetShs:]
62 |
63 | var splatDatas []*SplatData
64 | for i := range int(h.NumPoints) {
65 | data := &SplatData{}
66 | data.PositionX = cmn.SpzDecodePosition(positions[i*9:i*9+3], h.FractionalBits)
67 | data.PositionY = cmn.SpzDecodePosition(positions[i*9+3:i*9+6], h.FractionalBits)
68 | data.PositionZ = cmn.SpzDecodePosition(positions[i*9+6:i*9+9], h.FractionalBits)
69 | data.ScaleX = cmn.SpzDecodeScale(scales[i*3])
70 | data.ScaleY = cmn.SpzDecodeScale(scales[i*3+1])
71 | data.ScaleZ = cmn.SpzDecodeScale(scales[i*3+2])
72 | data.ColorR = cmn.SpzDecodeColor(colors[i*3])
73 | data.ColorG = cmn.SpzDecodeColor(colors[i*3+1])
74 | data.ColorB = cmn.SpzDecodeColor(colors[i*3+2])
75 | data.ColorA = alphas[i]
76 | data.RotationW, data.RotationX, data.RotationY, data.RotationZ = cmn.SpzDecodeRotations(rotations[i*3], rotations[i*3+1], rotations[i*3+2])
77 | if h.ShDegree == 1 {
78 | data.SH1 = shs[i*9 : i*9+9]
79 | } else if h.ShDegree == 2 {
80 | data.SH2 = shs[i*24 : i*24+24]
81 | } else if h.ShDegree == 3 {
82 | data.SH2 = shs[i*45 : i*45+24]
83 | data.SH3 = shs[i*45+24 : i*45+45]
84 | }
85 |
86 | splatDatas = append(splatDatas, data)
87 | }
88 |
89 | return splatDatas
90 | }
91 |
--------------------------------------------------------------------------------
/gsplat/write-gs-ply.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bufio"
5 | "gsbox/cmn"
6 | "log"
7 | "os"
8 | )
9 |
10 | func WritePly(plyFile string, datas []*SplatData, comment string, shDegree int) {
11 | file, err := os.Create(plyFile)
12 | cmn.ExitOnError(err)
13 | defer file.Close()
14 |
15 | log.Println("[Info] output shDegree:", shDegree)
16 | writer := bufio.NewWriter(file)
17 | _, err = writer.WriteString(genPlyHeader(len(datas), comment, shDegree))
18 | cmn.ExitOnError(err)
19 | for i := 0; i < len(datas); i++ {
20 | _, err = writer.Write(genPlyDataBin(datas[i], shDegree))
21 | cmn.ExitOnError(err)
22 | }
23 | err = writer.Flush()
24 | cmn.ExitOnError(err)
25 | }
26 |
27 | func genPlyDataBin(splatData *SplatData, shDegree int) []byte {
28 |
29 | bts := []byte{}
30 | bts = append(bts, cmn.Float32ToBytes(splatData.PositionX)...) // x
31 | bts = append(bts, cmn.Float32ToBytes(splatData.PositionY)...) // y
32 | bts = append(bts, cmn.Float32ToBytes(splatData.PositionZ)...) // z
33 | bts = append(bts, make([]byte, 3*4)...) // nx, ny, nz
34 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatColor(splatData.ColorR))...) // f_dc_0
35 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatColor(splatData.ColorG))...) // f_dc_1
36 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatColor(splatData.ColorB))...) // f_dc_2
37 |
38 | if shDegree > 0 {
39 | shDim := 0
40 | var shs []byte
41 | if shDegree == 1 {
42 | shDim = 3
43 | if len(splatData.SH1) > 0 {
44 | shs = append(shs, splatData.SH1...)
45 | } else if len(splatData.SH2) > 0 {
46 | shs = append(shs, splatData.SH2[:9]...)
47 | }
48 | } else if shDegree == 2 {
49 | shDim = 8
50 | if len(splatData.SH1) > 0 {
51 | shs = append(shs, splatData.SH1...)
52 | } else if len(splatData.SH2) > 0 {
53 | shs = append(shs, splatData.SH2...)
54 | }
55 | } else if shDegree == 3 {
56 | shDim = 15
57 | if len(splatData.SH3) > 0 {
58 | shs = append(shs, splatData.SH2...)
59 | shs = append(shs, splatData.SH3...)
60 | } else if len(splatData.SH2) > 0 {
61 | shs = append(shs, splatData.SH2...)
62 | } else if len(splatData.SH1) > 0 {
63 | shs = append(shs, splatData.SH1...)
64 | }
65 | }
66 |
67 | for i := len(shs); i < 45; i++ {
68 | shs = append(shs, cmn.EncodeSplatSH(0.0))
69 | }
70 |
71 | for c := range 3 {
72 | for i := range shDim {
73 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatSH(shs[c+i*3]))...) // f_rest_0 ... f_rest_n
74 | }
75 | }
76 | }
77 |
78 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatOpacity(splatData.ColorA))...) // opacity
79 | bts = append(bts, cmn.Float32ToBytes(splatData.ScaleX)...) // scale_0
80 | bts = append(bts, cmn.Float32ToBytes(splatData.ScaleY)...) // scale_1
81 | bts = append(bts, cmn.Float32ToBytes(splatData.ScaleZ)...) // scale_2
82 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatRotation(splatData.RotationW))...) // rot_0
83 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatRotation(splatData.RotationX))...) // rot_1
84 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatRotation(splatData.RotationY))...) // rot_2
85 | bts = append(bts, cmn.Float32ToBytes(cmn.DecodeSplatRotation(splatData.RotationZ))...) // rot_3
86 |
87 | return bts
88 | }
89 |
90 | func genPlyHeader(count int, comment string, shDegree int) string {
91 | lines := []string{}
92 | lines = append(lines, "ply")
93 | lines = append(lines, "format binary_little_endian 1.0")
94 | lines = append(lines, "element vertex "+cmn.IntToString(count))
95 | if comment != "" {
96 | _, comment = cmn.RemoveNonASCII(comment)
97 | lines = append(lines, "comment "+comment)
98 | }
99 | lines = append(lines, "property float x")
100 | lines = append(lines, "property float y")
101 | lines = append(lines, "property float z")
102 | lines = append(lines, "property float nx")
103 | lines = append(lines, "property float ny")
104 | lines = append(lines, "property float nz")
105 | lines = append(lines, "property float f_dc_0")
106 | lines = append(lines, "property float f_dc_1")
107 | lines = append(lines, "property float f_dc_2")
108 | if shDegree == 1 {
109 | for i := range 9 {
110 | lines = append(lines, "property float f_rest_"+cmn.IntToString(i))
111 | }
112 | } else if shDegree == 2 {
113 | for i := range 24 {
114 | lines = append(lines, "property float f_rest_"+cmn.IntToString(i))
115 | }
116 | } else if shDegree == 3 {
117 | for i := range 45 {
118 | lines = append(lines, "property float f_rest_"+cmn.IntToString(i))
119 | }
120 | }
121 | lines = append(lines, "property float opacity")
122 | lines = append(lines, "property float scale_0")
123 | lines = append(lines, "property float scale_1")
124 | lines = append(lines, "property float scale_2")
125 | lines = append(lines, "property float rot_0")
126 | lines = append(lines, "property float rot_1")
127 | lines = append(lines, "property float rot_2")
128 | lines = append(lines, "property float rot_3")
129 | lines = append(lines, "end_header")
130 | return cmn.Join(lines, "\n") + "\n"
131 | }
132 |
--------------------------------------------------------------------------------
/gsplat/write-gs-splat.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bufio"
5 | "gsbox/cmn"
6 | "os"
7 | )
8 |
9 | func WriteSplat(splatFile string, rows []*SplatData) {
10 | file, err := os.Create(splatFile)
11 | cmn.ExitOnError(err)
12 | defer file.Close()
13 |
14 | writer := bufio.NewWriter(file)
15 |
16 | for i := range rows {
17 | bts := make([]byte, 0)
18 | bts = append(bts, cmn.Float32ToBytes(rows[i].PositionX)...)
19 | bts = append(bts, cmn.Float32ToBytes(rows[i].PositionY)...)
20 | bts = append(bts, cmn.Float32ToBytes(rows[i].PositionZ)...)
21 | bts = append(bts, cmn.Float32ToBytes(cmn.EncodeSplatScale(rows[i].ScaleX))...)
22 | bts = append(bts, cmn.Float32ToBytes(cmn.EncodeSplatScale(rows[i].ScaleY))...)
23 | bts = append(bts, cmn.Float32ToBytes(cmn.EncodeSplatScale(rows[i].ScaleZ))...)
24 | bts = append(bts, rows[i].ColorR, rows[i].ColorG, rows[i].ColorB, rows[i].ColorA)
25 | bts = append(bts, rows[i].RotationW, rows[i].RotationX, rows[i].RotationY, rows[i].RotationZ)
26 | _, err = writer.Write(bts)
27 | cmn.ExitOnError(err)
28 | }
29 | err = writer.Flush()
30 | cmn.ExitOnError(err)
31 | }
32 |
--------------------------------------------------------------------------------
/gsplat/write-gs-spx.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bufio"
5 | "gsbox/cmn"
6 | "log"
7 | "math"
8 | "os"
9 | "sort"
10 | )
11 |
12 | const MinGzipBlockSize = 64
13 |
14 | func WriteSpx(spxFile string, rows []*SplatData, comment string, shDegree int, flag1 uint8, flag2 uint8, flag3 uint8) {
15 | file, err := os.Create(spxFile)
16 | cmn.ExitOnError(err)
17 | defer file.Close()
18 |
19 | log.Println("[Info] output shDegree:", shDegree)
20 | writer := bufio.NewWriter(file)
21 |
22 | blockSize := cmn.StringToInt(Args.GetArgIgnorecase("-bs", "--block-size"), 20480)
23 | if blockSize <= 0 {
24 | blockSize = len(rows) // 所有数据放到一个块
25 | } else if blockSize < MinGzipBlockSize {
26 | blockSize = MinGzipBlockSize // 最小
27 | } else if blockSize > 512000 {
28 | blockSize = 512000 // 最大512000
29 | }
30 |
31 | header := genSpxHeader(rows, comment, shDegree, flag1, flag2, flag3)
32 | _, err = writer.Write(header.ToBytes())
33 | cmn.ExitOnError(err)
34 |
35 | var blockDatasList [][]*SplatData
36 | blockCnt := (int(header.SplatCount) + blockSize - 1) / blockSize
37 | for i := range blockCnt {
38 | blockDatas := make([]*SplatData, 0)
39 | max := min(i*blockSize+blockSize, int(header.SplatCount))
40 | for n := i * blockSize; n < max; n++ {
41 | blockDatas = append(blockDatas, rows[n])
42 | }
43 | writeSpxBlockSplat20(writer, blockDatas, len(blockDatas))
44 | blockDatasList = append(blockDatasList, blockDatas)
45 | }
46 |
47 | if shDegree == 1 {
48 | for i := range blockDatasList {
49 | writeSpxBlockSH1(writer, blockDatasList[i])
50 | }
51 | } else if shDegree == 2 {
52 | for i := range blockDatasList {
53 | writeSpxBlockSH2(writer, blockDatasList[i])
54 | }
55 | } else if shDegree == 3 {
56 | for i := range blockDatasList {
57 | writeSpxBlockSH2(writer, blockDatasList[i])
58 | writeSpxBlockSH3(writer, blockDatasList[i])
59 | }
60 | }
61 |
62 | err = writer.Flush()
63 | cmn.ExitOnError(err)
64 | }
65 |
66 | func genSpxHeader(datas []*SplatData, comment string, shDegree int, flag1 uint8, flag2 uint8, flag3 uint8) *SpxHeader {
67 |
68 | header := new(SpxHeader)
69 | header.Fixed = "spx"
70 | header.Version = 1
71 | header.SplatCount = int32(len(datas))
72 |
73 | header.CreateDate = cmn.GetSystemDateYYYYMMDD() // 创建日期
74 | header.CreaterId = ID1202056903 // 0:官方默认识别号,(这里参考阿佩里常数1.202056903159594…以示区分,此常数由瑞士数学家罗杰·阿佩里在1978年证明其无理数性质而闻名)
75 | header.ExclusiveId = 0 // 0:官方开放格式的识别号
76 | header.ShDegree = uint8(shDegree)
77 | header.Flag1 = flag1
78 | header.Flag2 = flag2
79 | header.Flag3 = flag3
80 | header.Reserve1 = 0
81 | header.Reserve2 = 0
82 | del, comment := cmn.RemoveNonASCII(comment)
83 | if del {
84 | log.Println("[Warn] The existing non-ASCII characters in the comment have been removed!")
85 | }
86 | header.Comment = comment // 注释
87 | if header.Comment == "" {
88 | header.Comment = "created by gsbox " + cmn.VER + " https://github.com/gotoeasy/gsbox"
89 | }
90 |
91 | if len(datas) > 0 {
92 | minX := float64(datas[0].PositionX)
93 | minY := float64(datas[0].PositionY)
94 | minZ := float64(datas[0].PositionZ)
95 | maxX := float64(datas[0].PositionX)
96 | maxY := float64(datas[0].PositionY)
97 | maxZ := float64(datas[0].PositionZ)
98 |
99 | for i := 1; i < len(datas); i++ {
100 | minX = math.Min(minX, float64(datas[i].PositionX))
101 | minY = math.Min(minY, float64(datas[i].PositionY))
102 | minZ = math.Min(minZ, float64(datas[i].PositionZ))
103 | maxX = math.Max(maxX, float64(datas[i].PositionX))
104 | maxY = math.Max(maxY, float64(datas[i].PositionY))
105 | maxZ = math.Max(maxZ, float64(datas[i].PositionZ))
106 | }
107 | header.MinX = cmn.ToFloat32(minX)
108 | header.MaxX = cmn.ToFloat32(maxX)
109 | header.MinY = cmn.ToFloat32(minY)
110 | header.MaxY = cmn.ToFloat32(maxY)
111 | header.MinZ = cmn.ToFloat32(minZ)
112 | header.MaxZ = cmn.ToFloat32(maxZ)
113 |
114 | // TopY
115 | centerX := cmn.ToFloat32((maxX + minX) / 2)
116 | centerY := cmn.ToFloat32((maxY + minY) / 2)
117 | centerZ := cmn.ToFloat32((maxZ + minZ) / 2)
118 | radius10 := math.Sqrt(float64((centerX-header.MaxX)*(centerX-header.MaxX)+
119 | (centerY-header.MaxY)*(centerY-header.MaxY)+
120 | (centerZ-header.MaxZ)*(centerZ-header.MaxZ))) * 0.1
121 |
122 | minTopY := float64(header.MaxY)
123 | maxTopY := float64(header.MinY)
124 | for i := range datas {
125 | if math.Abs(float64(datas[i].PositionY)) < 30 && math.Sqrt(float64(datas[i].PositionX)*float64(datas[i].PositionX)+float64(datas[i].PositionZ)*float64(datas[i].PositionZ)) <= radius10 {
126 | minTopY = math.Min(minTopY, float64(datas[i].PositionY))
127 | maxTopY = math.Max(maxTopY, float64(datas[i].PositionY))
128 | }
129 | }
130 | header.MinTopY = cmn.ToFloat32(minTopY)
131 | header.MaxTopY = cmn.ToFloat32(maxTopY)
132 | }
133 |
134 | return header
135 | }
136 |
137 | func writeSpxBlockSplat20(writer *bufio.Writer, blockDatas []*SplatData, blockSplatCount int) {
138 | sort.Slice(blockDatas, func(i, j int) bool {
139 | return blockDatas[i].PositionY < blockDatas[j].PositionY // 坐标分别占3字节,按其中任一排序以更利于压缩
140 | })
141 |
142 | bts := make([]byte, 0)
143 | bts = append(bts, cmn.Uint32ToBytes(uint32(blockSplatCount))...) // 块中的高斯点个数
144 | bts = append(bts, cmn.Uint32ToBytes(20)...) // 开放的块数据格式 20:splat20重排
145 |
146 | for n := range blockSplatCount {
147 | bts = append(bts, cmn.EncodeSpxPositionUint24(blockDatas[n].PositionX)...)
148 | }
149 | for n := range blockSplatCount {
150 | bts = append(bts, cmn.EncodeSpxPositionUint24(blockDatas[n].PositionY)...)
151 | }
152 | for n := range blockSplatCount {
153 | bts = append(bts, cmn.EncodeSpxPositionUint24(blockDatas[n].PositionZ)...)
154 | }
155 | for n := range blockSplatCount {
156 | bts = append(bts, cmn.EncodeSpxScale(blockDatas[n].ScaleX))
157 | }
158 | for n := range blockSplatCount {
159 | bts = append(bts, cmn.EncodeSpxScale(blockDatas[n].ScaleY))
160 | }
161 | for n := range blockSplatCount {
162 | bts = append(bts, cmn.EncodeSpxScale(blockDatas[n].ScaleZ))
163 | }
164 | for n := range blockSplatCount {
165 | bts = append(bts, blockDatas[n].ColorR)
166 | }
167 | for n := range blockSplatCount {
168 | bts = append(bts, blockDatas[n].ColorG)
169 | }
170 | for n := range blockSplatCount {
171 | bts = append(bts, blockDatas[n].ColorB)
172 | }
173 | for n := range blockSplatCount {
174 | bts = append(bts, blockDatas[n].ColorA)
175 | }
176 | for n := range blockSplatCount {
177 | bts = append(bts, blockDatas[n].RotationW)
178 | }
179 | for n := range blockSplatCount {
180 | bts = append(bts, blockDatas[n].RotationX)
181 | }
182 | for n := range blockSplatCount {
183 | bts = append(bts, blockDatas[n].RotationY)
184 | }
185 | for n := range blockSplatCount {
186 | bts = append(bts, blockDatas[n].RotationZ)
187 | }
188 |
189 | if blockSplatCount >= MinGzipBlockSize {
190 | bts, err := cmn.GzipBytes(bts)
191 | cmn.ExitOnError(err)
192 | blockByteLength := -int32(len(bts))
193 | _, err = writer.Write(cmn.Int32ToBytes(blockByteLength))
194 | cmn.ExitOnError(err)
195 | _, err = writer.Write(bts)
196 | cmn.ExitOnError(err)
197 | } else {
198 | blockByteLength := int32(len(bts))
199 | _, err := writer.Write(cmn.Int32ToBytes(blockByteLength))
200 | cmn.ExitOnError(err)
201 | _, err = writer.Write(bts)
202 | cmn.ExitOnError(err)
203 | }
204 | }
205 |
206 | func writeSpxBlockSH1(writer *bufio.Writer, blockDatas []*SplatData) {
207 | blockSplatCount := len(blockDatas)
208 | bts := make([]byte, 0)
209 | bts = append(bts, cmn.Uint32ToBytes(uint32(blockSplatCount))...) // 块中的高斯点个数
210 | bts = append(bts, cmn.Uint32ToBytes(1)...) // 开放的块数据格式 1:sh1
211 |
212 | for n := range blockSplatCount {
213 | if len(blockDatas[n].SH1) > 0 {
214 | for i := range 9 {
215 | bts = append(bts, cmn.EncodeSpxSH(blockDatas[n].SH1[i]))
216 | }
217 | } else if len(blockDatas[n].SH2) > 0 {
218 | for i := range 9 {
219 | bts = append(bts, cmn.EncodeSpxSH(blockDatas[n].SH2[i]))
220 | }
221 | } else {
222 | for range 9 {
223 | bts = append(bts, cmn.EncodeSplatSH(0.0))
224 | }
225 | }
226 | }
227 |
228 | if blockSplatCount >= MinGzipBlockSize {
229 | bts, err := cmn.GzipBytes(bts)
230 | cmn.ExitOnError(err)
231 | blockByteLength := -int32(len(bts))
232 | _, err = writer.Write(cmn.Int32ToBytes(blockByteLength))
233 | cmn.ExitOnError(err)
234 | _, err = writer.Write(bts)
235 | cmn.ExitOnError(err)
236 | } else {
237 | blockByteLength := int32(len(bts))
238 | _, err := writer.Write(cmn.Int32ToBytes(blockByteLength))
239 | cmn.ExitOnError(err)
240 | _, err = writer.Write(bts)
241 | cmn.ExitOnError(err)
242 | }
243 | }
244 |
245 | func writeSpxBlockSH2(writer *bufio.Writer, blockDatas []*SplatData) {
246 | blockSplatCount := len(blockDatas)
247 | bts := make([]byte, 0)
248 | bts = append(bts, cmn.Uint32ToBytes(uint32(blockSplatCount))...) // 块中的高斯点个数
249 | bts = append(bts, cmn.Uint32ToBytes(2)...) // 开放的块数据格式 2:sh2
250 |
251 | for n := range blockSplatCount {
252 | if len(blockDatas[n].SH1) > 0 {
253 | for i := range 9 {
254 | bts = append(bts, cmn.EncodeSpxSH(blockDatas[n].SH1[i]))
255 | }
256 | for range 15 {
257 | bts = append(bts, cmn.EncodeSplatSH(0.0))
258 | }
259 | } else if len(blockDatas[n].SH2) > 0 {
260 | for i := range 24 {
261 | bts = append(bts, cmn.EncodeSpxSH(blockDatas[n].SH2[i]))
262 | }
263 | } else {
264 | for range 24 {
265 | bts = append(bts, cmn.EncodeSplatSH(0.0))
266 | }
267 | }
268 | }
269 |
270 | if blockSplatCount >= MinGzipBlockSize {
271 | bts, err := cmn.GzipBytes(bts)
272 | cmn.ExitOnError(err)
273 | blockByteLength := -int32(len(bts))
274 | _, err = writer.Write(cmn.Int32ToBytes(blockByteLength))
275 | cmn.ExitOnError(err)
276 | _, err = writer.Write(bts)
277 | cmn.ExitOnError(err)
278 | } else {
279 | blockByteLength := int32(len(bts))
280 | _, err := writer.Write(cmn.Int32ToBytes(blockByteLength))
281 | cmn.ExitOnError(err)
282 | _, err = writer.Write(bts)
283 | cmn.ExitOnError(err)
284 | }
285 | }
286 |
287 | func writeSpxBlockSH3(writer *bufio.Writer, blockDatas []*SplatData) {
288 | blockSplatCount := len(blockDatas)
289 | bts := make([]byte, 0)
290 | bts = append(bts, cmn.Uint32ToBytes(uint32(blockSplatCount))...) // 块中的高斯点个数
291 | bts = append(bts, cmn.Uint32ToBytes(3)...) // 开放的块数据格式 3:sh3
292 |
293 | for n := range blockSplatCount {
294 | if len(blockDatas[n].SH3) > 0 {
295 | for i := range 21 {
296 | bts = append(bts, cmn.EncodeSpxSH(blockDatas[n].SH3[i]))
297 | }
298 | } else {
299 | for range 21 {
300 | bts = append(bts, cmn.EncodeSplatSH(0.0))
301 | }
302 | }
303 | }
304 |
305 | if blockSplatCount >= MinGzipBlockSize {
306 | bts, err := cmn.GzipBytes(bts)
307 | cmn.ExitOnError(err)
308 | blockByteLength := -int32(len(bts))
309 | _, err = writer.Write(cmn.Int32ToBytes(blockByteLength))
310 | cmn.ExitOnError(err)
311 | _, err = writer.Write(bts)
312 | cmn.ExitOnError(err)
313 | } else {
314 | blockByteLength := int32(len(bts))
315 | _, err := writer.Write(cmn.Int32ToBytes(blockByteLength))
316 | cmn.ExitOnError(err)
317 | _, err = writer.Write(bts)
318 | cmn.ExitOnError(err)
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/gsplat/write-gs-spz.go:
--------------------------------------------------------------------------------
1 | package gsplat
2 |
3 | import (
4 | "bufio"
5 | "gsbox/cmn"
6 | "log"
7 | "os"
8 | )
9 |
10 | func WriteSpz(spzFile string, rows []*SplatData, shDegree int) {
11 | file, err := os.Create(spzFile)
12 | cmn.ExitOnError(err)
13 | defer file.Close()
14 |
15 | log.Println("[Info] output shDegree:", shDegree)
16 | writer := bufio.NewWriter(file)
17 |
18 | h := &SpzHeader{
19 | Magic: SPZ_MAGIC,
20 | Version: 2,
21 | NumPoints: uint32(len(rows)),
22 | ShDegree: uint8(shDegree),
23 | FractionalBits: 12,
24 | Flags: 0,
25 | Reserved: 0,
26 | }
27 |
28 | bts := make([]byte, 0)
29 | bts = append(bts, h.ToBytes()...)
30 |
31 | for i := range rows {
32 | bts = append(bts, cmn.SpzEncodePosition(rows[i].PositionX)...)
33 | bts = append(bts, cmn.SpzEncodePosition(rows[i].PositionY)...)
34 | bts = append(bts, cmn.SpzEncodePosition(rows[i].PositionZ)...)
35 | }
36 | for i := range rows {
37 | bts = append(bts, rows[i].ColorA)
38 | }
39 | for i := range rows {
40 | bts = append(bts, cmn.SpzEncodeColor(rows[i].ColorR), cmn.SpzEncodeColor(rows[i].ColorG), cmn.SpzEncodeColor(rows[i].ColorB))
41 | }
42 | for i := range rows {
43 | bts = append(bts, cmn.SpzEncodeScale(rows[i].ScaleX), cmn.SpzEncodeScale(rows[i].ScaleY), cmn.SpzEncodeScale(rows[i].ScaleZ))
44 | }
45 | for i := range rows {
46 | bts = append(bts, cmn.SpzEncodeRotations(rows[i].RotationW, rows[i].RotationX, rows[i].RotationY, rows[i].RotationZ)...)
47 | }
48 |
49 | if shDegree == 1 {
50 | for i := range rows {
51 | if len(rows[i].SH1) > 0 {
52 | for n := range 9 {
53 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH1[n]))
54 | }
55 | } else if len(rows[i].SH2) > 0 {
56 | for n := range 9 {
57 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH2[n]))
58 | }
59 | } else {
60 | for range 9 {
61 | bts = append(bts, cmn.EncodeSplatSH(0.0))
62 | }
63 | }
64 | }
65 | } else if shDegree == 2 {
66 | for i := range rows {
67 | if len(rows[i].SH1) > 0 {
68 | for n := range 9 {
69 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH1[n]))
70 | }
71 | for range 15 {
72 | bts = append(bts, cmn.EncodeSplatSH(0.0))
73 | }
74 | } else if len(rows[i].SH2) > 0 {
75 | for n := range 9 {
76 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH2[n]))
77 | }
78 | for j := 9; j < 24; j++ {
79 | bts = append(bts, cmn.SpzEncodeSH23(rows[i].SH2[j]))
80 | }
81 | } else {
82 | for range 24 {
83 | bts = append(bts, cmn.EncodeSplatSH(0.0))
84 | }
85 | }
86 | }
87 | } else if shDegree == 3 {
88 | for i := range rows {
89 | if len(rows[i].SH3) > 0 {
90 | for j := range 9 {
91 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH2[j]))
92 | }
93 | for j := 9; j < 24; j++ {
94 | bts = append(bts, cmn.SpzEncodeSH23(rows[i].SH2[j]))
95 | }
96 | for j := range 21 {
97 | bts = append(bts, cmn.SpzEncodeSH23(rows[i].SH3[j]))
98 | }
99 | } else if len(rows[i].SH2) > 0 {
100 | for j := range 9 {
101 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH2[j]))
102 | }
103 | for j := 9; j < 24; j++ {
104 | bts = append(bts, cmn.SpzEncodeSH23(rows[i].SH2[j]))
105 | }
106 | for range 21 {
107 | bts = append(bts, cmn.EncodeSplatSH(0.0))
108 | }
109 | } else if len(rows[i].SH1) > 0 {
110 | for n := range 9 {
111 | bts = append(bts, cmn.SpzEncodeSH1(rows[i].SH1[n]))
112 | }
113 | for range 36 {
114 | bts = append(bts, cmn.EncodeSplatSH(0.0))
115 | }
116 | } else {
117 | for range 45 {
118 | bts = append(bts, cmn.EncodeSplatSH(0.0))
119 | }
120 | }
121 | }
122 | }
123 |
124 | gzipDatas, err := cmn.GzipBytes(bts)
125 | cmn.ExitOnError(err)
126 |
127 | _, err = writer.Write(gzipDatas)
128 | cmn.ExitOnError(err)
129 |
130 | err = writer.Flush()
131 | cmn.ExitOnError(err)
132 | }
133 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "gsbox/cmn"
7 | "gsbox/gsplat"
8 | "log"
9 | "os"
10 | "time"
11 | )
12 |
13 | func main() {
14 | args := cmn.ParseArgs("-v", "-version", "--version", "-h", "-help", "--help")
15 | gsplat.Args = args
16 | if args.HasCmd("-v", "-version", "--version") && args.ArgCount == 2 {
17 | version()
18 | } else if args.HasCmd("-h", "-help", "--help") && args.ArgCount == 2 {
19 | usage()
20 | } else if args.HasCmd("p2s", "ply2splat") {
21 | ply2splat(args)
22 | } else if args.HasCmd("p2x", "ply2spx") {
23 | ply2spx(args)
24 | } else if args.HasCmd("p2z", "ply2spz") {
25 | ply2spz(args)
26 | } else if args.HasCmd("p2p", "ply2ply") {
27 | ply2ply(args)
28 | } else if args.HasCmd("s2p", "splat2ply") {
29 | splat2ply(args)
30 | } else if args.HasCmd("s2x", "splat2spx") {
31 | splat2spx(args)
32 | } else if args.HasCmd("s2z", "splat2spz") {
33 | splat2spz(args)
34 | } else if args.HasCmd("s2s", "splat2splat") {
35 | splat2splat(args)
36 | } else if args.HasCmd("x2p", "spx2ply") {
37 | spx2ply(args)
38 | } else if args.HasCmd("x2s", "spx2splat") {
39 | spx2splat(args)
40 | } else if args.HasCmd("x2z", "spx2spz") {
41 | spx2spz(args)
42 | } else if args.HasCmd("x2x", "spx2spx") {
43 | spx2spx(args)
44 | } else if args.HasCmd("z2p", "spz2ply") {
45 | spz2ply(args)
46 | } else if args.HasCmd("z2s", "spz2splat") {
47 | spz2splat(args)
48 | } else if args.HasCmd("z2x", "spz2spx") {
49 | spz2spx(args)
50 | } else if args.HasCmd("z2z", "spz2spz") {
51 | spz2spz(args)
52 | } else if args.HasCmd("join") {
53 | join(args)
54 | } else if args.HasCmd("info") {
55 | plyInfo(args)
56 | } else {
57 | usage()
58 | }
59 | fmt.Print(cmn.NewVersionMessage)
60 | }
61 |
62 | func version() {
63 | fmt.Println("")
64 | fmt.Println("gsbox", cmn.VER)
65 | fmt.Println("homepage", "https://github.com/gotoeasy/gsbox")
66 | }
67 | func usage() {
68 | fmt.Println("")
69 | fmt.Println("gsbox", cmn.VER)
70 | fmt.Println("homepage", "https://github.com/gotoeasy/gsbox")
71 | fmt.Println("")
72 | fmt.Println("")
73 | fmt.Println("Usage:")
74 | fmt.Println(" gsbox [options]")
75 | fmt.Println("")
76 | fmt.Println("Options:")
77 | fmt.Println(" p2s, ply2splat convert ply to splat")
78 | fmt.Println(" p2x, ply2spx convert ply to spx")
79 | fmt.Println(" p2z, ply2spz convert ply to spz")
80 | fmt.Println(" p2p, ply2ply convert ply to ply")
81 | fmt.Println(" s2p, splat2ply convert splat to ply")
82 | fmt.Println(" s2x, splat2spx convert splat to spx")
83 | fmt.Println(" s2z, splat2spz convert splat to spz")
84 | fmt.Println(" s2s, splat2splat convert splat to splat")
85 | fmt.Println(" x2p, spx2ply convert spx to ply")
86 | fmt.Println(" x2s, spx2splat convert spx to splat")
87 | fmt.Println(" x2z, spx2spz convert spx to spz")
88 | fmt.Println(" x2x, spx2spx convert spx to spx")
89 | fmt.Println(" z2p, spz2ply convert spz to ply")
90 | fmt.Println(" z2s, spz2splat convert spz to splat")
91 | fmt.Println(" z2x, spz2spx convert spz to spx")
92 | fmt.Println(" z2z, spz2spz convert spz to spz")
93 | fmt.Println(" join join the input model files into a single output file")
94 | fmt.Println(" info display the model file information")
95 | fmt.Println(" -i, --input specify the input file")
96 | fmt.Println(" -o, --output specify the output file")
97 | fmt.Println(" -c, --comment specify the comment for ply/spx output")
98 | fmt.Println(" -bs, --block-size specify the block size for spx output (default 20480)")
99 | fmt.Println(" -sh, --shDegree specify the SH degree for ply/spx/spz output")
100 | fmt.Println(" -f1, --flag1 specify the header flag1 for spx output")
101 | fmt.Println(" -f2, --flag2 specify the header flag2 for spx output")
102 | fmt.Println(" -f3, --flag3 specify the header flag3 for spx output")
103 | fmt.Println(" -rx, --rotateX specify the rotation angle in degrees about the x-axis for transform")
104 | fmt.Println(" -ry, --rotateY specify the rotation angle in degrees about the y-axis for transform")
105 | fmt.Println(" -rz, --rotateZ specify the rotation angle in degrees about the z-axis for transform")
106 | fmt.Println(" -s, --scale specify a uniform scaling factor(0.01~100.0) for transform")
107 | fmt.Println(" -tx, --translateX specify the translation value about the x-axis for transform")
108 | fmt.Println(" -ty, --translateY specify the translation value about the y-axis for transform")
109 | fmt.Println(" -tz, --translateZ specify the translation value about the z-axis for transform")
110 | fmt.Println(" -to, --transform-order specify the transform order (RST/RTS/SRT/STR/TRS/TSR), default is RST")
111 | fmt.Println(" -v, --version display version information")
112 | fmt.Println(" -h, --help display help information")
113 | fmt.Println("")
114 | fmt.Println("Examples:")
115 | fmt.Println(" gsbox ply2splat -i /path/to/input.ply -o /path/to/output.splat")
116 | fmt.Println(" gsbox s2x -i /path/to/input.splat -o /path/to/output.spx -c \"your comment\" -bs 10240")
117 | fmt.Println(" gsbox x2z -i /path/to/input.spx -o /path/to/output.spz -sh 0 -rz 90 -s 0.9 -tx 0.1 -to TRS")
118 | fmt.Println(" gsbox z2p -i /path/to/input.spz -o /path/to/output.ply -c \"your comment\"")
119 | fmt.Println(" gsbox join -i a.ply -i b.splat -i c.spx -i d.spz -o output.spx")
120 | fmt.Println(" gsbox info -i /path/to/file.spx")
121 | fmt.Println("")
122 | }
123 | func plyInfo(args *cmn.OsArgs) {
124 | // info
125 | input := args.GetArgIgnorecase("-i", "--input")
126 | if args.ArgCount == 3 {
127 | input = args.LastParam
128 | if cmn.EqualsIngoreCase(input, "info") {
129 | input = args.GetArgByIndex(1)
130 | }
131 | }
132 |
133 | if input == "" {
134 | cmn.ExitOnError(errors.New("please specify the input ply file"))
135 | }
136 | if !cmn.IsExistFile(input) {
137 | cmn.ExitOnError(errors.New("file not found: " + input))
138 | }
139 |
140 | isPly := cmn.Endwiths(input, ".ply", true)
141 | isSpx := cmn.Endwiths(input, ".spx", true)
142 | isSplat := cmn.Endwiths(input, ".splat", true)
143 | isSpz := cmn.Endwiths(input, ".spz", true)
144 |
145 | if isPly {
146 | header, err := gsplat.ReadPlyHeaderString(input, 1024)
147 | cmn.ExitOnError(err)
148 | fmt.Print(header)
149 | } else if isSpx {
150 | header := gsplat.ParseSpxHeader(input)
151 | fmt.Println(header.ToStringSpx())
152 | } else if isSpz {
153 | header, _ := gsplat.ReadSpz(input, true)
154 | fmt.Println(header.ToString())
155 | } else if isSplat {
156 | fileInfo, err := os.Stat(input)
157 | cmn.ExitOnError(err)
158 | fileSize := fileInfo.Size()
159 | if (fileSize)%32 > 0 {
160 | cmn.ExitOnError(errors.New("invalid splat format"))
161 | } else {
162 | fmt.Println("SplatCount :", fileSize/32)
163 | }
164 | } else {
165 | cmn.ExitOnError(errors.New("the input file must be (ply | splat | spx) format"))
166 | }
167 |
168 | }
169 |
170 | func join(args *cmn.OsArgs) {
171 | log.Println("[Info] join the input model files into a single output file")
172 | startTime := time.Now()
173 | inputs := checkInputFilesExists(args)
174 | output := createOutputDir(args)
175 | isOutPly := cmn.Endwiths(output, ".ply")
176 | isOutSplat := cmn.Endwiths(output, ".splat")
177 | isOutSpx := cmn.Endwiths(output, ".spx")
178 | isOutSpz := cmn.Endwiths(output, ".spz")
179 | cmn.ExitOnConditionError(!isOutPly && !isOutSplat && !isOutSpx && !isOutSpz, errors.New("output file must be (ply | splat | spx | spz) format"))
180 |
181 | datas := make([]*gsplat.SplatData, 0)
182 | maxFromShDegree := 0
183 | for _, file := range inputs {
184 | if cmn.Endwiths(file, ".ply") {
185 | header, ds := gsplat.ReadPly(file)
186 | maxFromShDegree = max(header.MaxShDegree(), maxFromShDegree)
187 | datas = append(datas, ds...)
188 | } else if cmn.Endwiths(file, ".splat") {
189 | ds := gsplat.ReadSplat(file)
190 | datas = append(datas, ds...)
191 | } else if cmn.Endwiths(file, ".spx") {
192 | header, ds := gsplat.ReadSpx(file)
193 | maxFromShDegree = max(int(header.ShDegree), maxFromShDegree)
194 | datas = append(datas, ds...)
195 | } else if cmn.Endwiths(file, ".spz") {
196 | header, ds := gsplat.ReadSpz(file, false)
197 | maxFromShDegree = max(int(header.ShDegree), maxFromShDegree)
198 | datas = append(datas, ds...)
199 | }
200 | }
201 | gsplat.TransformDatas(datas)
202 | gsplat.Sort(datas)
203 | shDegree := getArgShDegree(maxFromShDegree, args)
204 | if isOutPly {
205 | comment := args.GetArgIgnorecase("-c", "--comment")
206 | gsplat.WritePly(output, datas, comment, shDegree)
207 | } else if isOutSplat {
208 | gsplat.WriteSplat(output, datas)
209 | } else if isOutSpx {
210 | comment := args.GetArgIgnorecase("-c", "--comment")
211 | flag1 := getArgFlag(0, args, "-f1", "--flag1")
212 | flag2 := getArgFlag(0, args, "-f2", "--flag2")
213 | flag3 := getArgFlag(0, args, "-f3", "--flag3")
214 | gsplat.WriteSpx(output, datas, comment, shDegree, flag1, flag2, flag3)
215 | } else if isOutSpz {
216 | gsplat.WriteSpz(output, datas, shDegree)
217 | }
218 | log.Println("[Info]", inputs, " --> ", output)
219 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
220 | }
221 |
222 | func ply2splat(args *cmn.OsArgs) {
223 | log.Println("[Info] convert ply to splat.")
224 | startTime := time.Now()
225 | input := checkInputFileExists(args)
226 | output := createOutputDir(args)
227 | _, datas := gsplat.ReadPly(input, "ply-3dgs")
228 | gsplat.TransformDatas(datas)
229 | gsplat.Sort(datas)
230 | gsplat.WriteSplat(output, datas)
231 | log.Println("[Info]", input, " --> ", output)
232 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
233 | }
234 |
235 | func ply2spx(args *cmn.OsArgs) {
236 | log.Println("[Info] convert ply to spx.")
237 | startTime := time.Now()
238 | input := checkInputFileExists(args)
239 | output := createOutputDir(args)
240 | header, datas := gsplat.ReadPly(input, "ply-3dgs")
241 | gsplat.TransformDatas(datas)
242 | gsplat.Sort(datas)
243 | comment := args.GetArgIgnorecase("-c", "--comment")
244 | shDegree := getArgShDegree(header.MaxShDegree(), args)
245 | flag1 := getArgFlag(0, args, "-f1", "--flag1")
246 | flag2 := getArgFlag(0, args, "-f2", "--flag2")
247 | flag3 := getArgFlag(0, args, "-f3", "--flag3")
248 | gsplat.WriteSpx(output, datas, comment, shDegree, flag1, flag2, flag3)
249 | log.Println("[Info]", input, " --> ", output)
250 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
251 | }
252 |
253 | func ply2spz(args *cmn.OsArgs) {
254 | log.Println("[Info] convert ply to spz.")
255 | startTime := time.Now()
256 | input := checkInputFileExists(args)
257 | output := createOutputDir(args)
258 | header, datas := gsplat.ReadPly(input, "ply-3dgs")
259 | gsplat.TransformDatas(datas)
260 | gsplat.Sort(datas)
261 | shDegree := getArgShDegree(header.MaxShDegree(), args)
262 | gsplat.WriteSpz(output, datas, shDegree)
263 | log.Println("[Info]", input, " --> ", output)
264 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
265 | }
266 |
267 | func ply2ply(args *cmn.OsArgs) {
268 | log.Println("[Info] convert ply to ply.")
269 | startTime := time.Now()
270 | input := checkInputFileExists(args)
271 | output := createOutputDir(args)
272 | header, datas := gsplat.ReadPly(input, "ply-3dgs")
273 | gsplat.TransformDatas(datas)
274 | gsplat.Sort(datas)
275 | comment := args.GetArgIgnorecase("-c", "--comment")
276 | shDegree := getArgShDegree(header.MaxShDegree(), args)
277 | gsplat.WritePly(output, datas, comment, shDegree)
278 | log.Println("[Info]", input, " --> ", output)
279 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
280 | }
281 |
282 | func splat2ply(args *cmn.OsArgs) {
283 | log.Println("[Info] convert splat to ply.")
284 | startTime := time.Now()
285 | input := checkInputFileExists(args)
286 | output := createOutputDir(args)
287 | datas := gsplat.ReadSplat(input)
288 | gsplat.TransformDatas(datas)
289 | gsplat.Sort(datas)
290 | comment := args.GetArgIgnorecase("-c", "--comment")
291 | shDegree := getArgShDegree(0, args)
292 | gsplat.WritePly(output, datas, comment, shDegree)
293 | log.Println("[Info]", input, " --> ", output)
294 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
295 | }
296 | func splat2spx(args *cmn.OsArgs) {
297 | log.Println("[Info] convert splat to spx.")
298 | startTime := time.Now()
299 | input := checkInputFileExists(args)
300 | output := createOutputDir(args)
301 | datas := gsplat.ReadSplat(input)
302 | gsplat.TransformDatas(datas)
303 | gsplat.Sort(datas)
304 | comment := args.GetArgIgnorecase("-c", "--comment")
305 | shDegree := getArgShDegree(0, args)
306 | flag1 := getArgFlag(0, args, "-f1", "--flag1")
307 | flag2 := getArgFlag(0, args, "-f2", "--flag2")
308 | flag3 := getArgFlag(0, args, "-f3", "--flag3")
309 | gsplat.WriteSpx(output, datas, comment, shDegree, flag1, flag2, flag3)
310 | log.Println("[Info]", input, " --> ", output)
311 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
312 | }
313 | func splat2spz(args *cmn.OsArgs) {
314 | log.Println("[Info] convert splat to spz.")
315 | startTime := time.Now()
316 | input := checkInputFileExists(args)
317 | output := createOutputDir(args)
318 | datas := gsplat.ReadSplat(input)
319 | gsplat.TransformDatas(datas)
320 | gsplat.Sort(datas)
321 | gsplat.WriteSpz(output, datas, 0)
322 | log.Println("[Info]", input, " --> ", output)
323 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
324 | }
325 | func splat2splat(args *cmn.OsArgs) {
326 | log.Println("[Info] convert splat to splat.")
327 | startTime := time.Now()
328 | input := checkInputFileExists(args)
329 | output := createOutputDir(args)
330 | datas := gsplat.ReadSplat(input)
331 | gsplat.TransformDatas(datas)
332 | gsplat.Sort(datas)
333 | gsplat.WriteSplat(output, datas)
334 | log.Println("[Info]", input, " --> ", output)
335 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
336 | }
337 |
338 | func spx2ply(args *cmn.OsArgs) {
339 | log.Println("[Info] convert spx to ply.")
340 | startTime := time.Now()
341 | input := checkInputFileExists(args)
342 | output := createOutputDir(args)
343 | header, datas := gsplat.ReadSpx(input)
344 | gsplat.TransformDatas(datas)
345 | gsplat.Sort(datas)
346 | comment := args.GetArgIgnorecase("-c", "--comment")
347 | shDegree := getArgShDegree(int(header.ShDegree), args)
348 | gsplat.WritePly(output, datas, comment, shDegree)
349 | log.Println("[Info]", input, " --> ", output)
350 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
351 | }
352 | func spx2splat(args *cmn.OsArgs) {
353 | log.Println("[Info] convert spx to splat.")
354 | startTime := time.Now()
355 | input := checkInputFileExists(args)
356 | output := createOutputDir(args)
357 | _, datas := gsplat.ReadSpx(input)
358 | gsplat.TransformDatas(datas)
359 | gsplat.Sort(datas)
360 | gsplat.WriteSplat(output, datas)
361 | log.Println("[Info]", input, " --> ", output)
362 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
363 | }
364 | func spx2spz(args *cmn.OsArgs) {
365 | log.Println("[Info] convert spx to spz.")
366 | startTime := time.Now()
367 | input := checkInputFileExists(args)
368 | output := createOutputDir(args)
369 | header, datas := gsplat.ReadSpx(input)
370 | gsplat.TransformDatas(datas)
371 | gsplat.Sort(datas)
372 | shDegree := getArgShDegree(int(header.ShDegree), args)
373 | gsplat.WriteSpz(output, datas, shDegree)
374 | log.Println("[Info]", input, " --> ", output)
375 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
376 | }
377 | func spx2spx(args *cmn.OsArgs) {
378 | log.Println("[Info] convert spx to spx.")
379 | startTime := time.Now()
380 | input := checkInputFileExists(args)
381 | output := createOutputDir(args)
382 | header, datas := gsplat.ReadSpx(input)
383 | gsplat.TransformDatas(datas)
384 | gsplat.Sort(datas)
385 | comment := args.GetArgIgnorecase("-c", "--comment")
386 | shDegree := getArgShDegree(int(header.ShDegree), args)
387 | flag1 := getArgFlag(0, args, "-f1", "--flag1")
388 | flag2 := getArgFlag(0, args, "-f2", "--flag2")
389 | flag3 := getArgFlag(0, args, "-f3", "--flag3")
390 | gsplat.WriteSpx(output, datas, comment, shDegree, flag1, flag2, flag3)
391 | log.Println("[Info]", input, " --> ", output)
392 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
393 | }
394 |
395 | func spz2ply(args *cmn.OsArgs) {
396 | log.Println("[Info] convert spz to ply.")
397 | startTime := time.Now()
398 | input := checkInputFileExists(args)
399 | output := createOutputDir(args)
400 | header, datas := gsplat.ReadSpz(input, false)
401 | gsplat.TransformDatas(datas)
402 | gsplat.Sort(datas)
403 | comment := args.GetArgIgnorecase("-c", "--comment")
404 | shDegree := getArgShDegree(int(header.ShDegree), args)
405 | gsplat.WritePly(output, datas, comment, shDegree)
406 | log.Println("[Info]", input, " --> ", output)
407 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
408 | }
409 |
410 | func spz2splat(args *cmn.OsArgs) {
411 | log.Println("[Info] convert spz to splat.")
412 | startTime := time.Now()
413 | input := checkInputFileExists(args)
414 | output := createOutputDir(args)
415 | _, datas := gsplat.ReadSpz(input, false)
416 | gsplat.TransformDatas(datas)
417 | gsplat.Sort(datas)
418 | gsplat.WriteSplat(output, datas)
419 | log.Println("[Info]", input, " --> ", output)
420 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
421 | }
422 |
423 | func spz2spx(args *cmn.OsArgs) {
424 | log.Println("[Info] convert spz to spx.")
425 | startTime := time.Now()
426 | input := checkInputFileExists(args)
427 | output := createOutputDir(args)
428 | header, datas := gsplat.ReadSpz(input, false)
429 | gsplat.TransformDatas(datas)
430 | gsplat.Sort(datas)
431 | comment := args.GetArgIgnorecase("-c", "--comment")
432 | shDegree := getArgShDegree(int(header.ShDegree), args)
433 | flag1 := getArgFlag(0, args, "-f1", "--flag1")
434 | flag2 := getArgFlag(0, args, "-f2", "--flag2")
435 | flag3 := getArgFlag(0, args, "-f3", "--flag3")
436 | gsplat.WriteSpx(output, datas, comment, shDegree, flag1, flag2, flag3)
437 | log.Println("[Info]", input, " --> ", output)
438 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
439 | }
440 | func spz2spz(args *cmn.OsArgs) {
441 | log.Println("[Info] convert spz to spz.")
442 | startTime := time.Now()
443 | input := checkInputFileExists(args)
444 | output := createOutputDir(args)
445 | header, datas := gsplat.ReadSpz(input, false)
446 | gsplat.TransformDatas(datas)
447 | gsplat.Sort(datas)
448 | shDegree := getArgShDegree(int(header.ShDegree), args)
449 | gsplat.WriteSpz(output, datas, shDegree)
450 | log.Println("[Info]", input, " --> ", output)
451 | log.Println("[Info] processing time conversion:", cmn.GetTimeInfo(time.Since(startTime).Milliseconds()))
452 | }
453 |
454 | func checkInputFilesExists(args *cmn.OsArgs) []string {
455 | inputs := args.GetArgsIgnorecase("-i", "--input")
456 | for i := 0; i < len(inputs); i++ {
457 | cmn.ExitOnConditionError(inputs[i] == "", errors.New(`please specify the input file`))
458 | cmn.ExitOnConditionError(!cmn.IsExistFile(inputs[i]), errors.New("file not found: "+inputs[i]))
459 | }
460 | return inputs
461 | }
462 |
463 | func checkInputFileExists(args *cmn.OsArgs) string {
464 | input := args.GetArgIgnorecase("-i", "--input")
465 | cmn.ExitOnConditionError(input == "", errors.New(`please specify the input file`))
466 | cmn.ExitOnConditionError(!cmn.IsExistFile(input), errors.New("file not found: "+input))
467 | return input
468 | }
469 |
470 | func createOutputDir(args *cmn.OsArgs) string {
471 | output := args.GetArgIgnorecase("-o", "--output")
472 | cmn.ExitOnConditionError(output == "", errors.New(`please specify the output file`))
473 | cmn.ExitOnError(cmn.MkdirAll(cmn.Dir(output)))
474 | return output
475 | }
476 |
477 | func getArgShDegree(dataShDegree int, args *cmn.OsArgs) int {
478 | shDegree := dataShDegree
479 | if args.HasArg("-sh", "--shDegree") {
480 | sh := cmn.StringToInt(args.GetArgIgnorecase("-sh", "--shDegree"), -1)
481 | if sh >= 0 && sh <= 3 {
482 | shDegree = sh
483 | }
484 | }
485 | return shDegree
486 | }
487 |
488 | func getArgFlag(defaultFlag uint8, args *cmn.OsArgs, arg1 string, arg2 string) uint8 {
489 | flag := defaultFlag
490 | if args.HasArg(arg1, arg2) {
491 | val := cmn.StringToInt(args.GetArgIgnorecase(arg1, arg2), -1)
492 | if val >= 0 && val < 256 {
493 | flag = uint8(val)
494 | }
495 | }
496 | return flag
497 | }
498 |
--------------------------------------------------------------------------------