mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
13dbacad15
commit
f71000befd
45 files changed
+4383
-422
No files matched your search
@@ -7,9 +7,9 @@ require (
|
|||||||
github.com/emmansun/gmsm v0.45.0
|
github.com/emmansun/gmsm v0.45.0
|
||||||
github.com/go-text/typesetting v0.3.5
|
github.com/go-text/typesetting v0.3.5
|
||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4
|
github.com/xiaoqidun/jbig2 v1.1.4
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7
|
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f
|
||||||
golang.org/x/crypto v0.58.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/text v0.43.0
|
golang.org/x/text v0.43.0
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
|
|||||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893 h1:jmQlBO7R+bcimOMs9jjyDWR1FqiPNGtZqA/V1WiYfPA=
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e h1:OuRcvG5g5AieBN3fNKo130Iu3f+6/ihiWSe8//QT1qg=
|
||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7 h1:VFrEkWzhLLgoMZ4FNH/uN/Mdfo/TSGKzIPjqCL4m3J4=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f h1:m3yM1CzrHWm+LW84WWcA2Kr1Ux8K/Znrgd0XVxGEtIw=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7/go.mod h1:fXz1emtF1YCpcIv+fglDZstjkHxjS2l3G4DVMgUzVnk=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+37
-6
@@ -151,6 +151,30 @@ func (u *editorFontUsage) merge(other *editorFontUsage, unsafe bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorObjectFontScan 判断对象是否需要扫描嵌套文字或复合资源
|
||||||
|
// 入参: object 图形对象
|
||||||
|
// 返回: bool 是否需要完整引用扫描
|
||||||
|
func editorObjectFontScan(object GraphicObject) bool {
|
||||||
|
var clips *Clips
|
||||||
|
var fill, stroke *FillColor
|
||||||
|
switch object.Type {
|
||||||
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
|
return true
|
||||||
|
case "TextObject", "Text":
|
||||||
|
clips = object.TextObject.Clips
|
||||||
|
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
||||||
|
case "PathObject", "Path":
|
||||||
|
clips = object.PathObject.Clips
|
||||||
|
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
||||||
|
case "ImageObject":
|
||||||
|
clips = object.ImageObject.Clips
|
||||||
|
if object.ImageObject.Border != nil {
|
||||||
|
stroke = (*FillColor)(object.ImageObject.Border.BorderColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clips != nil || fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil
|
||||||
|
}
|
||||||
|
|
||||||
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
|
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
|
||||||
// 入参: progress 保存进度回调
|
// 入参: progress 保存进度回调
|
||||||
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||||
@@ -181,11 +205,17 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
return nil, progress.report("fonts", 0, 0)
|
return nil, progress.report("fonts", 0, 0)
|
||||||
}
|
}
|
||||||
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
||||||
composite := false
|
nested := false
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.draw != nil {
|
||||||
|
nested = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
for _, name := range e.annotationFiles() {
|
for _, name := range e.annotationFiles() {
|
||||||
if data, changed := e.source.reader.files[name]; changed {
|
if data, changed := e.source.reader.files[name]; changed {
|
||||||
composite = true
|
nested = true
|
||||||
if _, safe := refs.scan(bytes.NewReader(data), name); !safe {
|
if _, safe := refs.scan(bytes.NewReader(data), name); !safe {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -199,8 +229,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
if editorObjectFontScan(object) {
|
||||||
composite = true
|
nested = true
|
||||||
origin := e.objectOrigin(editorObjectID(object))
|
origin := e.objectOrigin(editorObjectID(object))
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -218,6 +248,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe {
|
if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
continue
|
continue
|
||||||
@@ -246,10 +277,10 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if composite {
|
if nested {
|
||||||
_, _, _, files := e.usedResources()
|
_, _, _, files := e.usedResources()
|
||||||
for i, resource := range e.resources {
|
for i, resource := range e.resources {
|
||||||
if resource.composite == "" || !slices.Contains(files, resource.name) {
|
if resource.definition() == "" || !slices.Contains(files, resource.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
||||||
|
|||||||
+23
-1
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -42,10 +43,26 @@ type editorXMLPatch struct {
|
|||||||
// 入参: data XML原文
|
// 入参: data XML原文
|
||||||
// 返回: *editorXML 根节点, error 错误信息
|
// 返回: *editorXML 根节点, error 错误信息
|
||||||
func parseEditorXML(data []byte) (*editorXML, error) {
|
func parseEditorXML(data []byte) (*editorXML, error) {
|
||||||
d := xml.NewDecoder(bytes.NewReader(data))
|
return parseEditorXMLContext(nil, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEditorXMLContext 解析XML节点位置并响应取消
|
||||||
|
// 入参: ctx 取消上下文,可为nil, data XML原文
|
||||||
|
// 返回: *editorXML 根节点, error 解析或取消错误
|
||||||
|
func parseEditorXMLContext(ctx context.Context, data []byte) (*editorXML, error) {
|
||||||
|
var input io.Reader = bytes.NewReader(data)
|
||||||
|
if ctx != nil {
|
||||||
|
input = imageInput{ReadCloser: io.NopCloser(input), context: ctx}
|
||||||
|
}
|
||||||
|
d := xml.NewDecoder(input)
|
||||||
var root *editorXML
|
var root *editorXML
|
||||||
var stack []*editorXML
|
var stack []*editorXML
|
||||||
for {
|
for {
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
start := int(d.InputOffset())
|
start := int(d.InputOffset())
|
||||||
token, err := d.Token()
|
token, err := d.Token()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -131,7 +148,12 @@ func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
|||||||
}
|
}
|
||||||
return a.end - b.end
|
return a.end - b.end
|
||||||
})
|
})
|
||||||
|
size := len(data)
|
||||||
|
for _, patch := range patches {
|
||||||
|
size += len(patch.data) - (patch.end - patch.start)
|
||||||
|
}
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
|
result.Grow(size)
|
||||||
position := 0
|
position := 0
|
||||||
for _, patch := range patches {
|
for _, patch := range patches {
|
||||||
result.Write(data[position:patch.start])
|
result.Write(data[position:patch.start])
|
||||||
|
|||||||
+10
-1
@@ -86,8 +86,17 @@ type FontShaperOptions interface {
|
|||||||
ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error)
|
ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FontShaperRun 保留完整单行原文的上下文,仅塑形指定区间,不执行字体回退
|
||||||
|
// start和end为原文的符文索引,区间左闭右开,返回Cluster仍使用原文索引
|
||||||
|
// 调用方负责方向和脚本分段,options.Bidi须为false;坐标相对当前区间的基线起点
|
||||||
|
// value为已校验的原文符文序列,调用期间不得修改,返回结果不引用该切片
|
||||||
|
type FontShaperRun interface {
|
||||||
|
FontShaperOptions
|
||||||
|
ShapeTextRun(value []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error)
|
||||||
|
}
|
||||||
|
|
||||||
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位
|
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位
|
||||||
// Bidi模式下VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序
|
// VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序
|
||||||
type ShapedGlyph struct {
|
type ShapedGlyph struct {
|
||||||
Glyph uint16
|
Glyph uint16
|
||||||
Cluster int
|
Cluster int
|
||||||
|
|||||||
+3
-109
@@ -18,7 +18,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/go-text/typesetting/font/cff"
|
"github.com/go-text/typesetting/font/cff"
|
||||||
@@ -172,10 +171,11 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
names := encodeCFFIndex([][]byte{[]byte(name)})
|
names := encodeCFFIndex([][]byte{[]byte(name)})
|
||||||
strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")})
|
strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")})
|
||||||
charset := []byte{0}
|
cids := make([]int, len(chars))
|
||||||
for gid := 1; gid < len(chars); gid++ {
|
for gid := 1; gid < len(chars); gid++ {
|
||||||
charset = binary.BigEndian.AppendUint16(charset, uint16(gid))
|
cids[gid] = gid
|
||||||
}
|
}
|
||||||
|
charset := encodeCFFSubsetCharset(cids)
|
||||||
selects := []byte{3, 0, 1, 0, 0, 0}
|
selects := []byte{3, 0, 1, 0, 0, 0}
|
||||||
selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars)))
|
selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars)))
|
||||||
fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})})
|
fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})})
|
||||||
@@ -203,109 +203,3 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("CFF offsets did not converge")
|
return nil, fmt.Errorf("CFF offsets did not converge")
|
||||||
}
|
}
|
||||||
|
|
||||||
// staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略
|
|
||||||
// 入参: sfnt 已解析字体
|
|
||||||
// 返回: [][]byte 独立字形程序,不符合条件时为空
|
|
||||||
func staticCFFPrograms(sfnt *sfntFont) [][]byte {
|
|
||||||
data := sfnt.Tables["CFF "]
|
|
||||||
if len(data) < 4 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, end, err := readType2Index(data, int(data[2]))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tops, end, err := readType2Index(data, end)
|
|
||||||
if err != nil || len(tops) != 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, end, err = readType2Index(data, end)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
globals, _, err := readType2Index(data, end)
|
|
||||||
if err != nil || len(globals) != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dict, err := readCFFDict(tops[0])
|
|
||||||
if err != nil || len(dict[1230]) != 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
matrix, err := readCFFMatrix(dict, [6]float64{.001, 0, 0, .001, 0, 0})
|
|
||||||
if err != nil || matrix[0] != matrix[3] || matrix[1] != 0 || matrix[2] != 0 || matrix[4] != 0 || matrix[5] != 0 || math.Abs(matrix[0]*float64(sfnt.UnitsPerEm())-1) > 1e-6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
offset, err := cffDictOffset(data, dict, 1236)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fds, _, err := readType2Index(data, offset)
|
|
||||||
if err != nil || len(fds) != 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fd, err := readCFFDict(fds[0])
|
|
||||||
if err != nil || len(fd) != 1 || len(fd[18]) != 2 || fd[18][0] != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
offset, err = cffDictOffset(data, dict, 17)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
chars, _, err := readType2Index(data, offset)
|
|
||||||
if err != nil || len(chars) != int(sfnt.NumGlyphs()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return chars
|
|
||||||
}
|
|
||||||
|
|
||||||
// subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量
|
|
||||||
// 入参: sfnt 已解析字体, glyphs 使用的字形
|
|
||||||
// 返回: []byte 字体子集, error 裁剪错误
|
|
||||||
func subsetStaticCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
|
||||||
chars := staticCFFPrograms(sfnt)
|
|
||||||
if chars == nil {
|
|
||||||
return nil, fmt.Errorf("CFF font cannot be subset safely")
|
|
||||||
}
|
|
||||||
keep := make(map[uint16]bool, len(glyphs)+1)
|
|
||||||
keep[0] = true
|
|
||||||
for _, gid := range glyphs {
|
|
||||||
if int(gid) >= len(chars) {
|
|
||||||
return nil, fmt.Errorf("CFF glyph index out of range")
|
|
||||||
}
|
|
||||||
keep[gid] = true
|
|
||||||
}
|
|
||||||
empty := make(map[uint16][]byte)
|
|
||||||
for gid := range chars {
|
|
||||||
if !keep[uint16(gid)] {
|
|
||||||
width := sfnt.GlyphAdvance(uint16(gid))
|
|
||||||
program := empty[width]
|
|
||||||
if program == nil {
|
|
||||||
var err error
|
|
||||||
program, err = cffOutlineProgram(nil, width)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
empty[width] = program
|
|
||||||
}
|
|
||||||
chars[gid] = program
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table := maps.Clone(sfnt.Tables)
|
|
||||||
var err error
|
|
||||||
table["CFF "], err = staticCIDFont(table, chars)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mapping := make(map[rune]uint16)
|
|
||||||
for gid := range keep {
|
|
||||||
for _, char := range sfnt.glyphCharacters(gid) {
|
|
||||||
mapping[char] = gid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping)
|
|
||||||
for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} {
|
|
||||||
delete(table, tag)
|
|
||||||
}
|
|
||||||
return serializeOTF(table)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cidSubsetFont 保存CID字体裁剪所需的字典、映射和执行资源
|
||||||
|
type cidSubsetFont struct {
|
||||||
|
prefix []byte
|
||||||
|
strings [][]byte
|
||||||
|
top cffDict
|
||||||
|
dicts []cffDict
|
||||||
|
private []cffDict
|
||||||
|
fonts []*type2Font
|
||||||
|
selects []int
|
||||||
|
charset []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// readCIDSubsetFont 读取CID字体的原始字典及逐字形归属,不合并字体字典
|
||||||
|
// 入参: data CFF数据
|
||||||
|
// 返回: *cidSubsetFont 裁剪资源, error 结构错误或不支持的字典
|
||||||
|
func readCIDSubsetFont(data []byte) (*cidSubsetFont, error) {
|
||||||
|
if !isBareCFFData(data) {
|
||||||
|
return nil, fmt.Errorf("invalid CFF header")
|
||||||
|
}
|
||||||
|
names, end, err := readType2Index(data, int(data[2]))
|
||||||
|
if err != nil || len(names) != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF name index")
|
||||||
|
}
|
||||||
|
tops, end, err := readType2Index(data, end)
|
||||||
|
if err != nil || len(tops) != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF top index")
|
||||||
|
}
|
||||||
|
strings, end, err := readType2Index(data, end)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
globals, _, err := readType2Index(data, end)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
top, err := readCFFDict(tops[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ros := top[1230]
|
||||||
|
if len(ros) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF ROS")
|
||||||
|
}
|
||||||
|
for index, value := range ros {
|
||||||
|
limit := float64(math.MaxInt32)
|
||||||
|
if index < 2 {
|
||||||
|
limit = float64(min(64999, 390+len(strings)))
|
||||||
|
}
|
||||||
|
if !finite(value) || value < 0 || value > limit || value != math.Trunc(value) {
|
||||||
|
return nil, fmt.Errorf("invalid CFF ROS operand")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for op := range top {
|
||||||
|
switch op {
|
||||||
|
case 0, 1, 2, 3, 4, 5, 13, 14, 15, 17, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1230, 1231, 1232, 1233, 1234, 1236, 1237:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported CID CFF top operator %d", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok := top[1206]; ok && (len(value) != 1 || value[0] != 2) {
|
||||||
|
return nil, fmt.Errorf("unsupported CFF charstring type")
|
||||||
|
}
|
||||||
|
if _, err := readCFFMatrix(top, [6]float64{.001, 0, 0, .001, 0, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
charOffset, err := cffDictOffset(data, top, 17)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chars, _, err := readType2Index(data, charOffset)
|
||||||
|
if err != nil || len(chars) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF charstrings")
|
||||||
|
}
|
||||||
|
charsetOffset, err := cffDictOffset(data, top, 15)
|
||||||
|
if err != nil || charsetOffset <= 2 {
|
||||||
|
return nil, fmt.Errorf("invalid CID CFF charset offset")
|
||||||
|
}
|
||||||
|
cids, err := parseCFFCharset(data, charsetOffset, len(chars))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fdOffset, err := cffDictOffset(data, top, 1236)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fds, _, err := readType2Index(data, fdOffset)
|
||||||
|
if err != nil || len(fds) == 0 || len(fds) > 256 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF font dictionary count")
|
||||||
|
}
|
||||||
|
selectOffset, err := cffDictOffset(data, top, 1237)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
selects, err := readType2FDSelect(data, selectOffset, len(chars), len(fds))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data[selectOffset] == 3 && binary.BigEndian.Uint16(data[selectOffset+3:]) != 0 {
|
||||||
|
return nil, fmt.Errorf("CFF FDSelect must include glyph zero")
|
||||||
|
}
|
||||||
|
font := &cidSubsetFont{
|
||||||
|
prefix: append(bytes.Clone(data[:int(data[2])]), encodeCFFIndex(names)...),
|
||||||
|
strings: strings, top: top, selects: selects, charset: encodeCFFSubsetCharset(cids),
|
||||||
|
dicts: make([]cffDict, len(fds)), private: make([]cffDict, len(fds)), fonts: make([]*type2Font, len(fds)),
|
||||||
|
}
|
||||||
|
for index, fd := range fds {
|
||||||
|
dict, err := readCFFDict(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for op := range dict {
|
||||||
|
switch op {
|
||||||
|
case 18, 1207, 1238:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported CFF font dictionary operator %d", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := readCFFMatrix(dict, [6]float64{1, 0, 0, 1, 0, 0}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(dict[18]) != 2 {
|
||||||
|
return nil, fmt.Errorf("missing CFF private dictionary")
|
||||||
|
}
|
||||||
|
resource := &type2Font{data: data, chars: chars, globals: globals, seed: 1, cid: true}
|
||||||
|
private, err := readType2Private(data, dict[18], resource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := encodeCFFSubsetPrivate(private); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
font.dicts[index], font.private[index], font.fonts[index] = dict, private, resource
|
||||||
|
}
|
||||||
|
return font, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write 重建CID字体偏移,保留CID映射、字体矩阵及各字典的私有参数
|
||||||
|
// 入参: chars 裁剪后的字形程序
|
||||||
|
// 返回: []byte CFF数据, error 偏移或字典错误
|
||||||
|
func (f *cidSubsetFont) write(chars [][]byte) ([]byte, error) {
|
||||||
|
suffix := append(encodeCFFIndex(f.strings), encodeCFFIndex(f.fonts[0].globals)...)
|
||||||
|
selects := encodeCFFSubsetFDSelect(f.selects)
|
||||||
|
programs := encodeCFFIndex(chars)
|
||||||
|
private := make([][]byte, len(f.fonts))
|
||||||
|
locals := make([][]byte, len(f.fonts))
|
||||||
|
fds := make([][]byte, len(f.fonts))
|
||||||
|
for index, font := range f.fonts {
|
||||||
|
var err error
|
||||||
|
private[index], err = encodeCFFSubsetPrivate(f.private[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := f.private[index][19]; ok {
|
||||||
|
locals[index] = encodeCFFIndex(font.locals)
|
||||||
|
}
|
||||||
|
fds[index] = encodeCFFDict(f.dicts[index])
|
||||||
|
}
|
||||||
|
for range 16 {
|
||||||
|
top := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
|
||||||
|
array := encodeCFFIndex(fds)
|
||||||
|
offset := len(f.prefix) + len(top) + len(suffix)
|
||||||
|
f.top[15] = []float64{float64(offset)}
|
||||||
|
offset += len(f.charset)
|
||||||
|
f.top[1237] = []float64{float64(offset)}
|
||||||
|
offset += len(selects)
|
||||||
|
f.top[17] = []float64{float64(offset)}
|
||||||
|
offset += len(programs)
|
||||||
|
f.top[1236] = []float64{float64(offset)}
|
||||||
|
offset += len(array)
|
||||||
|
for index := range f.fonts {
|
||||||
|
f.dicts[index][18] = []float64{float64(len(private[index])), float64(offset)}
|
||||||
|
fds[index] = encodeCFFDict(f.dicts[index])
|
||||||
|
offset += len(private[index]) + len(locals[index])
|
||||||
|
}
|
||||||
|
updatedTop := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
|
||||||
|
updatedArray := encodeCFFIndex(fds)
|
||||||
|
if len(updatedTop) != len(top) || len(updatedArray) != len(array) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := make([]byte, 0, offset)
|
||||||
|
for _, part := range [][]byte{f.prefix, updatedTop, suffix, f.charset, selects, programs, updatedArray} {
|
||||||
|
result = append(result, part...)
|
||||||
|
}
|
||||||
|
for index := range f.fonts {
|
||||||
|
result = append(result, private[index]...)
|
||||||
|
result = append(result, locals[index]...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CID CFF offsets did not converge")
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCFFSubsetFDSelect 选择较短的逐字形或分段格式保存字体字典归属
|
||||||
|
// 入参: selects 字形所属字典
|
||||||
|
// 返回: []byte FDSelect数据
|
||||||
|
func encodeCFFSubsetFDSelect(selects []int) []byte {
|
||||||
|
ranges := []byte{3, 0, 0}
|
||||||
|
count := 0
|
||||||
|
for gid, fd := range selects {
|
||||||
|
if gid == 0 || fd != selects[gid-1] {
|
||||||
|
ranges = binary.BigEndian.AppendUint16(ranges, uint16(gid))
|
||||||
|
ranges = append(ranges, byte(fd))
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(ranges[1:], uint16(count))
|
||||||
|
ranges = binary.BigEndian.AppendUint16(ranges, uint16(len(selects)))
|
||||||
|
if len(ranges) < len(selects)+1 {
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
result := make([]byte, len(selects)+1)
|
||||||
|
for gid, fd := range selects {
|
||||||
|
result[gid+1] = byte(fd)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// compactCFFGlyphs 重排已裁剪字体的字形,同步字符集、编码和字体字典归属
|
||||||
|
// 入参: data 已裁剪的CFF数据, keep 保留的原字形编号
|
||||||
|
// 返回: []byte 紧凑CFF数据, []uint16 新编号对应的原编号, error 结构或编码错误
|
||||||
|
func compactCFFGlyphs(data []byte, keep map[uint16]bool) ([]byte, []uint16, error) {
|
||||||
|
ids := []uint16{0}
|
||||||
|
for gid, used := range keep {
|
||||||
|
if used && gid != 0 {
|
||||||
|
ids = append(ids, gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(ids)
|
||||||
|
font, err := readType2Font(data)
|
||||||
|
if err == nil {
|
||||||
|
sids, _, _, _, ok := getCFFCharsetInfo(data, len(font.chars))
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("invalid CFF charset")
|
||||||
|
}
|
||||||
|
_, end, _ := readType2Index(data, int(data[2]))
|
||||||
|
tops, _, _ := readType2Index(data, end)
|
||||||
|
dict, _ := readCFFDict(tops[0])
|
||||||
|
encoding, err := cffSubsetEncoding(data, dict)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if encoding != nil {
|
||||||
|
codes, err := readCFFSubsetEncoding(encoding, sids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ordered := []uint16{0}
|
||||||
|
seen := map[uint16]bool{0: true}
|
||||||
|
for _, gid := range codes {
|
||||||
|
if keep[gid] && !seen[gid] {
|
||||||
|
ordered = append(ordered, gid)
|
||||||
|
seen[gid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, gid := range ids {
|
||||||
|
if !seen[gid] {
|
||||||
|
ordered = append(ordered, gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids = ordered
|
||||||
|
}
|
||||||
|
chars := make([][]byte, len(ids))
|
||||||
|
for index, gid := range ids {
|
||||||
|
if int(gid) >= len(font.chars) {
|
||||||
|
return nil, nil, fmt.Errorf("CFF glyph index out of range")
|
||||||
|
}
|
||||||
|
chars[index] = font.chars[gid]
|
||||||
|
}
|
||||||
|
result, err := rebuildNamedCFF(data, font, chars, ids)
|
||||||
|
return result, ids, err
|
||||||
|
}
|
||||||
|
cid, err := readCIDSubsetFont(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
cids, _, _, _, ok := getCFFCharsetInfo(data, len(cid.selects))
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("invalid CID CFF charset")
|
||||||
|
}
|
||||||
|
chars := make([][]byte, len(ids))
|
||||||
|
selected := make([]int, len(ids))
|
||||||
|
selects := make([]int, len(ids))
|
||||||
|
dicts, private, fonts := []cffDict{}, []cffDict{}, []*type2Font{}
|
||||||
|
remap := make(map[int]int)
|
||||||
|
for index, gid := range ids {
|
||||||
|
if int(gid) >= len(cid.selects) {
|
||||||
|
return nil, nil, fmt.Errorf("CFF glyph index out of range")
|
||||||
|
}
|
||||||
|
fd := cid.selects[gid]
|
||||||
|
mapped, exists := remap[fd]
|
||||||
|
if !exists {
|
||||||
|
mapped = len(fonts)
|
||||||
|
remap[fd] = mapped
|
||||||
|
dicts = append(dicts, cid.dicts[fd])
|
||||||
|
private = append(private, cid.private[fd])
|
||||||
|
fonts = append(fonts, cid.fonts[fd])
|
||||||
|
}
|
||||||
|
chars[index], selected[index], selects[index] = cid.fonts[fd].chars[gid], cids[gid], mapped
|
||||||
|
}
|
||||||
|
cid.dicts, cid.private, cid.fonts = dicts, private, fonts
|
||||||
|
cid.charset, cid.selects = encodeCFFSubsetCharset(selected), selects
|
||||||
|
result, err := cid.write(chars)
|
||||||
|
return result, ids, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表
|
||||||
|
// 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号
|
||||||
|
// 返回: []byte 独立OpenType字体, error 封装错误
|
||||||
|
func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, error) {
|
||||||
|
tables := make(map[string][]byte)
|
||||||
|
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "vhea", "gasp"} {
|
||||||
|
if data := font.Tables[tag]; data != nil {
|
||||||
|
tables[tag] = bytes.Clone(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var hmtx, vmtx []byte
|
||||||
|
mapping := make(map[rune]uint16)
|
||||||
|
for index, gid := range ids {
|
||||||
|
hmtx = binary.BigEndian.AppendUint16(hmtx, font.GlyphAdvance(gid))
|
||||||
|
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(font.sideBearing(gid, false)))
|
||||||
|
if font.vertical != 0 {
|
||||||
|
vmtx = binary.BigEndian.AppendUint16(vmtx, font.GlyphVerticalAdvance(gid))
|
||||||
|
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(font.sideBearing(gid, true)))
|
||||||
|
}
|
||||||
|
for _, char := range font.glyphCharacters(gid) {
|
||||||
|
mapping[char] = uint16(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tables["CFF "], tables["hmtx"] = cff, hmtx
|
||||||
|
tables["cmap"] = buildCmapTable(uint16(len(ids)), mapping)
|
||||||
|
binary.BigEndian.PutUint16(tables["hhea"][34:], uint16(len(ids)))
|
||||||
|
binary.BigEndian.PutUint16(tables["maxp"][4:], uint16(len(ids)))
|
||||||
|
if len(tables["post"]) >= 32 {
|
||||||
|
tables["post"] = tables["post"][:32]
|
||||||
|
binary.BigEndian.PutUint32(tables["post"], 0x30000)
|
||||||
|
}
|
||||||
|
if font.vertical != 0 {
|
||||||
|
tables["vmtx"] = vmtx
|
||||||
|
binary.BigEndian.PutUint16(tables["vhea"][34:], uint16(len(ids)))
|
||||||
|
}
|
||||||
|
if data := font.Tables["VORG"]; data != nil {
|
||||||
|
var err error
|
||||||
|
tables["VORG"], err = compactCFFVerticalOrigins(data, font.NumGlyphs(), ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serializeOTF(tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点
|
||||||
|
// 入参: data VORG表, count 原字形数, ids 新编号对应的原编号
|
||||||
|
// 返回: []byte 重建的VORG表, error 表结构或记录顺序错误
|
||||||
|
func compactCFFVerticalOrigins(data []byte, count uint16, ids []uint16) ([]byte, error) {
|
||||||
|
if len(data) < 8 || binary.BigEndian.Uint32(data) != 0x10000 {
|
||||||
|
return nil, fmt.Errorf("invalid CFF vertical origin header")
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(data[6:]))
|
||||||
|
if length > (len(data)-8)/4 {
|
||||||
|
return nil, fmt.Errorf("truncated CFF vertical origins")
|
||||||
|
}
|
||||||
|
origins := make(map[uint16]uint16, length)
|
||||||
|
previous := -1
|
||||||
|
for index := range length {
|
||||||
|
pos := 8 + index*4
|
||||||
|
gid := binary.BigEndian.Uint16(data[pos:])
|
||||||
|
if gid >= count || int(gid) <= previous {
|
||||||
|
return nil, fmt.Errorf("invalid CFF vertical origin glyph")
|
||||||
|
}
|
||||||
|
origins[gid] = binary.BigEndian.Uint16(data[pos+2:])
|
||||||
|
previous = int(gid)
|
||||||
|
}
|
||||||
|
result := bytes.Clone(data[:8])
|
||||||
|
for index, gid := range ids {
|
||||||
|
if origin, ok := origins[gid]; ok {
|
||||||
|
result = binary.BigEndian.AppendUint16(result, uint16(index))
|
||||||
|
result = binary.BigEndian.AppendUint16(result, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(result[6:], uint16((len(result)-8)/4))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCFFSubsetCharset 选择最短的标准字符集格式,保持SID或CID及字形顺序
|
||||||
|
// 入参: ids 按字形编号排列的SID或CID,包含.notdef
|
||||||
|
// 返回: []byte 字符集数据
|
||||||
|
func encodeCFFSubsetCharset(ids []int) []byte {
|
||||||
|
sizes := [3]int{1 + 2*(len(ids)-1), 1, 1}
|
||||||
|
for first := 1; first < len(ids); {
|
||||||
|
end := first + 1
|
||||||
|
for end < len(ids) && ids[end] == ids[end-1]+1 {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
sizes[1] += 3 * ((end - first + 255) / 256)
|
||||||
|
sizes[2] += 4
|
||||||
|
first = end
|
||||||
|
}
|
||||||
|
format := 0
|
||||||
|
for index, size := range sizes {
|
||||||
|
if size < sizes[format] {
|
||||||
|
format = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := make([]byte, 1, sizes[format])
|
||||||
|
result[0] = byte(format)
|
||||||
|
for first := 1; first < len(ids); {
|
||||||
|
end := first + 1
|
||||||
|
if format != 0 {
|
||||||
|
limit := len(ids)
|
||||||
|
if format == 1 {
|
||||||
|
limit = min(limit, first+256)
|
||||||
|
}
|
||||||
|
for end < limit && ids[end] == ids[end-1]+1 {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = binary.BigEndian.AppendUint16(result, uint16(ids[first]))
|
||||||
|
if format == 1 {
|
||||||
|
result = append(result, byte(end-first-1))
|
||||||
|
} else if format == 2 {
|
||||||
|
result = binary.BigEndian.AppendUint16(result, uint16(end-first-1))
|
||||||
|
}
|
||||||
|
first = end
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// readCFFSubsetEncoding 解析自定义编码及补充映射,校验重复编码和字形归属
|
||||||
|
// 入参: data 编码数据, sids 原字符集
|
||||||
|
// 返回: [256]uint16 编码对应的字形编号, error 编码结构错误
|
||||||
|
func readCFFSubsetEncoding(data []byte, sids []int) ([256]uint16, error) {
|
||||||
|
var codes [256]uint16
|
||||||
|
if len(data) < 2 {
|
||||||
|
return codes, fmt.Errorf("truncated CFF encoding")
|
||||||
|
}
|
||||||
|
pos, gid := 2, 1
|
||||||
|
for index := 0; index < int(data[1]); index++ {
|
||||||
|
if pos >= len(data) {
|
||||||
|
return codes, fmt.Errorf("truncated CFF encoding")
|
||||||
|
}
|
||||||
|
first, count := int(data[pos]), 1
|
||||||
|
pos++
|
||||||
|
switch data[0] & 0x7f {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
if pos >= len(data) {
|
||||||
|
return codes, fmt.Errorf("truncated CFF encoding range")
|
||||||
|
}
|
||||||
|
count += int(data[pos])
|
||||||
|
pos++
|
||||||
|
default:
|
||||||
|
return codes, fmt.Errorf("invalid CFF encoding format")
|
||||||
|
}
|
||||||
|
if first+count > len(codes) || gid+count > len(sids) {
|
||||||
|
return codes, fmt.Errorf("CFF encoding range out of bounds")
|
||||||
|
}
|
||||||
|
for code := first; code < first+count; code++ {
|
||||||
|
if codes[code] != 0 {
|
||||||
|
return codes, fmt.Errorf("duplicate CFF encoding")
|
||||||
|
}
|
||||||
|
codes[code] = uint16(gid)
|
||||||
|
gid++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data[0]&0x80 != 0 {
|
||||||
|
if pos >= len(data) {
|
||||||
|
return codes, fmt.Errorf("truncated CFF encoding supplements")
|
||||||
|
}
|
||||||
|
count := int(data[pos])
|
||||||
|
pos++
|
||||||
|
if count > (len(data)-pos)/3 {
|
||||||
|
return codes, fmt.Errorf("truncated CFF encoding supplements")
|
||||||
|
}
|
||||||
|
for range count {
|
||||||
|
code, sid := data[pos], int(binary.BigEndian.Uint16(data[pos+1:]))
|
||||||
|
pos += 3
|
||||||
|
mapped := slices.Index(sids[:gid], sid)
|
||||||
|
if codes[code] != 0 || mapped <= 0 {
|
||||||
|
return codes, fmt.Errorf("invalid CFF supplemental glyph")
|
||||||
|
}
|
||||||
|
codes[code] = uint16(mapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data[0]&0x7f > 1 || pos != len(data) {
|
||||||
|
return codes, fmt.Errorf("invalid CFF encoding length or format")
|
||||||
|
}
|
||||||
|
return codes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCFFSubsetEncoding 按新字形顺序重建主编码及同字形的补充编码
|
||||||
|
// 入参: codes 原编码映射, sids 原字符集, ids 新编号对应的原编号
|
||||||
|
// 返回: []byte 编码数据, error 字形顺序不满足编码约束
|
||||||
|
func encodeCFFSubsetEncoding(codes [256]uint16, sids []int, ids []uint16) ([]byte, error) {
|
||||||
|
var base, supplements []byte
|
||||||
|
encodings := make(map[uint16][]byte)
|
||||||
|
for code, gid := range codes {
|
||||||
|
if gid != 0 {
|
||||||
|
encodings[gid] = append(encodings[gid], byte(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unencoded := false
|
||||||
|
for _, gid := range ids[1:] {
|
||||||
|
values := encodings[gid]
|
||||||
|
if len(values) == 0 {
|
||||||
|
unencoded = true
|
||||||
|
} else if unencoded {
|
||||||
|
return nil, fmt.Errorf("CFF encoded glyph follows unencoded glyph")
|
||||||
|
} else {
|
||||||
|
base = append(base, values[0])
|
||||||
|
for _, code := range values[1:] {
|
||||||
|
supplements = append(supplements, code)
|
||||||
|
supplements = binary.BigEndian.AppendUint16(supplements, uint16(sids[gid]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result []byte
|
||||||
|
if len(base) == 256 {
|
||||||
|
for index, code := range base {
|
||||||
|
if int(code) != index {
|
||||||
|
return nil, fmt.Errorf("CFF encoding exceeds format zero capacity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = []byte{1, 1, 0, 255}
|
||||||
|
} else {
|
||||||
|
result = append([]byte{0, byte(len(base))}, base...)
|
||||||
|
}
|
||||||
|
if len(supplements) != 0 {
|
||||||
|
result[0] |= 0x80
|
||||||
|
result = append(result, byte(len(supplements)/3))
|
||||||
|
result = append(result, supplements...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
+116
-40
@@ -16,15 +16,14 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// subsetNamedCFFFont 裁剪非CID的CFF字体,保留字形编号、组合组件和度量
|
// subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形
|
||||||
// 入参: sfnt 已解析字体, glyphs 使用的字形
|
// 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号
|
||||||
// 返回: []byte 字体子集, error 裁剪错误
|
// 返回: []byte 字体子集, error 裁剪错误
|
||||||
func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
func subsetCFFFont(sfnt *sfntFont, glyphs []uint16, preserve bool) ([]byte, error) {
|
||||||
keep := map[uint16]bool{0: true}
|
keep := map[uint16]bool{0: true}
|
||||||
for _, gid := range glyphs {
|
for _, gid := range glyphs {
|
||||||
if gid >= sfnt.NumGlyphs() {
|
if gid >= sfnt.NumGlyphs() {
|
||||||
@@ -36,6 +35,11 @@ func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return bytes.Clone(sfnt.Write()), nil
|
return bytes.Clone(sfnt.Write()), nil
|
||||||
}
|
}
|
||||||
|
if !preserve {
|
||||||
|
if compact, ids, err := compactCFFGlyphs(data, keep); err == nil {
|
||||||
|
return compactCFFTables(sfnt, compact, ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
tables := maps.Clone(sfnt.Tables)
|
tables := maps.Clone(sfnt.Tables)
|
||||||
tables["CFF "] = data
|
tables["CFF "] = data
|
||||||
mapping := make(map[rune]uint16)
|
mapping := make(map[rune]uint16)
|
||||||
@@ -52,60 +56,106 @@ func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号
|
// subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号
|
||||||
// 入参: data 非CID的CFF数据, keep 保留的字形,调用后包含组合组件
|
// 入参: data CFF数据, keep 保留的字形,调用后包含组合组件
|
||||||
// 返回: []byte 重建的CFF数据, error 结构或依赖错误
|
// 返回: []byte 重建的CFF数据, error 结构或依赖错误
|
||||||
func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) {
|
func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) {
|
||||||
font, err := readType2Font(data)
|
font, err := readType2Font(data)
|
||||||
|
if err != nil {
|
||||||
|
cid, err := readCIDSubsetFont(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chars, err := subsetType2Programs(cid.fonts, cid.selects, keep)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cid.write(chars)
|
||||||
|
}
|
||||||
|
chars, err := subsetType2Programs([]*type2Font{font}, nil, keep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return rebuildNamedCFF(data, font, chars, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subsetType2Programs 按字形所属字典收集子程序依赖并清空未使用的轮廓
|
||||||
|
// 入参: fonts 执行资源, selects 字形所属字典,单字典时可为空, keep 保留的字形
|
||||||
|
// 返回: [][]byte 字形程序, error 执行或字宽错误
|
||||||
|
func subsetType2Programs(fonts []*type2Font, selects []int, keep map[uint16]bool) ([][]byte, error) {
|
||||||
keep[0] = true
|
keep[0] = true
|
||||||
locals := make([]bool, len(font.locals))
|
programs := fonts[0].chars
|
||||||
globals := make([]bool, len(font.globals))
|
locals := make([][]bool, len(fonts))
|
||||||
|
empty := make([]map[float64][]byte, len(fonts))
|
||||||
|
for index, font := range fonts {
|
||||||
|
locals[index] = make([]bool, len(font.locals))
|
||||||
|
empty[index] = make(map[float64][]byte)
|
||||||
|
}
|
||||||
|
globals := make([]bool, len(fonts[0].globals))
|
||||||
var stack [48]float64
|
var stack [48]float64
|
||||||
for gid := range keep {
|
for gid := range keep {
|
||||||
if int(gid) >= len(font.chars) {
|
if int(gid) >= len(programs) {
|
||||||
return nil, fmt.Errorf("CFF glyph index out of range")
|
return nil, fmt.Errorf("CFF glyph index out of range")
|
||||||
}
|
}
|
||||||
|
fd := 0
|
||||||
|
if selects != nil {
|
||||||
|
fd = selects[gid]
|
||||||
|
}
|
||||||
|
font := fonts[fd]
|
||||||
steps := 0
|
steps := 0
|
||||||
state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep, localUsage: locals, globalUsage: globals}
|
state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep, localUsage: locals[fd], globalUsage: globals}
|
||||||
if _, err := state.run(font.chars[gid], 0); err != nil {
|
if _, err := state.run(font.chars[gid], 0); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chars := make([][]byte, len(font.chars))
|
chars := make([][]byte, len(programs))
|
||||||
empty := make(map[float64][]byte)
|
for gid, program := range programs {
|
||||||
for gid, program := range font.chars {
|
|
||||||
if keep[uint16(gid)] {
|
if keep[uint16(gid)] {
|
||||||
chars[gid] = program
|
chars[gid] = program
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fd := 0
|
||||||
|
if selects != nil {
|
||||||
|
fd = selects[gid]
|
||||||
|
}
|
||||||
|
font := fonts[fd]
|
||||||
steps := 0
|
steps := 0
|
||||||
state := type2State{font: font, args: stack[:0], width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep}
|
state := type2State{font: font, args: stack[:0], width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep}
|
||||||
if _, err := state.run(program, 0); err != nil {
|
if _, err := state.run(program, 0); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if encoded, ok := empty[state.width]; ok {
|
if encoded, ok := empty[fd][state.width]; ok {
|
||||||
chars[gid] = encoded
|
chars[gid] = encoded
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var encoded bytes.Buffer
|
var encoded bytes.Buffer
|
||||||
if state.width != font.def {
|
if state.width != font.def {
|
||||||
value := state.width - font.nominal
|
value := state.width - font.nominal
|
||||||
if err := encodeType2Number(&encoded, value); err != nil {
|
divisor := 1
|
||||||
return nil, err
|
if value < -32768 || value >= 32768 {
|
||||||
|
divisor = 2
|
||||||
}
|
}
|
||||||
if parseNumberType2(encoded.Bytes(), 0) != value {
|
for range divisor {
|
||||||
|
if err := encodeType2Number(&encoded, value/float64(divisor)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parseNumberType2(encoded.Bytes(), 0)*float64(divisor) != value {
|
||||||
return nil, fmt.Errorf("CFF width cannot be represented exactly")
|
return nil, fmt.Errorf("CFF width cannot be represented exactly")
|
||||||
}
|
}
|
||||||
|
if divisor == 2 {
|
||||||
|
encoded.Write([]byte{12, 10})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
encoded.WriteByte(14)
|
encoded.WriteByte(14)
|
||||||
chars[gid] = encoded.Bytes()
|
chars[gid] = encoded.Bytes()
|
||||||
empty[state.width] = chars[gid]
|
empty[fd][state.width] = chars[gid]
|
||||||
}
|
}
|
||||||
font.locals = subsetCFFSubroutines(font.locals, locals)
|
globalPrograms := subsetCFFSubroutines(fonts[0].globals, globals)
|
||||||
font.globals = subsetCFFSubroutines(font.globals, globals)
|
for index, font := range fonts {
|
||||||
return rebuildNamedCFF(data, font, chars)
|
font.locals = subsetCFFSubroutines(font.locals, locals[index])
|
||||||
|
font.globals = globalPrograms
|
||||||
|
}
|
||||||
|
return chars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsetCFFSubroutines 将未引用的子程序替换为空返回,保持调用编号及偏移基数
|
// subsetCFFSubroutines 将未引用的子程序替换为空返回,保持调用编号及偏移基数
|
||||||
@@ -125,9 +175,9 @@ func subsetCFFSubroutines(programs [][]byte, used []bool) [][]byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数
|
// rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数
|
||||||
// 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序
|
// 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序, ids 字形重排顺序,nil时保留原编号
|
||||||
// 返回: []byte 重建的CFF数据, error 不支持的结构
|
// 返回: []byte 重建的CFF数据, error 不支持的结构
|
||||||
func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, error) {
|
func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte, ids []uint16) ([]byte, error) {
|
||||||
names, end, err := readType2Index(data, int(data[2]))
|
names, end, err := readType2Index(data, int(data[2]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -151,35 +201,39 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, erro
|
|||||||
return nil, fmt.Errorf("unsupported CFF top operator %d", op)
|
return nil, fmt.Errorf("unsupported CFF top operator %d", op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sids, _, _, _, ok := getCFFCharsetInfo(data, len(chars))
|
sids, _, _, _, ok := getCFFCharsetInfo(data, len(font.chars))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid CFF charset")
|
return nil, fmt.Errorf("invalid CFF charset")
|
||||||
}
|
}
|
||||||
charset := []byte{0}
|
|
||||||
for _, sid := range sids[1:] {
|
|
||||||
charset = binary.BigEndian.AppendUint16(charset, uint16(sid))
|
|
||||||
}
|
|
||||||
encoding, err := cffSubsetEncoding(data, dict)
|
encoding, err := cffSubsetEncoding(data, dict)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if ids != nil {
|
||||||
|
if encoding != nil {
|
||||||
|
codes, err := readCFFSubsetEncoding(encoding, sids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encoding, err = encodeCFFSubsetEncoding(codes, sids, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected := make([]int, len(ids))
|
||||||
|
for index, gid := range ids {
|
||||||
|
selected[index] = sids[gid]
|
||||||
|
}
|
||||||
|
sids = selected
|
||||||
|
}
|
||||||
|
charset := encodeCFFSubsetCharset(sids)
|
||||||
private, err := readType2Private(data, dict[18], &type2Font{})
|
private, err := readType2Private(data, dict[18], &type2Font{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for op := range private {
|
privateData, err := encodeCFFSubsetPrivate(private)
|
||||||
switch op {
|
if err != nil {
|
||||||
case 6, 7, 8, 9, 10, 11, 19, 20, 21, 1209, 1210, 1211, 1212, 1213, 1214, 1217, 1218, 1219:
|
return nil, err
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported CFF private operator %d", op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privateData := encodeCFFDict(private)
|
|
||||||
if _, ok := private[19]; ok {
|
|
||||||
for private[19][0] != float64(len(privateData)) {
|
|
||||||
private[19] = []float64{float64(len(privateData))}
|
|
||||||
privateData = encodeCFFDict(private)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prefix := bytes.Clone(data[:int(data[2])])
|
prefix := bytes.Clone(data[:int(data[2])])
|
||||||
prefix = append(prefix, encodeCFFIndex(names)...)
|
prefix = append(prefix, encodeCFFIndex(names)...)
|
||||||
@@ -212,6 +266,28 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, erro
|
|||||||
return nil, fmt.Errorf("CFF dictionary offsets did not converge")
|
return nil, fmt.Errorf("CFF dictionary offsets did not converge")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeCFFSubsetPrivate 保留私有字典参数,将局部子程序安排在字典之后
|
||||||
|
// 入参: private 私有字典
|
||||||
|
// 返回: []byte 字典数据, error 不支持的参数或偏移错误
|
||||||
|
func encodeCFFSubsetPrivate(private cffDict) ([]byte, error) {
|
||||||
|
for op := range private {
|
||||||
|
switch op {
|
||||||
|
case 6, 7, 8, 9, 10, 11, 19, 20, 21, 1209, 1210, 1211, 1212, 1213, 1214, 1217, 1218, 1219:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported CFF private operator %d", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for range 16 {
|
||||||
|
data := encodeCFFDict(private)
|
||||||
|
value, ok := private[19]
|
||||||
|
if !ok || len(value) == 1 && value[0] == float64(len(data)) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
private[19] = []float64{float64(len(data))}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CFF private offset did not converge")
|
||||||
|
}
|
||||||
|
|
||||||
// cffSubsetEncoding 读取自定义CFF编码及补充映射,预定义编码无需复制
|
// cffSubsetEncoding 读取自定义CFF编码及补充映射,预定义编码无需复制
|
||||||
// 入参: data CFF数据, dict 顶层字典
|
// 入参: data CFF数据, dict 顶层字典
|
||||||
// 返回: []byte 编码数据, error 编码格式错误
|
// 返回: []byte 编码数据, error 编码格式错误
|
||||||
|
|||||||
+16
-11
@@ -102,7 +102,11 @@ func sfntSubsettable(sfnt *sfntFont) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
font, err := readType2Font(sfnt.Tables["CFF "])
|
font, err := readType2Font(sfnt.Tables["CFF "])
|
||||||
return err == nil && len(font.chars) == int(sfnt.NumGlyphs()) || staticCFFPrograms(sfnt) != nil
|
if err == nil {
|
||||||
|
return len(font.chars) == int(sfnt.NumGlyphs())
|
||||||
|
}
|
||||||
|
cid, err := readCIDSubsetFont(sfnt.Tables["CFF "])
|
||||||
|
return err == nil && len(cid.fonts[0].chars) == int(sfnt.NumGlyphs())
|
||||||
}
|
}
|
||||||
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
|
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
|
||||||
}
|
}
|
||||||
@@ -166,13 +170,14 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
|
|||||||
return bytes.Clone(data), nil
|
return bytes.Clone(data), nil
|
||||||
}
|
}
|
||||||
if sfnt.IsCFF {
|
if sfnt.IsCFF {
|
||||||
if font, err := readType2Font(sfnt.Tables["CFF "]); err == nil {
|
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
|
||||||
if len(font.chars) != int(sfnt.NumGlyphs()) {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("CFF glyph count does not match maxp")
|
return nil, err
|
||||||
}
|
|
||||||
return subsetNamedCFFFont(sfnt, glyphs)
|
|
||||||
}
|
}
|
||||||
return subsetStaticCFFFont(sfnt, glyphs)
|
if count != int(sfnt.NumGlyphs()) {
|
||||||
|
return nil, fmt.Errorf("CFF glyph count does not match maxp")
|
||||||
|
}
|
||||||
|
return subsetCFFFont(sfnt, glyphs, mapped)
|
||||||
}
|
}
|
||||||
if sfnt.Tables["fvar"] != nil {
|
if sfnt.Tables["fvar"] != nil {
|
||||||
return subsetVariableSFNTFont(sfnt, glyphs)
|
return subsetVariableSFNTFont(sfnt, glyphs)
|
||||||
@@ -203,7 +208,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
|
|||||||
return subsetCompactSFNTFont(sfnt, glyphs)
|
return subsetCompactSFNTFont(sfnt, glyphs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsetSourceSFNTFont 裁剪静态TrueType或非CID的CFF原有字体,保留字形编号、组合依赖、度量和提示指令
|
// subsetSourceSFNTFont 裁剪静态TrueType或CFF原有字体,保留字形编号、组合依赖、度量和提示指令
|
||||||
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
|
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
|
||||||
// 入参: data 字体数据, usage 全包用字记录
|
// 入参: data 字体数据, usage 全包用字记录
|
||||||
// 返回: []byte 更小的字体子集,无确定收益时为空
|
// 返回: []byte 更小的字体子集,无确定收益时为空
|
||||||
@@ -217,7 +222,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
}
|
}
|
||||||
for tag := range sfnt.Tables {
|
for tag := range sfnt.Tables {
|
||||||
switch tag {
|
switch tag {
|
||||||
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
|
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -250,8 +255,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sfnt.IsCFF {
|
if sfnt.IsCFF {
|
||||||
font, err := readType2Font(sfnt.Tables["CFF "])
|
count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
|
||||||
if err != nil || len(font.chars) != int(sfnt.NumGlyphs()) {
|
if err != nil || count != int(sfnt.NumGlyphs()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs)
|
cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs)
|
||||||
|
|||||||
+63
-7
@@ -35,6 +35,9 @@ import (
|
|||||||
xlanguage "golang.org/x/text/language"
|
xlanguage "golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fontShapingBufferLimit 限制字体实例长期保留的字形缓冲容量
|
||||||
|
const fontShapingBufferLimit = 4096
|
||||||
|
|
||||||
// fontShaper 使用字体度量和原始数据提供可配置塑形,不依赖绘图后端
|
// fontShaper 使用字体度量和原始数据提供可配置塑形,不依赖绘图后端
|
||||||
type fontShaper struct {
|
type fontShaper struct {
|
||||||
metrics FontMetrics
|
metrics FontMetrics
|
||||||
@@ -61,8 +64,32 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
|||||||
if !rasterPositive(size) || !utf8.ValidString(value) {
|
if !rasterPositive(size) || !utf8.ValidString(value) {
|
||||||
return nil, fmt.Errorf("invalid shaping text or size")
|
return nil, fmt.Errorf("invalid shaping text or size")
|
||||||
}
|
}
|
||||||
for _, char := range value {
|
runes := []rune(value)
|
||||||
if unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
|
return f.shapeTextRun(runes, 0, len(runes), size, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShapeTextRun 利用原文上下文塑形指定区间,保留原文簇索引
|
||||||
|
// 入参: value 已校验的原文符文, start 起始符文索引, end 结束符文索引, size 毫米字号, options 已分段的塑形选项
|
||||||
|
// 返回: []ShapedGlyph 原文簇索引及区间内定位字形, error 区间、字符或选项错误
|
||||||
|
func (f *fontShaper) ShapeTextRun(value []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
|
||||||
|
if options.Bidi {
|
||||||
|
return nil, fmt.Errorf("bidirectional segmentation is not valid for an individual shaping run")
|
||||||
|
}
|
||||||
|
return f.shapeTextRun(value, start, end, size, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shapeTextRun 共用完整行与局部区间的选项校验、字体初始化及字形塑形
|
||||||
|
// 入参: runes 原文符文, start 起始符文索引, end 结束符文索引, size 毫米字号, options 塑形选项
|
||||||
|
// 返回: []ShapedGlyph 定位字形, error 字体、区间或选项错误
|
||||||
|
func (f *fontShaper) shapeTextRun(runes []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
|
||||||
|
if !rasterPositive(size) {
|
||||||
|
return nil, fmt.Errorf("invalid shaping size")
|
||||||
|
}
|
||||||
|
if start < 0 || end < start || end > len(runes) {
|
||||||
|
return nil, fmt.Errorf("invalid shaping text range")
|
||||||
|
}
|
||||||
|
for _, char := range runes[start:end] {
|
||||||
|
if !utf8.ValidRune(char) || unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
|
||||||
return nil, fmt.Errorf("shaping requires a single text line")
|
return nil, fmt.Errorf("shaping requires a single text line")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +140,9 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
|||||||
features = append(features, feature)
|
features = append(features, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if start == end {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
f.shapeMu.Lock()
|
f.shapeMu.Lock()
|
||||||
defer f.shapeMu.Unlock()
|
defer f.shapeMu.Unlock()
|
||||||
if f.shapeFont == nil {
|
if f.shapeFont == nil {
|
||||||
@@ -125,24 +155,32 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
|||||||
f.shapeBuffer = harfbuzz.NewBuffer()
|
f.shapeBuffer = harfbuzz.NewBuffer()
|
||||||
}
|
}
|
||||||
if options.Bidi {
|
if options.Bidi {
|
||||||
return f.shapeBidiText([]rune(value), size, props, features)
|
return f.shapeBidiText(runes, size, props, features)
|
||||||
}
|
}
|
||||||
buffer := f.shapeBuffer
|
buffer := f.shapeBuffer
|
||||||
buffer.Clear()
|
buffer.Clear()
|
||||||
|
defer f.releaseShapeBuffer()
|
||||||
buffer.Props = props
|
buffer.Props = props
|
||||||
runes := []rune(value)
|
buffer.AddRunes(runes, start, end-start)
|
||||||
buffer.AddRunes(runes, 0, len(runes))
|
|
||||||
buffer.GuessSegmentProperties()
|
buffer.GuessSegmentProperties()
|
||||||
buffer.Shape(f.shapeFont, features)
|
buffer.Shape(f.shapeFont, features)
|
||||||
unit := size / float64(f.metrics.UnitsPerEm())
|
unit := size / float64(f.metrics.UnitsPerEm())
|
||||||
result := make([]ShapedGlyph, len(buffer.Info))
|
result := make([]ShapedGlyph, len(buffer.Info))
|
||||||
var x, y float64
|
var x, y float64
|
||||||
|
previous, order := -1, -1
|
||||||
for i, info := range buffer.Info {
|
for i, info := range buffer.Info {
|
||||||
if info.Glyph >= typefont.GID(f.metrics.NumGlyphs()) {
|
if info.Glyph >= typefont.GID(f.metrics.NumGlyphs()) {
|
||||||
return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph)
|
return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph)
|
||||||
}
|
}
|
||||||
|
if info.Cluster < start || info.Cluster >= end {
|
||||||
|
return nil, fmt.Errorf("invalid shaped cluster index %d", info.Cluster)
|
||||||
|
}
|
||||||
|
if info.Cluster != previous {
|
||||||
|
previous = info.Cluster
|
||||||
|
order++
|
||||||
|
}
|
||||||
position := buffer.Pos[i]
|
position := buffer.Pos[i]
|
||||||
result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster,
|
result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster, VisualOrder: order,
|
||||||
X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit,
|
X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit,
|
||||||
Advance: float64(position.XAdvance) * unit}
|
Advance: float64(position.XAdvance) * unit}
|
||||||
if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) {
|
if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) {
|
||||||
@@ -155,6 +193,17 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// releaseShapeBuffer 释放原文上下文和超限缓冲,普通文本继续复用已有容量
|
||||||
|
func (f *fontShaper) releaseShapeBuffer() {
|
||||||
|
buffer := f.shapeBuffer
|
||||||
|
if cap(buffer.Info) > fontShapingBufferLimit || cap(buffer.Pos) > fontShapingBufferLimit {
|
||||||
|
f.shapeBuffer = harfbuzz.NewBuffer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer.Clear()
|
||||||
|
buffer.AddRunes(nil, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveFace 返回指定字体
|
// ResolveFace 返回指定字体
|
||||||
// 入参: char 字符
|
// 入参: char 字符
|
||||||
// 返回: *typefont.Face 字体
|
// 返回: *typefont.Face 字体
|
||||||
@@ -211,12 +260,19 @@ func (f *fontShaper) shapeBidiText(runes []rune, size float64, props harfbuzz.Se
|
|||||||
return nil, fmt.Errorf("invalid shaped glyph index %d", glyph.GlyphID)
|
return nil, fmt.Errorf("invalid shaped glyph index %d", glyph.GlyphID)
|
||||||
}
|
}
|
||||||
cluster := glyph.TextIndex()
|
cluster := glyph.TextIndex()
|
||||||
|
if cluster < 0 || cluster >= len(runes) {
|
||||||
|
return nil, fmt.Errorf("invalid shaped cluster index %d", cluster)
|
||||||
|
}
|
||||||
order, exists := visual[cluster]
|
order, exists := visual[cluster]
|
||||||
if !exists {
|
if !exists {
|
||||||
order = len(visual)
|
order = len(visual)
|
||||||
visual[cluster] = order
|
visual[cluster] = order
|
||||||
}
|
}
|
||||||
result = append(result, ShapedGlyph{Glyph: uint16(glyph.GlyphID), Cluster: cluster, VisualOrder: order, X: (x + float64(glyph.XOffset)) * unit, Y: -float64(glyph.YOffset) * unit, Advance: float64(glyph.Advance) * unit})
|
shaped := ShapedGlyph{Glyph: uint16(glyph.GlyphID), Cluster: cluster, VisualOrder: order, X: (x + float64(glyph.XOffset)) * unit, Y: -float64(glyph.YOffset) * unit, Advance: float64(glyph.Advance) * unit}
|
||||||
|
if !finite(shaped.X) || !finite(shaped.Y) || !finite(shaped.Advance) {
|
||||||
|
return nil, fmt.Errorf("shaped position exceeds finite range")
|
||||||
|
}
|
||||||
|
result = append(result, shaped)
|
||||||
x += float64(glyph.Advance)
|
x += float64(glyph.Advance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type type2Font struct {
|
|||||||
names map[string]int
|
names map[string]int
|
||||||
nominal, def float64
|
nominal, def float64
|
||||||
seed uint64
|
seed uint64
|
||||||
|
cid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// type2State 按Type2规范保存跨子程序共享的执行状态
|
// type2State 按Type2规范保存跨子程序共享的执行状态
|
||||||
@@ -489,6 +490,9 @@ func (s *type2State) run(data []byte, depth int) (bool, error) {
|
|||||||
// 入参: code 标准字符码, x 横向偏移, y 纵向偏移
|
// 入参: code 标准字符码, x 横向偏移, y 纵向偏移
|
||||||
// 返回: error 组件或指令错误
|
// 返回: error 组件或指令错误
|
||||||
func (s *type2State) appendComponent(code, x, y float64) error {
|
func (s *type2State) appendComponent(code, x, y float64) error {
|
||||||
|
if s.font.cid {
|
||||||
|
return fmt.Errorf("CID CFF cannot use StandardEncoding components")
|
||||||
|
}
|
||||||
if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" {
|
if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" {
|
||||||
return fmt.Errorf("invalid Type2 component code")
|
return fmt.Errorf("invalid Type2 component code")
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -34,7 +34,7 @@ var ErrPDFPassword = pdfgo.ErrPassword
|
|||||||
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
|
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
|
||||||
// RasterDPI指定局部合成与三维绘制精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
|
// RasterDPI指定局部合成与三维绘制精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
|
||||||
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
||||||
// ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径
|
// ResolveFile读取PDF引用的外部媒体、附件、页面及三维纹理,默认不访问网络或本地路径
|
||||||
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
|
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
|
||||||
// Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化
|
// Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化
|
||||||
// Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样
|
// Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样
|
||||||
@@ -156,7 +156,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8}
|
editor := NewEditor()
|
||||||
|
editor.SetRenderBackends(renderer.Backends())
|
||||||
|
editor.fontDirs, editor.fontFS = renderer.FontSources()
|
||||||
|
editor.fontSourcesCache = renderer.fontSourcesCache
|
||||||
|
importer := pdfImporter{ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
||||||
|
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText}
|
||||||
var diagnostics []pdfgo.Diagnostic
|
var diagnostics []pdfgo.Diagnostic
|
||||||
var readWarning func(pdfgo.Diagnostic)
|
var readWarning func(pdfgo.Diagnostic)
|
||||||
if !options.Strict {
|
if !options.Strict {
|
||||||
@@ -168,11 +173,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
editor := NewEditor()
|
importer.reader = reader
|
||||||
editor.SetRenderBackends(renderer.Backends())
|
|
||||||
editor.fontDirs, editor.fontFS = renderer.FontSources()
|
|
||||||
editor.fontSourcesCache = renderer.fontSourcesCache
|
|
||||||
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
|
||||||
importer.report.Warnings = diagnostics
|
importer.report.Warnings = diagnostics
|
||||||
importer.pageIndexes = make(map[pdfgo.Reference]int)
|
importer.pageIndexes = make(map[pdfgo.Reference]int)
|
||||||
importer.resolveFile = options.ResolveFile
|
importer.resolveFile = options.ResolveFile
|
||||||
|
|||||||
+16
-4
@@ -30,10 +30,16 @@ const pdfModelAttachmentLimit = 1024
|
|||||||
|
|
||||||
// PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
|
// PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
|
||||||
// 缓存与实例上限为0时采用U3DRenderOptions的默认值
|
// 缓存与实例上限为0时采用U3DRenderOptions的默认值
|
||||||
|
// ResolveTexture按原始URL提供外部纹理,空值不访问外部资源
|
||||||
|
// Nodes按名称替换PDF视图中的节点状态,可指定动作轨道与采样时间,不改变原始注解
|
||||||
|
// Time以秒指定模型动画的采样时刻,零值为初始时刻
|
||||||
type PDFThreeDRenderOptions struct {
|
type PDFThreeDRenderOptions struct {
|
||||||
Width, Height int
|
Width, Height int
|
||||||
MaxMemoryBytes int64
|
MaxMemoryBytes int64
|
||||||
MaxInstances int
|
MaxInstances int
|
||||||
|
ResolveTexture U3DTextureResolver
|
||||||
|
Nodes []U3DNodeState
|
||||||
|
Time float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
|
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
|
||||||
@@ -147,7 +153,13 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc
|
|||||||
if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 {
|
if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 {
|
||||||
return nil, fmt.Errorf("invalid PDF 3D raster dimensions")
|
return nil, fmt.Errorf("invalid PDF 3D raster dimensions")
|
||||||
}
|
}
|
||||||
img, err := RenderPDFThreeD(ctx, model, source, PDFThreeDRenderOptions{Width: int(w), Height: int(h)})
|
options := PDFThreeDRenderOptions{Width: int(w), Height: int(h)}
|
||||||
|
if p.resolveFile != nil {
|
||||||
|
options.ResolveTexture = func(ctx context.Context, address string) ([]byte, error) {
|
||||||
|
return p.resolveFile(ctx, pdfgo.FileSpecification{Name: address, FileSystem: "URL"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img, err := RenderPDFThreeD(ctx, model, source, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -166,7 +178,7 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制3DB区域,不执行脚本或动画
|
// RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制指定时刻的3DB区域,不执行脚本
|
||||||
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
|
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
|
||||||
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
|
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
|
||||||
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
|
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
|
||||||
@@ -200,7 +212,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
|
|||||||
if source.Projection != nil {
|
if source.Projection != nil {
|
||||||
projection = *source.Projection
|
projection = *source.Projection
|
||||||
}
|
}
|
||||||
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB}
|
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB, ResolveTexture: options.ResolveTexture, Time: options.Time}
|
||||||
if err := pdfThreeDRenderMode(source, &render); err != nil {
|
if err := pdfThreeDRenderMode(source, &render); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,7 +222,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
|
|||||||
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
|
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r, err := newU3DRender(ctx, model, render)
|
r, err := newU3DRender(ctx, model, render, options.Nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-28
@@ -16,11 +16,10 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"slices"
|
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -42,6 +41,46 @@ type pdfFontRepair struct {
|
|||||||
locations bool
|
locations bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shapePDFText 使用导入字体整形注释文字,将毫米坐标约定转换为PDF文字空间方向
|
||||||
|
// 入参: ctx 取消上下文, source 源字体, text 单行原文, rightToLeft 段落方向
|
||||||
|
// 返回: []pdfgo.ShapedGlyph 千分之一字号的定位字形, error 字体、整形或取消错误
|
||||||
|
func (p *pdfImporter) shapePDFText(ctx context.Context, source *pdfgo.Font, text string, rightToLeft bool) ([]pdfgo.ShapedGlyph, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(source.Program) == 0 {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "annotation shaping without embedded font"}
|
||||||
|
}
|
||||||
|
font, err := p.importedFont(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shaper, ok := font.metrics.(FontShaperOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "annotation text shaping backend"}
|
||||||
|
}
|
||||||
|
options := TextShapeOptions{Bidi: true}
|
||||||
|
if rightToLeft {
|
||||||
|
options.Direction = "rtl"
|
||||||
|
}
|
||||||
|
glyphs, err := shaper.ShapeTextWithOptions(text, 1000, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]pdfgo.ShapedGlyph, len(glyphs))
|
||||||
|
units := float64(font.metrics.UnitsPerEm())
|
||||||
|
if units <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid annotation font units")
|
||||||
|
}
|
||||||
|
for i, glyph := range glyphs {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[i] = pdfgo.ShapedGlyph{ID: glyph.Glyph, Cluster: glyph.Cluster, VisualOrder: glyph.VisualOrder, X: glyph.X, Y: -glyph.Y, Advance: glyph.Advance, Width: float64(font.metrics.GlyphAdvance(glyph.Glyph)) * 1000 / units}
|
||||||
|
}
|
||||||
|
return result, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序
|
// embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序
|
||||||
// 入参: source 外部字体, glyphs 当前文字字形
|
// 入参: source 外部字体, glyphs 当前文字字形
|
||||||
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
|
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
|
||||||
@@ -292,33 +331,13 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(tables["cmap"]) {
|
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(tables["cmap"]) {
|
||||||
mapping := map[rune]uint16{}
|
mapping, err := source.GlyphMap()
|
||||||
var codes []string
|
if err != nil {
|
||||||
if source.Subtype == "TrueType" {
|
return nil, repair, err
|
||||||
codes = make([]string, 256)
|
|
||||||
for code := range codes {
|
|
||||||
codes[code] = string([]byte{byte(code)})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
codes = slices.Sorted(maps.Keys(source.Unicode))
|
|
||||||
}
|
}
|
||||||
for _, code := range codes {
|
for _, glyph := range mapping {
|
||||||
glyphs, err := source.Decode([]byte(code))
|
if glyph >= count {
|
||||||
if err != nil {
|
return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count)
|
||||||
return nil, repair, err
|
|
||||||
}
|
|
||||||
if len(glyphs) != 1 || !glyphs[0].HasID {
|
|
||||||
return nil, repair, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
|
|
||||||
}
|
|
||||||
glyph := glyphs[0]
|
|
||||||
if glyph.ID >= count {
|
|
||||||
return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count)
|
|
||||||
}
|
|
||||||
if utf8.RuneCountInString(glyph.Text) == 1 {
|
|
||||||
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
|
||||||
if _, exists := mapping[char]; !exists {
|
|
||||||
mapping[char] = glyph.ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addPackedGlyphMapping(mapping, count)
|
addPackedGlyphMapping(mapping, count)
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ import (
|
|||||||
|
|
||||||
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
|
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
|
||||||
type pdfTextReplacement struct {
|
type pdfTextReplacement struct {
|
||||||
text *pdfgo.ReplacementText
|
text *pdfgo.ReplacementText
|
||||||
count int
|
count int
|
||||||
offset int64
|
matched int
|
||||||
blocked bool
|
offset int64
|
||||||
applied bool
|
blocked bool
|
||||||
|
applied bool
|
||||||
|
identity bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfTextReplacements 按绘制顺序收集替换区段,避免拆分或重复写入原文
|
// pdfTextReplacements 按绘制顺序收集替换区段,避免拆分或重复写入原文
|
||||||
@@ -104,21 +106,26 @@ func (r *pdfTextReplacements) markedContent(mark pdfgo.MarkedContentMark) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply 将完整且样式一致的区段合并为标准OFD文字对象
|
// apply 合并可等价表示的替换区段,原文相同但样式不同的区段保留原图元
|
||||||
// 入参: nodes 原始图元
|
// 入参: nodes 原始图元
|
||||||
// 返回: []pdfCompositeNode 转换图元, error 取消或语义转换错误
|
// 返回: []pdfCompositeNode 转换图元, error 取消或语义转换错误
|
||||||
func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) {
|
func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) {
|
||||||
if len(r.order) == 0 {
|
if len(r.order) == 0 {
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
r.count(nodes, false)
|
if err := r.count(nodes, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, span := range r.order {
|
||||||
|
span.identity = !span.blocked && span.count > 0 && span.matched == len(span.text.Text)
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
nodes, err = r.merge(nodes)
|
nodes, err = r.merge(nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, span := range r.order {
|
for _, span := range r.order {
|
||||||
if span.applied || !span.blocked && span.count == 0 && span.text.Text == "" {
|
if span.applied || span.identity || !span.blocked && span.count == 0 && span.text.Text == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.importer.warning == nil {
|
if r.importer.warning == nil {
|
||||||
@@ -130,14 +137,33 @@ func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNod
|
|||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// count 统计完整区段中的文字图元,排除需要组级合成的内容
|
// count 统计区段文字并逐字核对原文,排除需要组级合成的内容
|
||||||
// 入参: nodes 图元, blocked 上层组是否阻止直接转换
|
// 入参: nodes 图元, blocked 上层组是否阻止直接转换
|
||||||
func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) {
|
// 返回: error 取消错误
|
||||||
|
func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) error {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
|
if err := r.importer.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if node.text != nil {
|
if node.text != nil {
|
||||||
if span := r.span(node.text.Replacement); span != nil {
|
if span := r.span(node.text.Replacement); span != nil {
|
||||||
span.count++
|
span.count++
|
||||||
span.blocked = span.blocked || blocked
|
span.blocked = span.blocked || blocked
|
||||||
|
for i, glyph := range node.text.Glyphs {
|
||||||
|
if span.matched < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.importer.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if glyph.Text == "" || !strings.HasPrefix(span.text.Text[span.matched:], glyph.Text) {
|
||||||
|
span.matched = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
span.matched += len(glyph.Text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
childBlocked := blocked
|
childBlocked := blocked
|
||||||
@@ -145,8 +171,11 @@ func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) {
|
|||||||
opaque, err := node.opaque(group.ColorSpace)
|
opaque, err := node.opaque(group.ColorSpace)
|
||||||
childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent()
|
childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent()
|
||||||
}
|
}
|
||||||
r.count(node.children, childBlocked)
|
if err := r.count(node.children, childBlocked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge 合并同层连续图元,保留原始绘制顺序与字形坐标
|
// merge 合并同层连续图元,保留原始绘制顺序与字形坐标
|
||||||
@@ -171,7 +200,7 @@ func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNod
|
|||||||
first = node.children[0].text
|
first = node.children[0].text
|
||||||
}
|
}
|
||||||
if first != nil {
|
if first != nil {
|
||||||
if span := r.span(first.Replacement); span != nil && !span.applied && !span.blocked && span.text.Text != "" {
|
if span := r.span(first.Replacement); span != nil && !span.applied && !span.blocked && span.text.Text != "" && !(span.identity && span.count == 1) {
|
||||||
var marks []pdfCompositeNode
|
var marks []pdfCompositeNode
|
||||||
for end = i; end < len(nodes); end++ {
|
for end = i; end < len(nodes); end++ {
|
||||||
current := nodes[end]
|
current := nodes[end]
|
||||||
@@ -221,7 +250,7 @@ func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNod
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear 清除未转换的区段引用,防止局部绘制重复使用整段原文
|
// clear 清除无需替换及未转换的区段引用,防止局部绘制重复使用整段原文
|
||||||
// 入参: nodes 图元
|
// 入参: nodes 图元
|
||||||
func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) {
|
func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -546,6 +547,31 @@ func (r *Reader) readFileView(name string) ([]byte, error) {
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readFileViewContext 读取包内文件并响应取消,已加载数据直接借用
|
||||||
|
// 入参: ctx 取消上下文, name 文件名
|
||||||
|
// 返回: []byte 只读文件内容, error 读取或取消错误
|
||||||
|
func (r *Reader) readFileViewContext(ctx context.Context, name string) ([]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data, ok := r.files[cleanPackagePath(name)]; ok {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
input, err := r.openFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// readFileData 统一解析包内路径并区分缓存借用与独立读取
|
// readFileData 统一解析包内路径并区分缓存借用与独立读取
|
||||||
// 入参: name 文件名
|
// 入参: name 文件名
|
||||||
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
|
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
|
||||||
|
|||||||
+78
-9
@@ -31,7 +31,7 @@ type U3DOptions struct {
|
|||||||
MaxDecodedBytes int64
|
MaxDecodedBytes int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// U3DModel 保存独立于PDF的静态U3D场景,不执行动作或读取外部文件
|
// U3DModel 保存独立于PDF的U3D场景及动作数据,不执行动作或读取外部文件
|
||||||
type U3DModel struct {
|
type U3DModel struct {
|
||||||
Major, Minor int16
|
Major, Minor int16
|
||||||
DefinedUnits bool
|
DefinedUnits bool
|
||||||
@@ -42,6 +42,8 @@ type U3DModel struct {
|
|||||||
Shaders []U3DShader
|
Shaders []U3DShader
|
||||||
Lights []U3DLight
|
Lights []U3DLight
|
||||||
Views []U3DViewResource
|
Views []U3DViewResource
|
||||||
|
Textures []U3DTexture
|
||||||
|
Motions []U3DMotion
|
||||||
Metadata []U3DMetadata
|
Metadata []U3DMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,8 @@ type U3DNode struct {
|
|||||||
LineShaders [][]string
|
LineShaders [][]string
|
||||||
PointShaders [][]string
|
PointShaders [][]string
|
||||||
View *U3DView
|
View *U3DView
|
||||||
|
BoneWeights []U3DBoneWeights
|
||||||
|
Animations []U3DAnimation
|
||||||
}
|
}
|
||||||
|
|
||||||
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
|
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
|
||||||
@@ -77,12 +81,13 @@ type U3DMaterial struct {
|
|||||||
Reflectivity, Opacity float32
|
Reflectivity, Opacity float32
|
||||||
}
|
}
|
||||||
|
|
||||||
// U3DShader 保存无纹理着色器参数,不提前应用混合、透明测试或光照
|
// U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用
|
||||||
type U3DShader struct {
|
type U3DShader struct {
|
||||||
Name, Material string
|
Name, Material string
|
||||||
Attributes, AlphaFunction, Blend uint32
|
Attributes, AlphaFunction, Blend uint32
|
||||||
RenderPass uint32
|
RenderPass uint32
|
||||||
AlphaReference float32
|
AlphaReference float32
|
||||||
|
Textures [8]*U3DTextureLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
|
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
|
||||||
@@ -110,10 +115,12 @@ type u3dDecoder struct {
|
|||||||
meshOrder []string
|
meshOrder []string
|
||||||
nodes map[string]int
|
nodes map[string]int
|
||||||
palettes map[uint32]map[string]bool
|
palettes map[uint32]map[string]bool
|
||||||
|
textures map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeU3D 按ECMA-363解码静态基础网格、线集、点集、组、材质、着色器和光源
|
// DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源
|
||||||
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景
|
// 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验
|
||||||
|
// 渐进网格尚未支持
|
||||||
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
|
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
|
||||||
// 入参: ctx 取消上下文, data U3D数据, options 解码限制
|
// 入参: ctx 取消上下文, data U3D数据, options 解码限制
|
||||||
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
|
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
|
||||||
@@ -177,7 +184,7 @@ func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
|
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
|
||||||
}
|
}
|
||||||
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f
|
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f || block.kind == 0xffffff5c
|
||||||
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
|
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
|
||||||
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
|
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
|
||||||
}
|
}
|
||||||
@@ -209,9 +216,15 @@ func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel,
|
|||||||
}
|
}
|
||||||
d.model.Meshes = append(d.model.Meshes, mesh.mesh)
|
d.model.Meshes = append(d.model.Meshes, mesh.mesh)
|
||||||
}
|
}
|
||||||
|
if err := d.checkTextures(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := d.checkParents(); err != nil {
|
if err := d.checkParents(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := d.checkBoneWeights(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -307,6 +320,12 @@ func (d *u3dDecoder) chain(block u3dBlock) error {
|
|||||||
}
|
}
|
||||||
r := u3dValues{data: block.data}
|
r := u3dValues{data: block.data}
|
||||||
name, kind, flags := r.text(), r.u32(), r.u32()
|
name, kind, flags := r.text(), r.u32(), r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("cannot redefine U3D default resource")
|
||||||
|
}
|
||||||
if kind > 2 || flags & ^uint32(3) != 0 {
|
if kind > 2 || flags & ^uint32(3) != 0 {
|
||||||
return fmt.Errorf("invalid U3D modifier chain")
|
return fmt.Errorf("invalid U3D modifier chain")
|
||||||
}
|
}
|
||||||
@@ -367,6 +386,12 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
|
|||||||
}
|
}
|
||||||
r := u3dValues{data: block.data}
|
r := u3dValues{data: block.data}
|
||||||
name := r.text()
|
name := r.text()
|
||||||
|
if r.err != nil {
|
||||||
|
return "", r.err
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return "", fmt.Errorf("cannot redefine U3D default resource")
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
switch block.kind {
|
switch block.kind {
|
||||||
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
|
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
|
||||||
@@ -394,16 +419,38 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
|
|||||||
return name, fmt.Errorf("invalid U3D primitive continuation location")
|
return name, fmt.Errorf("invalid U3D primitive continuation location")
|
||||||
}
|
}
|
||||||
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
|
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
|
||||||
|
case 0xffffff43, 0xffffff44:
|
||||||
|
if (chain != 0 && chain != 1) || index == 0 || owner != name || r.u32() != index {
|
||||||
|
return name, fmt.Errorf("invalid U3D animation or bone weight modifier chain")
|
||||||
|
}
|
||||||
|
if _, exists := d.nodes[name]; chain == 0 && !exists || chain == 1 && d.meshes[name] == nil {
|
||||||
|
return name, fmt.Errorf("missing U3D animation or bone weight modifier target")
|
||||||
|
}
|
||||||
|
if block.kind == 0xffffff43 {
|
||||||
|
err = d.animation(&r, name, chain, index)
|
||||||
|
} else {
|
||||||
|
err = d.boneWeights(&r, name, chain, index)
|
||||||
|
}
|
||||||
case 0xffffff45:
|
case 0xffffff45:
|
||||||
if chain != 0 || index == 0 || owner != name || r.u32() != index {
|
if chain != 0 || index == 0 || owner != name || r.u32() != index {
|
||||||
return name, fmt.Errorf("invalid U3D shading modifier chain")
|
return name, fmt.Errorf("invalid U3D shading modifier chain")
|
||||||
}
|
}
|
||||||
err = d.shading(&r, name)
|
err = d.shading(&r, name)
|
||||||
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54:
|
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54, 0xffffff56:
|
||||||
if chain >= 0 {
|
if chain >= 0 {
|
||||||
return name, fmt.Errorf("invalid U3D palette resource location")
|
return name, fmt.Errorf("invalid U3D palette resource location")
|
||||||
}
|
}
|
||||||
err = d.resource(&r, block.kind, name)
|
err = d.resource(&r, block.kind, name)
|
||||||
|
case 0xffffff55:
|
||||||
|
if chain >= 0 && (chain != 2 || index != 0) {
|
||||||
|
return name, fmt.Errorf("invalid U3D texture chain")
|
||||||
|
}
|
||||||
|
err = d.textureDeclaration(&r, name)
|
||||||
|
case 0xffffff5c:
|
||||||
|
if chain >= 0 {
|
||||||
|
return name, fmt.Errorf("invalid U3D texture continuation location")
|
||||||
|
}
|
||||||
|
err = d.textureContinuation(&r, name)
|
||||||
default:
|
default:
|
||||||
return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
|
return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
|
||||||
}
|
}
|
||||||
@@ -513,7 +560,7 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
|
|||||||
return r.err
|
return r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// resource 读取独立材质、着色器、光源和视图资源
|
// resource 读取独立材质、着色器、光源、视图和动作资源
|
||||||
// 入参: r 字段读取器, kind 资源类型, name 资源名
|
// 入参: r 字段读取器, kind 资源类型, name 资源名
|
||||||
// 返回: error 结构或能力错误
|
// 返回: error 结构或能力错误
|
||||||
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
|
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
|
||||||
@@ -527,6 +574,8 @@ func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
|
|||||||
}
|
}
|
||||||
palette[name] = true
|
palette[name] = true
|
||||||
switch kind {
|
switch kind {
|
||||||
|
case 0xffffff56:
|
||||||
|
return d.motion(r, name)
|
||||||
case 0xffffff52:
|
case 0xffffff52:
|
||||||
return d.viewResource(r, name)
|
return d.viewResource(r, name)
|
||||||
case 0xffffff54:
|
case 0xffffff54:
|
||||||
@@ -548,8 +597,28 @@ func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
|
|||||||
if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 {
|
if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 {
|
||||||
return fmt.Errorf("invalid U3D shader parameters")
|
return fmt.Errorf("invalid U3D shader parameters")
|
||||||
}
|
}
|
||||||
if channels != 0 {
|
for channel := range shader.Textures {
|
||||||
return fmt.Errorf("unsupported U3D textured shader")
|
if channels&(1<<channel) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := d.reserve(1, 192); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layer := &U3DTextureLayer{Texture: r.text(), Intensity: r.f32(), Blend: r.u8(), BlendSource: r.u8(), BlendConstant: r.f32(), Mode: r.u8(), Alpha: alpha&(1<<channel) != 0}
|
||||||
|
for i := range layer.Transform {
|
||||||
|
layer.Transform[i] = r.f32()
|
||||||
|
}
|
||||||
|
for i := range layer.WrapTransform {
|
||||||
|
layer.WrapTransform[i] = r.f32()
|
||||||
|
}
|
||||||
|
layer.Repeat = r.u8()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 {
|
||||||
|
return fmt.Errorf("invalid U3D texture layer")
|
||||||
|
}
|
||||||
|
shader.Textures[channel] = layer
|
||||||
}
|
}
|
||||||
d.model.Shaders = append(d.model.Shaders, shader)
|
d.model.Shaders = append(d.model.Shaders, shader)
|
||||||
case 0xffffff51:
|
case 0xffffff51:
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DAnimation 保存ECMA-363第9.7.3节的动画修饰器,Index为链内位置
|
||||||
|
// Attributes的1、2、4、8分别表示启动、锁定根骨骼、单轨播放和动作间平滑过渡
|
||||||
|
// TimeScale为整体时间倍率,BlendTime以毫秒计;Motions为空时使用无轨道的默认动作
|
||||||
|
type U3DAnimation struct {
|
||||||
|
Index, Attributes uint32
|
||||||
|
TimeScale, BlendTime float32
|
||||||
|
Motions []U3DAnimationMotion
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DAnimationMotion 保存动画中的动作引用,空名称引用默认动作
|
||||||
|
// Attributes的1、2分别表示循环和同步结束,TimeOffset以毫秒计,TimeScale为动作时间倍率
|
||||||
|
type U3DAnimationMotion struct {
|
||||||
|
Name string
|
||||||
|
Attributes uint32
|
||||||
|
TimeOffset, TimeScale float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// animation 读取动画参数及有序动作引用,不在解码时调度或求值
|
||||||
|
// 入参: r 字段读取器, name 所属链名, chain 链类型, index 链内位置
|
||||||
|
// 返回: error 结构、参数、预算或取消错误
|
||||||
|
func (d *u3dDecoder) animation(r *u3dValues, name string, chain int, index uint32) error {
|
||||||
|
animation := U3DAnimation{Index: index, Attributes: r.u32(), TimeScale: r.f32()}
|
||||||
|
count := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if len(r.data)-r.pos < 4 || uint64(count) > uint64(len(r.data)-r.pos-4)/14 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 32); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
animation.Motions = make([]U3DAnimationMotion, int(count))
|
||||||
|
}
|
||||||
|
for i := range animation.Motions {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
motion := &animation.Motions[i]
|
||||||
|
motion.Name, motion.Attributes = r.text(), r.u32()
|
||||||
|
motion.TimeOffset, motion.TimeScale = r.f32(), r.f32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animation.BlendTime = r.f32()
|
||||||
|
if err := r.done(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := animation.validate(d.ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chain == 1 {
|
||||||
|
mesh := &d.meshes[name].mesh
|
||||||
|
mesh.Animations = append(mesh.Animations, animation)
|
||||||
|
} else {
|
||||||
|
node := &d.model.Nodes[d.nodes[name]]
|
||||||
|
node.Animations = append(node.Animations, animation)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate 校验动画属性及有限数值,保留零倍率与反向播放倍率
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 参数或取消错误
|
||||||
|
func (animation U3DAnimation) validate(ctx context.Context) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if animation.Index == 0 || animation.Attributes & ^uint32(15) != 0 || !finite(float64(animation.TimeScale)) || !finite(float64(animation.BlendTime)) || animation.BlendTime < 0 {
|
||||||
|
return fmt.Errorf("invalid U3D animation parameters")
|
||||||
|
}
|
||||||
|
for i, motion := range animation.Motions {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if motion.Attributes & ^uint32(3) != 0 || !finite(float64(motion.TimeOffset)) || !finite(float64(motion.TimeScale)) {
|
||||||
|
return fmt.Errorf("invalid U3D animation motion parameters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力
|
||||||
|
// 返回: error 引用、能力或取消错误
|
||||||
|
func (r *u3dRender) checkAnimations() error {
|
||||||
|
check := func(animations []U3DAnimation, evaluate bool) error {
|
||||||
|
for _, animation := range animations {
|
||||||
|
if err := animation.validate(r.ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, motion := range animation.Motions {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if motion.Name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index, exists := r.motionIndex[motion.Name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("missing U3D animation motion %q", motion.Name)
|
||||||
|
}
|
||||||
|
if evaluate && len(animation.Motions) > 1 && len(r.model.Motions[index].Tracks) != 0 {
|
||||||
|
return fmt.Errorf("unsupported U3D animation evaluation for motion %q", motion.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var active []bool
|
||||||
|
if r.connected != nil {
|
||||||
|
if err := r.reserve(uint64(len(r.model.Meshes)), 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { r.remaining += uint64(len(active)) }()
|
||||||
|
active = make([]bool, len(r.model.Meshes))
|
||||||
|
for i, node := range r.model.Nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.connected[i] && node.Kind == "Model" {
|
||||||
|
if mesh, exists := r.meshes[node.Resource]; exists {
|
||||||
|
active[mesh] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range r.model.Meshes {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := check(r.model.Meshes[i].Animations, active == nil || active[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range r.model.Nodes {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := check(r.model.Nodes[i].Animations, r.connected == nil || r.connected[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
@@ -46,6 +46,27 @@ type u3dHistogram struct {
|
|||||||
total uint32
|
total uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finish 校验压缩块的冲刷字段及结束位置
|
||||||
|
// 返回: error 截断、冲刷或多余数据错误
|
||||||
|
func (r *u3dBits) finish() error {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if r.pos > uint64(len(r.data))*8 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if r.u32() != 0 {
|
||||||
|
return fmt.Errorf("invalid U3D compression flush")
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if (r.pos-16+7)/8 != uint64(len(r.data)) {
|
||||||
|
return fmt.Errorf("unexpected U3D compressed block data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// take 读取定长字段,失败后不继续推进位置
|
// take 读取定长字段,失败后不继续推进位置
|
||||||
// 入参: size 字节数
|
// 入参: size 字节数
|
||||||
// 返回: []byte 字段数据,失败时为空
|
// 返回: []byte 字段数据,失败时为空
|
||||||
|
|||||||
+18
-4
@@ -63,10 +63,13 @@ func (r *u3dRender) collectLights() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
j, ok := r.lightIndex[node.Resource]
|
j, ok := r.lightIndex[node.Resource]
|
||||||
if !ok {
|
if !ok && node.Resource != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
light := r.model.Lights[j]
|
light := U3DLight{Attributes: 1, Color: [3]float32{.75, .75, .75}}
|
||||||
|
if ok {
|
||||||
|
light = r.model.Lights[j]
|
||||||
|
}
|
||||||
if light.Attributes&1 == 0 {
|
if light.Attributes&1 == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -152,11 +155,21 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// style 解析着色器与材质,未启用的材质项使用固定默认值
|
// shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮
|
||||||
|
// 入参: name 着色器名
|
||||||
|
// 返回: uint32 绘制轮次位图
|
||||||
|
func (r *u3dRender) shaderPass(name string) uint32 {
|
||||||
|
if index, exists := r.shaders[name]; exists {
|
||||||
|
return r.model.Shaders[index].RenderPass
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源
|
||||||
// 入参: name 着色器名
|
// 入参: name 着色器名
|
||||||
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误
|
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误
|
||||||
func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
|
func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
|
||||||
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, AlphaFunction: 0x617, RenderPass: math.MaxUint32}, ambient: [3]float64{.2, .2, .2}, diffuse: [3]float64{.8, .8, .8}, opacity: 1}
|
style := u3dRenderStyle{shader: U3DShader{Attributes: 1, Blend: 0x606, AlphaFunction: 0x617, RenderPass: 1}, ambient: [3]float64{.75, .75, .75}, opacity: 1}
|
||||||
if index, ok := r.shaders[name]; ok {
|
if index, ok := r.shaders[name]; ok {
|
||||||
style.shader = r.model.Shaders[index]
|
style.shader = r.model.Shaders[index]
|
||||||
}
|
}
|
||||||
@@ -166,6 +179,7 @@ func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
|
|||||||
}
|
}
|
||||||
if index, ok := r.materials[shader.Material]; ok {
|
if index, ok := r.materials[shader.Material]; ok {
|
||||||
material := r.model.Materials[index]
|
material := r.model.Materials[index]
|
||||||
|
style.ambient, style.diffuse = [3]float64{.2, .2, .2}, [3]float64{.8, .8, .8}
|
||||||
if material.Attributes > 63 {
|
if material.Attributes > 63 {
|
||||||
return style, fmt.Errorf("invalid U3D render material")
|
return style, fmt.Errorf("invalid U3D render material")
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-9
@@ -68,15 +68,13 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
|
|||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}}
|
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
|
||||||
var err error
|
var err error
|
||||||
if shaded {
|
if shaded {
|
||||||
style, err = r.style(name)
|
style, err = r.style(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if index, ok := r.shaders[name]; ok {
|
|
||||||
style.shader.RenderPass = r.model.Shaders[index].RenderPass
|
|
||||||
}
|
}
|
||||||
if style.shader.RenderPass&(1<<passIndex) == 0 {
|
if style.shader.RenderPass&(1<<passIndex) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -84,15 +82,22 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
|
|||||||
if instance.opacity >= 0 || r.opacity >= 0 {
|
if instance.opacity >= 0 || r.opacity >= 0 {
|
||||||
style.shader.Blend = 0x606
|
style.shader.Blend = 0x606
|
||||||
}
|
}
|
||||||
|
textures, err := r.shaderTextures(style.shader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for i, corner := range corners {
|
for i, corner := range corners {
|
||||||
|
var normal u3dVector
|
||||||
|
if shaded && !mesh.ExcludeNormals {
|
||||||
|
normal = normals[corner.Normal]
|
||||||
|
}
|
||||||
|
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
|
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
|
||||||
if !shaded {
|
if !shaded {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var normal u3dVector
|
|
||||||
if !mesh.ExcludeNormals {
|
|
||||||
normal = normals[corner.Normal]
|
|
||||||
}
|
|
||||||
var diffuse, specular *[4]float32
|
var diffuse, specular *[4]float32
|
||||||
if shading.Attributes&1 != 0 {
|
if shading.Attributes&1 != 0 {
|
||||||
diffuse = &mesh.Diffuse[corner.Diffuse]
|
diffuse = &mesh.Diffuse[corner.Diffuse]
|
||||||
@@ -106,8 +111,9 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
|
|||||||
}
|
}
|
||||||
if instance.opacity >= 0 {
|
if instance.opacity >= 0 {
|
||||||
vertices[i].color[3] = instance.opacity
|
vertices[i].color[3] = instance.opacity
|
||||||
|
vertices[i].opacityOverride = true
|
||||||
}
|
}
|
||||||
if r.opacity >= 0 {
|
if r.opacity >= 0 && textures == nil {
|
||||||
vertices[i].color[3] *= r.opacity
|
vertices[i].color[3] *= r.opacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +232,7 @@ func u3dClipFraction(a, b float64) float64 {
|
|||||||
return a / (a + b)
|
return a / (a + b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// u3dInterpolate 在相机空间插值坐标与未预乘颜色
|
// u3dInterpolate 在相机空间插值位置、颜色与纹理输入
|
||||||
// 入参: a 起点, b 终点, t 比例
|
// 入参: a 起点, b 终点, t 比例
|
||||||
// 返回: u3dVertex 插值顶点
|
// 返回: u3dVertex 插值顶点
|
||||||
func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
|
func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
|
||||||
@@ -237,6 +243,18 @@ func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
|
|||||||
for i := range result.color {
|
for i := range result.color {
|
||||||
result.color[i] = a.color[i]*(1-t) + b.color[i]*t
|
result.color[i] = a.color[i]*(1-t) + b.color[i]*t
|
||||||
}
|
}
|
||||||
|
result.textureMask = a.textureMask | b.textureMask
|
||||||
|
result.opacityOverride = a.opacityOverride
|
||||||
|
if result.textureMask != 0 {
|
||||||
|
for layer := range result.texture {
|
||||||
|
if result.textureMask&(1<<layer) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for c := range result.texture[layer] {
|
||||||
|
result.texture[layer][c] = a.texture[layer][c]*(1-t) + b.texture[layer][c]*t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +362,10 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
|
|||||||
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
|
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
|
||||||
// 返回: error 数值、资源或取消错误
|
// 返回: error 数值、资源或取消错误
|
||||||
func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
|
func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
|
||||||
|
textures, err := r.shaderTextures(shader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
|
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
|
||||||
cy := r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY
|
cy := r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY
|
||||||
planes := [4][4]float64{{r.camera.ScaleX, 0, 0, cx}, {-r.camera.ScaleX, 0, 0, float64(r.output.Rect.Dx()) - cx}, {0, -r.camera.ScaleY, 0, cy}, {0, r.camera.ScaleY, 0, float64(r.output.Rect.Dy()) - cy}}
|
planes := [4][4]float64{{r.camera.ScaleX, 0, 0, cx}, {-r.camera.ScaleX, 0, 0, float64(r.output.Rect.Dx()) - cx}, {0, -r.camera.ScaleY, 0, cy}, {0, r.camera.ScaleY, 0, float64(r.output.Rect.Dy()) - cy}}
|
||||||
@@ -413,6 +435,9 @@ func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader
|
|||||||
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
|
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
|
||||||
if depth <= r.frame[pixel].depth {
|
if depth <= r.frame[pixel].depth {
|
||||||
vertex := u3dInterpolate(a, b, weight)
|
vertex := u3dInterpolate(a, b, weight)
|
||||||
|
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
|
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+6
-14
@@ -16,7 +16,6 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// U3D线集增量编码的算术编码上下文
|
// U3D线集增量编码的算术编码上下文
|
||||||
@@ -94,9 +93,11 @@ func (d *u3dDecoder) primitiveDeclaration(r *u3dValues, name string, points bool
|
|||||||
return fmt.Errorf("invalid U3D reserved primitive parameter")
|
return fmt.Errorf("invalid U3D reserved primitive parameter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.u32() != 0 {
|
bones, err := d.skeleton(r)
|
||||||
return fmt.Errorf("unsupported U3D skeletal primitive set")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
decl.mesh.Bones = bones
|
||||||
if err := r.done(); err != nil {
|
if err := r.done(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -444,17 +445,8 @@ func (d *u3dDecoder) primitiveContinuation(r *u3dValues, name string, points boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if b := reader.bits; b != nil {
|
if b := reader.bits; b != nil {
|
||||||
if b.pos > uint64(len(b.data))*8 {
|
if err := b.finish(); err != nil {
|
||||||
return io.ErrUnexpectedEOF
|
return err
|
||||||
}
|
|
||||||
if b.u32() != 0 {
|
|
||||||
return fmt.Errorf("invalid U3D primitive compression flush")
|
|
||||||
}
|
|
||||||
if b.err != nil {
|
|
||||||
return b.err
|
|
||||||
}
|
|
||||||
if (b.pos-16+7)/8 != uint64(len(b.data)) {
|
|
||||||
return fmt.Errorf("unexpected U3D primitive compressed data")
|
|
||||||
}
|
}
|
||||||
r.pos = len(r.data)
|
r.pos = len(r.data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dTextureInverse 计算包裹变换的逆矩阵,保留非仿射齐次分量
|
||||||
|
// 入参: matrix 按列排列的包裹变换
|
||||||
|
// 返回: [16]float64 逆矩阵, error 奇异矩阵或非有限结果
|
||||||
|
func u3dTextureInverse(matrix [16]float32) ([16]float64, error) {
|
||||||
|
var rows [4][8]float64
|
||||||
|
for row := range 4 {
|
||||||
|
for col := range 4 {
|
||||||
|
rows[row][col] = float64(matrix[col*4+row])
|
||||||
|
if !finite(rows[row][col]) {
|
||||||
|
return [16]float64{}, fmt.Errorf("invalid U3D texture wrap transform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows[row][row+4] = 1
|
||||||
|
}
|
||||||
|
for col := range 4 {
|
||||||
|
pivot := col
|
||||||
|
for row := col + 1; row < 4; row++ {
|
||||||
|
if math.Abs(rows[row][col]) > math.Abs(rows[pivot][col]) {
|
||||||
|
pivot = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rows[pivot][col] == 0 {
|
||||||
|
return [16]float64{}, fmt.Errorf("singular U3D texture wrap transform")
|
||||||
|
}
|
||||||
|
rows[col], rows[pivot] = rows[pivot], rows[col]
|
||||||
|
divisor := rows[col][col]
|
||||||
|
for i := range 8 {
|
||||||
|
rows[col][i] /= divisor
|
||||||
|
}
|
||||||
|
for row := range 4 {
|
||||||
|
if row == col {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
factor := rows[row][col]
|
||||||
|
for i := range 8 {
|
||||||
|
rows[row][i] -= factor * rows[col][i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result [16]float64
|
||||||
|
for row := range 4 {
|
||||||
|
for col := range 4 {
|
||||||
|
value := rows[row][col+4]
|
||||||
|
if !finite(value) {
|
||||||
|
return [16]float64{}, fmt.Errorf("invalid U3D inverse texture wrap transform")
|
||||||
|
}
|
||||||
|
result[col*4+row] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generated 将插值后的位置或法线映射到纹理,再应用纹理变换
|
||||||
|
// 经线未定义时取0,球心纬度取1/2,反射方向奇点取图像中心
|
||||||
|
// 入参: channel 通道索引, input 模型位置或相机法线, position 相机空间位置
|
||||||
|
// 返回: [3]float64 齐次纹理坐标, error 非有限值或无效投影
|
||||||
|
func (s *u3dTextureSet) generated(channel int, input [3]float64, position u3dVector) ([3]float64, error) {
|
||||||
|
layer := s.layers[channel]
|
||||||
|
for _, value := range input {
|
||||||
|
if !finite(value) {
|
||||||
|
return [3]float64{}, fmt.Errorf("invalid U3D texture mapping input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uv := [4]float64{0, 0, 0, 1}
|
||||||
|
if layer.Mode == 4 {
|
||||||
|
incident := u3dVector{0, 0, 1}
|
||||||
|
if s.perspective {
|
||||||
|
incident = position.unit()
|
||||||
|
}
|
||||||
|
normal := u3dVector(input).unit()
|
||||||
|
factor := 2 * normal.dot(incident)
|
||||||
|
reflected := incident.sub(u3dVector{factor * normal[0], factor * normal[1], factor * normal[2]})
|
||||||
|
denominator := 2 * math.Hypot(math.Hypot(reflected[0], reflected[1]), 1-reflected[2])
|
||||||
|
uv[0], uv[1] = .5, .5
|
||||||
|
if denominator != 0 {
|
||||||
|
uv[0] += reflected[0] / denominator
|
||||||
|
uv[1] -= reflected[1] / denominator
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var point [4]float64
|
||||||
|
for row := range 4 {
|
||||||
|
point[row] = s.wrap[channel][12+row]
|
||||||
|
for col, value := range input {
|
||||||
|
point[row] += s.wrap[channel][col*4+row] * value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range 3 {
|
||||||
|
point[i] /= point[3]
|
||||||
|
if !finite(point[i]) {
|
||||||
|
return [3]float64{}, fmt.Errorf("invalid U3D texture wrap projection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch layer.Mode {
|
||||||
|
case 1:
|
||||||
|
uv[0], uv[1] = point[0], point[1]
|
||||||
|
case 2, 3:
|
||||||
|
if point[0] != 0 || point[1] != 0 {
|
||||||
|
uv[0] = math.Atan2(point[1], point[0]) / (2 * math.Pi)
|
||||||
|
if uv[0] < 0 {
|
||||||
|
uv[0]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uv[1] = point[2]
|
||||||
|
if layer.Mode == 3 {
|
||||||
|
direction := (u3dVector{point[0], point[1], point[2]}).unit()
|
||||||
|
uv[1] = math.Acos(min(1, max(-1, direction[2]))) / math.Pi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result [3]float64
|
||||||
|
for component, row := range [3]int{0, 1, 3} {
|
||||||
|
for col, value := range uv {
|
||||||
|
result[component] += float64(layer.Transform[col*4+row]) * value
|
||||||
|
}
|
||||||
|
if !finite(result[component]) {
|
||||||
|
return [3]float64{}, fmt.Errorf("invalid U3D generated texture coordinate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
+22
-18
@@ -29,6 +29,9 @@ type U3DMesh struct {
|
|||||||
Faces []U3DFace
|
Faces []U3DFace
|
||||||
Lines []U3DLine
|
Lines []U3DLine
|
||||||
Points []U3DPoint
|
Points []U3DPoint
|
||||||
|
Bones []U3DBone
|
||||||
|
BoneWeights []U3DBoneWeights
|
||||||
|
Animations []U3DAnimation
|
||||||
Quality [3]uint32
|
Quality [3]uint32
|
||||||
InverseQuantization [5]float32
|
InverseQuantization [5]float32
|
||||||
NormalCrease, NormalUpdate, NormalTolerance float32
|
NormalCrease, NormalUpdate, NormalTolerance float32
|
||||||
@@ -76,7 +79,7 @@ type u3dMeshDeclaration struct {
|
|||||||
|
|
||||||
// primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组
|
// primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组
|
||||||
// 入参: index 图元下标,依次为三角面、线段和点
|
// 入参: index 图元下标,依次为三角面、线段和点
|
||||||
// 返回: uint32 着色编号, []U3DCorner 只读角点
|
// 返回: uint32 着色编号, []U3DCorner 原数组中的角点切片
|
||||||
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
|
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
|
||||||
if index < len(m.Faces) {
|
if index < len(m.Faces) {
|
||||||
return m.Faces[index].Shading, m.Faces[index].Corners[:]
|
return m.Faces[index].Shading, m.Faces[index].Corners[:]
|
||||||
@@ -116,16 +119,20 @@ func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
|
|||||||
decl.mesh.InverseQuantization[index] = r.f32()
|
decl.mesh.InverseQuantization[index] = r.f32()
|
||||||
}
|
}
|
||||||
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
|
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
|
||||||
bones := r.u32()
|
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return r.err
|
return r.err
|
||||||
}
|
}
|
||||||
if minimum > maximum || maximum != decl.counts[1] {
|
if minimum > maximum || maximum != decl.counts[1] {
|
||||||
return fmt.Errorf("invalid U3D mesh resolution")
|
return fmt.Errorf("invalid U3D mesh resolution")
|
||||||
}
|
}
|
||||||
if minimum != maximum || bones != 0 {
|
if minimum != maximum {
|
||||||
return fmt.Errorf("unsupported U3D progressive or skeletal mesh")
|
return fmt.Errorf("unsupported U3D progressive mesh")
|
||||||
}
|
}
|
||||||
|
bones, err := d.skeleton(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decl.mesh.Bones = bones
|
||||||
if minimum == 0 {
|
if minimum == 0 {
|
||||||
for _, count := range decl.counts[:6] {
|
for _, count := range decl.counts[:6] {
|
||||||
if count != 0 {
|
if count != 0 {
|
||||||
@@ -247,10 +254,16 @@ func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
|
|||||||
var histogram u3dHistogram
|
var histogram u3dHistogram
|
||||||
var compressed *u3dBits
|
var compressed *u3dBits
|
||||||
if !d.plain && counts[0] != 0 {
|
if !d.plain && counts[0] != 0 {
|
||||||
if err := d.reserve(1, 1<<20); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
compressed = newU3DBits(r.data[r.pos:])
|
compressed = newU3DBits(r.data[r.pos:])
|
||||||
|
var charged uint64
|
||||||
|
compressed.reserve = func(count, size uint64) error {
|
||||||
|
if err := d.reserve(count, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
charged += count * size
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { d.remaining += charged }()
|
||||||
}
|
}
|
||||||
readIndex := func(count uint32) (uint32, error) {
|
readIndex := func(count uint32) (uint32, error) {
|
||||||
var value uint32
|
var value uint32
|
||||||
@@ -321,17 +334,8 @@ func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if compressed != nil {
|
if compressed != nil {
|
||||||
if compressed.pos > uint64(len(compressed.data))*8 {
|
if err := compressed.finish(); err != nil {
|
||||||
return io.ErrUnexpectedEOF
|
return err
|
||||||
}
|
|
||||||
if compressed.u32() != 0 {
|
|
||||||
return fmt.Errorf("invalid U3D compression flush")
|
|
||||||
}
|
|
||||||
if compressed.err != nil {
|
|
||||||
return compressed.err
|
|
||||||
}
|
|
||||||
if (compressed.pos-16+7)/8 != uint64(len(compressed.data)) {
|
|
||||||
return fmt.Errorf("unexpected U3D compressed block data: decoded %d bits, stored %d bytes", compressed.pos-16, len(compressed.data))
|
|
||||||
}
|
}
|
||||||
r.pos = len(r.data)
|
r.pos = len(r.data)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-5
@@ -40,12 +40,20 @@ const (
|
|||||||
|
|
||||||
// u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加
|
// u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加
|
||||||
type u3dPrimitiveKey struct {
|
type u3dPrimitiveKey struct {
|
||||||
a, b u3dVertex
|
a, b [7]float64
|
||||||
blend uint32
|
blend uint32
|
||||||
function uint32
|
function uint32
|
||||||
reference float32
|
reference float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// u3dTexturedPrimitiveKey 额外区分纹理坐标及纹理层,不扩大普通边点缓存
|
||||||
|
type u3dTexturedPrimitiveKey struct {
|
||||||
|
base u3dPrimitiveKey
|
||||||
|
coordinates [2][8][3]float64
|
||||||
|
layers [8]*U3DTextureLayer
|
||||||
|
opacityOverride bool
|
||||||
|
}
|
||||||
|
|
||||||
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
|
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
|
||||||
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
|
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
|
||||||
// 返回: bool 是否首次绘制, error 预算或取消错误
|
// 返回: bool 是否首次绘制, error 预算或取消错误
|
||||||
@@ -59,14 +67,29 @@ func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, err
|
|||||||
if left[i] != right[i] {
|
if left[i] != right[i] {
|
||||||
if left[i] > right[i] {
|
if left[i] > right[i] {
|
||||||
a, b = b, a
|
a, b = b, a
|
||||||
|
left, right = right, left
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key := u3dPrimitiveKey{a: a, b: b, blend: shader.Blend}
|
key := u3dPrimitiveKey{a: left, b: right, blend: shader.Blend}
|
||||||
if shader.Attributes&2 != 0 {
|
if shader.Attributes&2 != 0 {
|
||||||
key.function, key.reference = shader.AlphaFunction, shader.AlphaReference
|
key.function, key.reference = shader.AlphaFunction, shader.AlphaReference
|
||||||
}
|
}
|
||||||
|
if shader.Textures != ([8]*U3DTextureLayer{}) {
|
||||||
|
textured := u3dTexturedPrimitiveKey{base: key, coordinates: [2][8][3]float64{a.texture, b.texture}, layers: shader.Textures, opacityOverride: a.opacityOverride}
|
||||||
|
if _, ok := r.texturedPrimitives[textured]; ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if r.texturedPrimitives == nil {
|
||||||
|
r.texturedPrimitives = make(map[u3dTexturedPrimitiveKey]struct{})
|
||||||
|
}
|
||||||
|
r.texturedPrimitives[textured] = struct{}{}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
if _, ok := r.primitives[key]; ok {
|
if _, ok := r.primitives[key]; ok {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -221,6 +244,13 @@ func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPa
|
|||||||
if vertex.point[2] > r.frame[pixel].depth {
|
if vertex.point[2] > r.frame[pixel].depth {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
textures, err := r.shaderTextures(shader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
|
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
|
||||||
if err != nil || !visible {
|
if err != nil || !visible {
|
||||||
return err
|
return err
|
||||||
@@ -251,11 +281,10 @@ func (r *u3dRender) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if uint64(shading) >= uint64(len(shaders)) {
|
if uint64(shading) >= uint64(len(shaders)) {
|
||||||
visible = true
|
visible = visible || passIndex == 0
|
||||||
} else if !visible {
|
} else if !visible {
|
||||||
for _, name := range shaders[shading] {
|
for _, name := range shaders[shading] {
|
||||||
index, ok := r.shaders[name]
|
if r.shaderPass(name)&(1<<passIndex) != 0 {
|
||||||
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
|
|
||||||
visible = true
|
visible = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DMotion 保存骨骼或节点动作轨道,解码不执行动画
|
||||||
|
type U3DMotion struct {
|
||||||
|
Name string
|
||||||
|
TimeInverseQuantization, RotationInverseQuantization float32
|
||||||
|
Tracks []U3DMotionTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DMotionTrack 保存命名轨道及其关键帧,保持文件中的时间与帧次序
|
||||||
|
type U3DMotionTrack struct {
|
||||||
|
Name string
|
||||||
|
DisplacementInverseQuantization, ScaleInverseQuantization float32
|
||||||
|
Frames []U3DKeyFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DKeyFrame 保存关键帧时间及相对父项的变换,Rotation按W、X、Y、Z排列
|
||||||
|
type U3DKeyFrame struct {
|
||||||
|
Time float32
|
||||||
|
Displacement, Scale [3]float32
|
||||||
|
Rotation [4]float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// motion 读取ECMA-363第9.8.6节的动作资源,首尾帧保留未量化数值
|
||||||
|
// 入参: values 字段读取器, name 资源名
|
||||||
|
// 返回: error 结构、数值、预算或取消错误
|
||||||
|
func (d *u3dDecoder) motion(values *u3dValues, name string) error {
|
||||||
|
r := u3dResourceReader{values: values, decoder: d}
|
||||||
|
defer r.release()
|
||||||
|
count := r.u32()
|
||||||
|
motion := U3DMotion{Name: name, TimeInverseQuantization: r.f32(), RotationInverseQuantization: r.f32()}
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if motion.TimeInverseQuantization < 0 || motion.RotationInverseQuantization < 0 {
|
||||||
|
return fmt.Errorf("invalid U3D motion quantization")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 48); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
motion.Tracks = make([]U3DMotionTrack, int(count))
|
||||||
|
for index := range motion.Tracks {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
track := &motion.Tracks[index]
|
||||||
|
track.Name = r.text()
|
||||||
|
frames := r.u32()
|
||||||
|
track.DisplacementInverseQuantization, track.ScaleInverseQuantization = r.f32(), r.f32()
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if track.DisplacementInverseQuantization < 0 || track.ScaleInverseQuantization < 0 {
|
||||||
|
return fmt.Errorf("invalid U3D motion track quantization")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(frames), 44); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
track.Frames = make([]U3DKeyFrame, int(frames))
|
||||||
|
for i := range track.Frames {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame := &track.Frames[i]
|
||||||
|
if i == 0 || i == len(track.Frames)-1 {
|
||||||
|
frame.Time = r.f32()
|
||||||
|
for component := range frame.Displacement {
|
||||||
|
frame.Displacement[component] = r.f32()
|
||||||
|
}
|
||||||
|
for component := range frame.Rotation {
|
||||||
|
frame.Rotation[component] = r.f32()
|
||||||
|
}
|
||||||
|
for component := range frame.Scale {
|
||||||
|
frame.Scale[component] = r.f32()
|
||||||
|
}
|
||||||
|
} else if err := r.motionFrame(frame, track.Frames[i-1], &motion, track); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.done(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.model.Motions = append(d.model.Motions, motion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// motionFrame 按前一帧预测时间、位移和缩放,并右乘旋转差分四元数
|
||||||
|
// 入参: frame 目标帧, previous 前一帧, motion 动作量化参数, track 轨道量化参数
|
||||||
|
// 返回: error 符号、数值或读取错误
|
||||||
|
func (r *u3dResourceReader) motionFrame(frame *U3DKeyFrame, previous U3DKeyFrame, motion *U3DMotion, track *U3DMotionTrack) error {
|
||||||
|
time, err := r.motionDifference([]float32{previous.Time}, motion.TimeInverseQuantization, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame.Time = time[0]
|
||||||
|
position, err := r.motionDifference(previous.Displacement[:], track.DisplacementInverseQuantization, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(frame.Displacement[:], position[:])
|
||||||
|
signs := r.value(4, true)
|
||||||
|
if signs > 15 {
|
||||||
|
return fmt.Errorf("invalid U3D motion rotation signs")
|
||||||
|
}
|
||||||
|
var difference [4]float64
|
||||||
|
var squared float64
|
||||||
|
for component := 1; component < 4; component++ {
|
||||||
|
value := float64(r.value(5, false)) * float64(motion.RotationInverseQuantization)
|
||||||
|
if signs&(1<<component) != 0 {
|
||||||
|
value = -value
|
||||||
|
}
|
||||||
|
difference[component] = value
|
||||||
|
squared += value * value
|
||||||
|
}
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if squared > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D motion rotation difference")
|
||||||
|
}
|
||||||
|
difference[0] = math.Sqrt(1 - squared)
|
||||||
|
if signs&1 != 0 {
|
||||||
|
difference[0] = -difference[0]
|
||||||
|
}
|
||||||
|
p := previous.Rotation
|
||||||
|
w, x, y, z := float64(p[0]), float64(p[1]), float64(p[2]), float64(p[3])
|
||||||
|
a, b, c, e := difference[0], difference[1], difference[2], difference[3]
|
||||||
|
frame.Rotation = [4]float32{
|
||||||
|
float32(w*a - x*b - y*c - z*e),
|
||||||
|
float32(w*b + x*a + y*e - z*c),
|
||||||
|
float32(w*c - x*e + y*a + z*b),
|
||||||
|
float32(w*e + x*c - y*b + z*a),
|
||||||
|
}
|
||||||
|
for _, value := range frame.Rotation {
|
||||||
|
if !finite(float64(value)) {
|
||||||
|
return fmt.Errorf("invalid U3D reconstructed rotation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scale, err := r.motionDifference(previous.Scale[:], track.ScaleInverseQuantization, 6)
|
||||||
|
copy(frame.Scale[:], scale[:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// motionDifference 用共享幅值上下文恢复一至三个差分分量
|
||||||
|
// 入参: previous 前一帧分量, scale 逆量化系数, context 符号上下文,下一项为幅值上下文
|
||||||
|
// 返回: [3]float32 重建分量, error 符号、数值或读取错误
|
||||||
|
func (r *u3dResourceReader) motionDifference(previous []float32, scale float32, context int) (values [3]float32, err error) {
|
||||||
|
signs := r.value(context, true)
|
||||||
|
if signs >= 1<<len(previous) {
|
||||||
|
return values, fmt.Errorf("invalid U3D motion difference signs")
|
||||||
|
}
|
||||||
|
for index, prior := range previous {
|
||||||
|
difference := float64(r.value(context+1, false)) * float64(scale)
|
||||||
|
if signs&(1<<index) != 0 {
|
||||||
|
difference = -difference
|
||||||
|
}
|
||||||
|
values[index] = float32(float64(prior) + difference)
|
||||||
|
if !finite(float64(values[index])) {
|
||||||
|
return values, fmt.Errorf("invalid U3D reconstructed motion value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, r.err()
|
||||||
|
}
|
||||||
@@ -47,8 +47,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index, ok := r.shaders[name]
|
if r.shaderPass(name)&(1<<passIndex) != 0 {
|
||||||
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
|
|
||||||
active = true
|
active = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dAnimationState 保存一次绘制中动画求值的节点变换、骨骼姿态及对应权重
|
||||||
|
type u3dAnimationState struct {
|
||||||
|
matrix u3dMatrix
|
||||||
|
bones []U3DBoneState
|
||||||
|
weights *U3DBoneWeights
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dPlaybackKey 标识一条轨道在指定动作时间的采样,不包含节点绑定
|
||||||
|
type u3dPlaybackKey struct {
|
||||||
|
track *U3DMotionTrack
|
||||||
|
time float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果
|
||||||
|
type u3dPlaybackCache struct {
|
||||||
|
render *u3dRender
|
||||||
|
bounds map[*U3DMotionTrack][2]float64
|
||||||
|
samples map[u3dPlaybackKey]U3DKeyFrame
|
||||||
|
bytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// limits 校验轨道时间并缓存范围,避免共享动作按节点数量重复扫描全部帧
|
||||||
|
// 入参: track 动作轨道
|
||||||
|
// 返回: [2]float64 首末时间, error 空轨道、数值、预算或取消错误
|
||||||
|
func (c *u3dPlaybackCache) limits(track *U3DMotionTrack) ([2]float64, error) {
|
||||||
|
if err := c.render.ctx.Err(); err != nil {
|
||||||
|
return [2]float64{}, err
|
||||||
|
}
|
||||||
|
if bounds, exists := c.bounds[track]; exists {
|
||||||
|
return bounds, nil
|
||||||
|
}
|
||||||
|
if len(track.Frames) == 0 {
|
||||||
|
return [2]float64{}, fmt.Errorf("empty U3D animation track")
|
||||||
|
}
|
||||||
|
bounds := [2]float64{math.Inf(1), math.Inf(-1)}
|
||||||
|
for i, frame := range track.Frames {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := c.render.ctx.Err(); err != nil {
|
||||||
|
return bounds, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !finite(float64(frame.Time)) {
|
||||||
|
return bounds, fmt.Errorf("invalid U3D animation key frame time")
|
||||||
|
}
|
||||||
|
bounds[0], bounds[1] = min(bounds[0], float64(frame.Time)), max(bounds[1], float64(frame.Time))
|
||||||
|
}
|
||||||
|
if c.bounds == nil {
|
||||||
|
if err := c.render.reserve(1, 1024); err != nil {
|
||||||
|
return bounds, err
|
||||||
|
}
|
||||||
|
c.bytes += 1024
|
||||||
|
c.bounds = make(map[*U3DMotionTrack][2]float64)
|
||||||
|
}
|
||||||
|
if err := c.render.reserve(1, 96); err != nil {
|
||||||
|
return bounds, err
|
||||||
|
}
|
||||||
|
c.bytes += 96
|
||||||
|
c.bounds[track] = bounds
|
||||||
|
return bounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sample 复用相同轨道和时间的姿态,缓存只在本次求值期间有效
|
||||||
|
// 入参: track 动作轨道, time 已换算的动作时间
|
||||||
|
// 返回: U3DKeyFrame 采样姿态, error 轨道、预算或取消错误
|
||||||
|
func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFrame, error) {
|
||||||
|
if err := c.render.ctx.Err(); err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
key := u3dPlaybackKey{track, time}
|
||||||
|
if frame, exists := c.samples[key]; exists {
|
||||||
|
return frame, nil
|
||||||
|
}
|
||||||
|
frame, err := track.Sample(c.render.ctx, time, false)
|
||||||
|
if err != nil {
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
if c.samples == nil {
|
||||||
|
if err := c.render.reserve(1, 1024); err != nil {
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
c.bytes += 1024
|
||||||
|
c.samples = make(map[u3dPlaybackKey]U3DKeyFrame)
|
||||||
|
}
|
||||||
|
if err := c.render.reserve(1, 128); err != nil {
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
c.bytes += 128
|
||||||
|
c.samples[key] = frame
|
||||||
|
return frame, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟
|
||||||
|
// 入参: time 从初始时刻起算的秒数
|
||||||
|
// 返回: error 动作、骨架、修饰顺序、预算或取消错误
|
||||||
|
func (r *u3dRender) prepareAnimations(time float64) error {
|
||||||
|
cache := u3dPlaybackCache{render: r}
|
||||||
|
defer func() { r.remaining += cache.bytes }()
|
||||||
|
for i := range r.model.Nodes {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.connected != nil && !r.connected[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node := &r.model.Nodes[i]
|
||||||
|
mesh := r.nodeMesh(node)
|
||||||
|
groups := [2][]U3DAnimation{nil, node.Animations}
|
||||||
|
if node.Kind == "Model" && mesh != nil {
|
||||||
|
groups[0] = mesh.Animations
|
||||||
|
}
|
||||||
|
state := u3dAnimationState{matrix: u3dIdentity()}
|
||||||
|
used := false
|
||||||
|
for chain, group := range groups {
|
||||||
|
ordered := group
|
||||||
|
if len(group) > 1 {
|
||||||
|
if err := r.reserve(uint64(len(group)), 48); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ordered = slices.Clone(group)
|
||||||
|
slices.SortFunc(ordered, func(a, b U3DAnimation) int { return cmp.Compare(a.Index, b.Index) })
|
||||||
|
}
|
||||||
|
for index, animation := range ordered {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if index != 0 && animation.Index == ordered[index-1].Index {
|
||||||
|
return fmt.Errorf("duplicate U3D animation chain index")
|
||||||
|
}
|
||||||
|
if len(animation.Motions) == 0 || animation.Motions[0].Name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
motion := &r.model.Motions[r.motionIndex[animation.Motions[0].Name]]
|
||||||
|
if len(motion.Tracks) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
used = true
|
||||||
|
if err := r.sampleAnimation(&state, node, mesh, &animation, motion, chain, time, &cache); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(group) > 1 {
|
||||||
|
r.remaining += uint64(len(group)) * 48
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if used {
|
||||||
|
if r.animated == nil {
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.animated = make(map[string]u3dAnimationState)
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 384); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.animated[node.Name] = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间
|
||||||
|
// 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存
|
||||||
|
// 返回: float64 动作采样时间, error 时间数值或取消错误
|
||||||
|
func (r *u3dRender) animationTime(animation *U3DAnimation, tracks []U3DMotionTrack, bindings map[string]int, time float64, cache *u3dPlaybackCache) (float64, error) {
|
||||||
|
reference := animation.Motions[0]
|
||||||
|
if animation.Attributes&1 == 0 {
|
||||||
|
time = 0
|
||||||
|
}
|
||||||
|
time = time*float64(animation.TimeScale)*float64(reference.TimeScale) - float64(reference.TimeOffset)/1000
|
||||||
|
if !finite(time) {
|
||||||
|
return 0, fmt.Errorf("invalid U3D animation time")
|
||||||
|
}
|
||||||
|
minimum, maximum := math.Inf(1), math.Inf(-1)
|
||||||
|
for i, track := range tracks {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if bindings != nil {
|
||||||
|
if index, exists := bindings[track.Name]; !exists || index != i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bounds, err := cache.limits(&tracks[i])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
minimum, maximum = min(minimum, bounds[0]), max(maximum, bounds[1])
|
||||||
|
}
|
||||||
|
if math.IsInf(minimum, 1) {
|
||||||
|
return 0, fmt.Errorf("empty U3D animation tracks")
|
||||||
|
}
|
||||||
|
if reference.Attributes&1 != 0 && maximum > minimum {
|
||||||
|
time = math.Mod(time-minimum, maximum-minimum)
|
||||||
|
if time < 0 {
|
||||||
|
time += maximum - minimum
|
||||||
|
}
|
||||||
|
time += minimum
|
||||||
|
}
|
||||||
|
return max(minimum, min(time, maximum)), r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换
|
||||||
|
// 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存
|
||||||
|
// 返回: error 轨道、修饰顺序、预算或取消错误
|
||||||
|
func (r *u3dRender) sampleAnimation(state *u3dAnimationState, node *U3DNode, mesh *U3DMesh, animation *U3DAnimation, motion *U3DMotion, chain int, time float64, cache *u3dPlaybackCache) error {
|
||||||
|
if animation.Attributes&4 != 0 {
|
||||||
|
if chain == 0 {
|
||||||
|
return fmt.Errorf("unsupported U3D resource single-track animation")
|
||||||
|
}
|
||||||
|
time, err := r.animationTime(animation, motion.Tracks[:1], nil, time, cache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame, err := cache.sample(&motion.Tracks[0], time)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matrix, err := frame.Matrix()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if animation.Attributes&2 == 0 {
|
||||||
|
state.matrix = state.matrix.multiply(u3dMatrix(matrix))
|
||||||
|
if !state.matrix.valid() {
|
||||||
|
return fmt.Errorf("invalid U3D animated node transform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if node.Kind != "Model" || mesh == nil || len(mesh.Bones) == 0 {
|
||||||
|
return fmt.Errorf("missing U3D animation skeleton")
|
||||||
|
}
|
||||||
|
if state.weights != nil {
|
||||||
|
return fmt.Errorf("unsupported U3D successive skeletal animations")
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(mesh.Bones)), 96); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { r.remaining += uint64(len(mesh.Bones)) * 96 }()
|
||||||
|
tracks := make(map[string]int, len(mesh.Bones))
|
||||||
|
for i, bone := range mesh.Bones {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, exists := tracks[bone.Name]; exists {
|
||||||
|
return fmt.Errorf("duplicate U3D bone %q", bone.Name)
|
||||||
|
}
|
||||||
|
tracks[bone.Name] = -1
|
||||||
|
}
|
||||||
|
bound := 0
|
||||||
|
for i, track := range motion.Tracks {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index, exists := tracks[track.Name]; exists {
|
||||||
|
if index >= 0 {
|
||||||
|
return fmt.Errorf("ambiguous U3D animation track %q", track.Name)
|
||||||
|
}
|
||||||
|
tracks[track.Name] = i
|
||||||
|
bound++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bound == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time, err := r.animationTime(animation, motion.Tracks, tracks, time, cache)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(mesh.Bones)), 64); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.bones = make([]U3DBoneState, 0, len(mesh.Bones))
|
||||||
|
for i, bone := range mesh.Bones {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == 0 && animation.Attributes&2 != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if index := tracks[bone.Name]; index >= 0 {
|
||||||
|
frame, err := cache.sample(&motion.Tracks[index], time)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.bones = append(state.bones, U3DBoneState{Name: bone.Name, Frame: frame})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(state.bones) == 0 {
|
||||||
|
r.remaining += uint64(cap(state.bones)) * 64
|
||||||
|
state.bones = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resourceEnd, nodeEnd := uint64(animation.Index), uint64(0)
|
||||||
|
if chain != 0 {
|
||||||
|
resourceEnd, nodeEnd = math.MaxUint64, uint64(animation.Index)
|
||||||
|
}
|
||||||
|
state.weights, err = r.skinWeightSet(mesh, node.BoneWeights, resourceEnd, nodeEnd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
+91
-33
@@ -36,9 +36,12 @@ type U3DCamera struct {
|
|||||||
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
|
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
|
||||||
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光
|
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光
|
||||||
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
|
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
|
||||||
|
// 纹理合成后先应用节点透明度,再应用Opacity
|
||||||
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
|
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
|
||||||
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
|
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
|
||||||
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
|
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
|
||||||
|
// Time以秒指定动画采样时刻,零值为初始时刻,不随实际时钟推进
|
||||||
|
// ResolveTexture提供外部纹理,空值不访问外部资源;回调自身的内存与超时由调用方控制
|
||||||
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
|
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
|
||||||
type U3DRenderOptions struct {
|
type U3DRenderOptions struct {
|
||||||
Width, Height int
|
Width, Height int
|
||||||
@@ -53,7 +56,9 @@ type U3DRenderOptions struct {
|
|||||||
FaceColor *[3]float64
|
FaceColor *[3]float64
|
||||||
CreaseAngle *float64
|
CreaseAngle *float64
|
||||||
Nodes []U3DNodeState
|
Nodes []U3DNodeState
|
||||||
|
Time float64
|
||||||
Sections []U3DCrossSection
|
Sections []U3DCrossSection
|
||||||
|
ResolveTexture U3DTextureResolver
|
||||||
MaxMemoryBytes int64
|
MaxMemoryBytes int64
|
||||||
MaxInstances int
|
MaxInstances int
|
||||||
}
|
}
|
||||||
@@ -87,8 +92,15 @@ type u3dRender struct {
|
|||||||
toCamera u3dMatrix
|
toCamera u3dMatrix
|
||||||
scene []u3dInstance
|
scene []u3dInstance
|
||||||
byNode [][]int
|
byNode [][]int
|
||||||
|
connected []bool
|
||||||
members []bool
|
members []bool
|
||||||
meshes, shaders, materials, lightIndex map[string]int
|
meshes, shaders, materials, lightIndex map[string]int
|
||||||
|
textureIndex map[string]int
|
||||||
|
viewIndex, motionIndex map[string]int
|
||||||
|
textures map[string]*image.NRGBA64
|
||||||
|
textureSets map[[8]*U3DTextureLayer]*u3dTextureSet
|
||||||
|
resolveTexture U3DTextureResolver
|
||||||
|
view *U3DView
|
||||||
lights []u3dRenderLight
|
lights []u3dRenderLight
|
||||||
lighting *U3DLighting
|
lighting *U3DLighting
|
||||||
opacity float64
|
opacity float64
|
||||||
@@ -97,7 +109,10 @@ type u3dRender struct {
|
|||||||
auxiliaryColor, faceColor [3]float64
|
auxiliaryColor, faceColor [3]float64
|
||||||
depthOnly bool
|
depthOnly bool
|
||||||
primitives map[u3dPrimitiveKey]struct{}
|
primitives map[u3dPrimitiveKey]struct{}
|
||||||
|
texturedPrimitives map[u3dTexturedPrimitiveKey]struct{}
|
||||||
states map[string]U3DNodeState
|
states map[string]U3DNodeState
|
||||||
|
posed map[string]*U3DMesh
|
||||||
|
animated map[string]u3dAnimationState
|
||||||
sections []u3dSection
|
sections []u3dSection
|
||||||
sectionScratch [2][]u3dVertex
|
sectionScratch [2][]u3dVertex
|
||||||
chunks [][]u3dFragment
|
chunks [][]u3dFragment
|
||||||
@@ -106,8 +121,16 @@ type u3dRender struct {
|
|||||||
order uint64
|
order uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderU3D 绘制静态无纹理网格、线集、点集及包围盒,返回不透明图像,不执行脚本或动画
|
// RenderU3D 绘制几何及视图纹理层,返回指定时刻的不透明图像,不执行脚本
|
||||||
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源
|
// 可通过节点覆盖指定动作轨道与采样时间,逐次生成独立姿态,不修改原模型
|
||||||
|
// 显式骨骼姿态按权重变换位置和法线,暂不生成骨骼链接或推导缺失权重
|
||||||
|
// 动画修饰器支持单动作的节点和骨骼求值,尚未实现的动作衔接返回明确错误
|
||||||
|
// 几何纹理支持模型坐标与程序化映射,参考形状约定见U3DTextureLayer
|
||||||
|
// 纹理按需解码并复用,不自动访问外部图像
|
||||||
|
// 采用逐顶点光照及逐像素深度混合,空名称引用标准默认资源
|
||||||
|
// 缺失几何、材质和着色器采用默认资源,缺失的非空光源引用不产生光照
|
||||||
|
// 缺失纹理返回错误,空纹理名称使用标准默认图像
|
||||||
|
// 仅绘制默认根的后代,非空父节点引用缺失时不展开该父路径
|
||||||
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
|
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
|
||||||
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
|
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
|
||||||
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
|
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
|
||||||
@@ -123,17 +146,17 @@ func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newU3DRender 建立有预算限制的场景实例与资源索引
|
// newU3DRender 建立有预算限制的场景实例与资源索引
|
||||||
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制
|
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组
|
||||||
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误
|
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误
|
||||||
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*u3dRender, error) {
|
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*u3dRender, error) {
|
||||||
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 {
|
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 || !finite(options.Time) {
|
||||||
return nil, fmt.Errorf("invalid U3D render options")
|
return nil, fmt.Errorf("invalid U3D render options")
|
||||||
}
|
}
|
||||||
limit := options.MaxMemoryBytes
|
limit := options.MaxMemoryBytes
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
limit = u3dDefaultBytes
|
limit = u3dDefaultBytes
|
||||||
}
|
}
|
||||||
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45}
|
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45, resolveTexture: options.ResolveTexture}
|
||||||
if options.Mode > U3DRenderShadedIllustration {
|
if options.Mode > U3DRenderShadedIllustration {
|
||||||
return nil, fmt.Errorf("unsupported U3D render mode")
|
return nil, fmt.Errorf("unsupported U3D render mode")
|
||||||
}
|
}
|
||||||
@@ -157,23 +180,37 @@ func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions
|
|||||||
if err := r.reserve(pixels, 60); err != nil {
|
if err := r.reserve(pixels, 60); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := r.resources(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
order, index, err := r.sceneGraph()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.checkAnimations(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
maxInstances := options.MaxInstances
|
maxInstances := options.MaxInstances
|
||||||
if maxInstances == 0 {
|
if maxInstances == 0 {
|
||||||
maxInstances = 65536
|
maxInstances = 65536
|
||||||
}
|
}
|
||||||
if err := r.nodeStates(options.Nodes); err != nil {
|
groups := append([][]U3DNodeState{options.Nodes}, states...)
|
||||||
|
if err := r.nodeStates(groups...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.instances(maxInstances); err != nil {
|
if err := r.prepareAnimations(options.Time); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.instances(order, index, maxInstances); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.poseNodes(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.reserve(uint64(len(r.scene)), 1); err != nil {
|
if err := r.reserve(uint64(len(r.scene)), 1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.members = make([]bool, len(r.scene))
|
r.members = make([]bool, len(r.scene))
|
||||||
if err := r.resources(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +260,21 @@ func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*ima
|
|||||||
}
|
}
|
||||||
r.frame[i].color = background
|
r.frame[i].color = background
|
||||||
}
|
}
|
||||||
|
if r.view != nil {
|
||||||
|
if err := r.viewLayers(r.view.Backdrops); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
for passIndex, pass := range passes {
|
for passIndex, pass := range passes {
|
||||||
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
|
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if r.view != nil {
|
||||||
|
if err := r.viewLayers(r.view.Overlays); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, pixel := range r.frame {
|
for i, pixel := range r.frame {
|
||||||
if i%4096 == 0 {
|
if i%4096 == 0 {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
@@ -263,6 +310,7 @@ func (r *u3dRender) reserve(count, size uint64) error {
|
|||||||
// 入参: options 绘制选项
|
// 入参: options 绘制选项
|
||||||
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
|
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
|
||||||
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) {
|
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) {
|
||||||
|
r.view = nil
|
||||||
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
|
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
|
||||||
selected := -1
|
selected := -1
|
||||||
for i, node := range r.model.Nodes {
|
for i, node := range r.model.Nodes {
|
||||||
@@ -286,23 +334,12 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error)
|
|||||||
return nil, fmt.Errorf("invalid U3D view node")
|
return nil, fmt.Errorf("invalid U3D view node")
|
||||||
}
|
}
|
||||||
view := node.View
|
view := node.View
|
||||||
if len(view.Backdrops) != 0 || len(view.Overlays) != 0 {
|
if view.Attributes & ^uint32(7) != 0 {
|
||||||
return nil, fmt.Errorf("unsupported U3D textured view layers")
|
return nil, fmt.Errorf("invalid U3D view attributes")
|
||||||
}
|
}
|
||||||
found := false
|
r.view = view
|
||||||
for i, resource := range r.model.Views {
|
if i, exists := r.viewIndex[node.Resource]; exists {
|
||||||
if i%256 == 0 {
|
passes = r.model.Views[i].Passes
|
||||||
if err := r.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if resource.Name == node.Resource {
|
|
||||||
if found {
|
|
||||||
return nil, fmt.Errorf("duplicate U3D view resource")
|
|
||||||
}
|
|
||||||
passes = resource.Passes
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if options.Camera == nil {
|
if options.Camera == nil {
|
||||||
if options.Instance >= len(r.byNode[selected]) {
|
if options.Instance >= len(r.byNode[selected]) {
|
||||||
@@ -374,10 +411,10 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error)
|
|||||||
return passes, nil
|
return passes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resources 建立只读资源索引,不按名称反复扫描场景
|
// resources 建立只读资源索引,空名称保留给标准默认资源
|
||||||
// 返回: error 重名或预算错误
|
// 返回: error 名称、预算或取消错误
|
||||||
func (r *u3dRender) resources() error {
|
func (r *u3dRender) resources() error {
|
||||||
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights))
|
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) + uint64(len(r.model.Textures)) + uint64(len(r.model.Views)) + uint64(len(r.model.Motions))
|
||||||
if err := r.reserve(count, 96); err != nil {
|
if err := r.reserve(count, 96); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -385,12 +422,18 @@ func (r *u3dRender) resources() error {
|
|||||||
r.shaders = make(map[string]int, len(r.model.Shaders))
|
r.shaders = make(map[string]int, len(r.model.Shaders))
|
||||||
r.materials = make(map[string]int, len(r.model.Materials))
|
r.materials = make(map[string]int, len(r.model.Materials))
|
||||||
r.lightIndex = make(map[string]int, len(r.model.Lights))
|
r.lightIndex = make(map[string]int, len(r.model.Lights))
|
||||||
|
r.textureIndex = make(map[string]int, len(r.model.Textures))
|
||||||
|
r.viewIndex = make(map[string]int, len(r.model.Views))
|
||||||
|
r.motionIndex = make(map[string]int, len(r.model.Motions))
|
||||||
add := func(index map[string]int, name string, i int) error {
|
add := func(index map[string]int, name string, i int) error {
|
||||||
if i%256 == 0 {
|
if i%256 == 0 {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("cannot redefine U3D default resource")
|
||||||
|
}
|
||||||
if _, ok := index[name]; ok {
|
if _, ok := index[name]; ok {
|
||||||
return fmt.Errorf("duplicate U3D render resource %q", name)
|
return fmt.Errorf("duplicate U3D render resource %q", name)
|
||||||
}
|
}
|
||||||
@@ -417,6 +460,21 @@ func (r *u3dRender) resources() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i, v := range r.model.Textures {
|
||||||
|
if err := add(r.textureIndex, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Views {
|
||||||
|
if err := add(r.viewIndex, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Motions {
|
||||||
|
if err := add(r.motionIndex, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,11 +552,11 @@ func (r *u3dRender) drawScene(pass U3DViewPass, passIndex int) error {
|
|||||||
if node.Visibility > 3 {
|
if node.Visibility > 3 {
|
||||||
return fmt.Errorf("invalid U3D model visibility")
|
return fmt.Errorf("invalid U3D model visibility")
|
||||||
}
|
}
|
||||||
mesh, ok := r.meshes[node.Resource]
|
mesh := r.nodeMesh(node)
|
||||||
if !ok {
|
if mesh == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := r.drawMesh(instance, node, &r.model.Meshes[mesh], pass, passIndex); err != nil {
|
if err := r.drawMesh(instance, node, mesh, pass, passIndex); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dResourceReader 读取骨骼权重与动作资源中交错的普通字段和压缩字段
|
||||||
|
type u3dResourceReader struct {
|
||||||
|
values *u3dValues
|
||||||
|
decoder *u3dDecoder
|
||||||
|
bits *u3dBits
|
||||||
|
hist [8]u3dHistogram
|
||||||
|
charged uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// value 读取指定上下文的压缩字段,首次使用时初始化算术区间
|
||||||
|
// 入参: context 压缩上下文, small 是否为8位字段
|
||||||
|
// 返回: uint32 字段值
|
||||||
|
func (r *u3dResourceReader) value(context int, small bool) uint32 {
|
||||||
|
if r.err() != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if r.decoder.plain {
|
||||||
|
if small {
|
||||||
|
return uint32(r.values.u8())
|
||||||
|
}
|
||||||
|
return r.values.u32()
|
||||||
|
}
|
||||||
|
if r.bits == nil {
|
||||||
|
r.bits = newU3DBits(r.values.data[r.values.pos:])
|
||||||
|
r.bits.reserve = func(count, size uint64) error {
|
||||||
|
if err := r.decoder.reserve(count, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.charged += count * size
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.bits.dynamicValue(&r.hist[context], small)
|
||||||
|
}
|
||||||
|
|
||||||
|
// u32 读取普通32位字段,压缩开始后沿用当前算术区间
|
||||||
|
// 返回: uint32 字段值
|
||||||
|
func (r *u3dResourceReader) u32() uint32 {
|
||||||
|
if r.bits != nil {
|
||||||
|
return r.bits.u32()
|
||||||
|
}
|
||||||
|
return r.values.u32()
|
||||||
|
}
|
||||||
|
|
||||||
|
// f32 读取有限单精度数值
|
||||||
|
// 返回: float32 字段值
|
||||||
|
func (r *u3dResourceReader) f32() float32 {
|
||||||
|
value := math.Float32frombits(r.u32())
|
||||||
|
if !finite(float64(value)) && r.err() == nil {
|
||||||
|
r.values.err = fmt.Errorf("invalid U3D floating-point value")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// text 读取UTF-8名称,压缩开始后逐字节解码而不重置区间
|
||||||
|
// 返回: string 名称
|
||||||
|
func (r *u3dResourceReader) text() string {
|
||||||
|
if r.err() != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if r.bits == nil {
|
||||||
|
return r.values.text()
|
||||||
|
}
|
||||||
|
length := int(r.bits.byteValue()) | int(r.bits.byteValue())<<8
|
||||||
|
if r.err() != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if err := r.decoder.reserve(uint64(length), 2); err != nil {
|
||||||
|
r.values.err = err
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data := make([]byte, length)
|
||||||
|
for index := range data {
|
||||||
|
if index%1024 == 0 {
|
||||||
|
if err := r.decoder.ctx.Err(); err != nil {
|
||||||
|
r.values.err = err
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data[index] = r.bits.byteValue()
|
||||||
|
}
|
||||||
|
if r.err() == nil && !utf8.Valid(data) {
|
||||||
|
r.values.err = fmt.Errorf("invalid U3D UTF-8 string")
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// err 返回字段或算术解码错误
|
||||||
|
// 返回: error 读取错误
|
||||||
|
func (r *u3dResourceReader) err() error {
|
||||||
|
if r.values.err != nil {
|
||||||
|
return r.values.err
|
||||||
|
}
|
||||||
|
if r.bits != nil {
|
||||||
|
return r.bits.err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// done 校验末尾冲刷字段和块边界
|
||||||
|
// 返回: error 读取或多余数据错误
|
||||||
|
func (r *u3dResourceReader) done() error {
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.bits != nil {
|
||||||
|
if err := r.bits.finish(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.values.pos = len(r.values.data)
|
||||||
|
}
|
||||||
|
return r.values.done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// release 归还当前块压缩上下文占用的临时预算
|
||||||
|
func (r *u3dResourceReader) release() {
|
||||||
|
r.decoder.remaining += r.charged
|
||||||
|
r.charged = 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DTrackState 指定动作资源、轨道和采样时间,Time与关键帧使用相同单位
|
||||||
|
// Loop将时间循环到首末帧之间,未启用时停留在端点,不自动解释动画修饰器的时间参数
|
||||||
|
type U3DTrackState struct {
|
||||||
|
Motion, Track string
|
||||||
|
Time float64
|
||||||
|
Loop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample 按时间采样轨道,不修改原始帧或按帧数量分配临时数组
|
||||||
|
// 位移和缩放采用线性插值,旋转归一化后采用最短弧球面插值
|
||||||
|
// 允许帧乱序,同一时刻取文件中最后一帧;循环区间不包含末端,单帧轨道始终返回该帧
|
||||||
|
// 入参: ctx 取消上下文, time 与关键帧同单位的时间, loop 是否循环
|
||||||
|
// 返回: U3DKeyFrame 采样姿态, error 空轨道、数值或取消错误
|
||||||
|
func (track U3DMotionTrack) Sample(ctx context.Context, time float64, loop bool) (U3DKeyFrame, error) {
|
||||||
|
if ctx == nil || !finite(time) || len(track.Frames) == 0 {
|
||||||
|
return U3DKeyFrame{}, fmt.Errorf("invalid U3D motion sample")
|
||||||
|
}
|
||||||
|
first, last := 0, 0
|
||||||
|
for index, frame := range track.Frames {
|
||||||
|
if index%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !finite(float64(frame.Time)) {
|
||||||
|
return U3DKeyFrame{}, fmt.Errorf("invalid U3D key frame time")
|
||||||
|
}
|
||||||
|
if frame.Time <= track.Frames[first].Time {
|
||||||
|
first = index
|
||||||
|
}
|
||||||
|
if frame.Time >= track.Frames[last].Time {
|
||||||
|
last = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
minimum, maximum := float64(track.Frames[first].Time), float64(track.Frames[last].Time)
|
||||||
|
if loop && maximum > minimum {
|
||||||
|
time = math.Mod(time-minimum, maximum-minimum)
|
||||||
|
if time < 0 {
|
||||||
|
time += maximum - minimum
|
||||||
|
}
|
||||||
|
time += minimum
|
||||||
|
} else {
|
||||||
|
time = max(minimum, min(time, maximum))
|
||||||
|
}
|
||||||
|
lower, upper := first, last
|
||||||
|
for index, frame := range track.Frames {
|
||||||
|
if index%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := float64(frame.Time)
|
||||||
|
if value <= time && frame.Time >= track.Frames[lower].Time {
|
||||||
|
lower = index
|
||||||
|
}
|
||||||
|
if value > time && frame.Time <= track.Frames[upper].Time {
|
||||||
|
upper = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a, b := track.Frames[lower], track.Frames[upper]
|
||||||
|
if float64(a.Time) == time {
|
||||||
|
b = a
|
||||||
|
}
|
||||||
|
qa, err := a.quaternion()
|
||||||
|
if err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
qb, err := b.quaternion()
|
||||||
|
if err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
fraction := float64(0)
|
||||||
|
if b.Time != a.Time {
|
||||||
|
fraction = (time - float64(a.Time)) / (float64(b.Time) - float64(a.Time))
|
||||||
|
}
|
||||||
|
result := U3DKeyFrame{Time: float32(time)}
|
||||||
|
for i := range result.Displacement {
|
||||||
|
result.Displacement[i] = float32(float64(a.Displacement[i])*(1-fraction) + float64(b.Displacement[i])*fraction)
|
||||||
|
result.Scale[i] = float32(float64(a.Scale[i])*(1-fraction) + float64(b.Scale[i])*fraction)
|
||||||
|
}
|
||||||
|
var dot float64
|
||||||
|
for i := range qa {
|
||||||
|
dot += qa[i] * qb[i]
|
||||||
|
}
|
||||||
|
if dot < 0 {
|
||||||
|
dot = -dot
|
||||||
|
for i := range qb {
|
||||||
|
qb[i] = -qb[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left, right := 1-fraction, fraction
|
||||||
|
if dot < .9995 {
|
||||||
|
angle := math.Acos(min(1, dot))
|
||||||
|
divisor := math.Sin(angle)
|
||||||
|
left, right = math.Sin(left*angle)/divisor, math.Sin(right*angle)/divisor
|
||||||
|
}
|
||||||
|
var rotation [4]float64
|
||||||
|
var length float64
|
||||||
|
for i := range rotation {
|
||||||
|
rotation[i] = left*qa[i] + right*qb[i]
|
||||||
|
length += rotation[i] * rotation[i]
|
||||||
|
}
|
||||||
|
length = math.Sqrt(length)
|
||||||
|
for i := range result.Rotation {
|
||||||
|
result.Rotation[i] = float32(rotation[i] / length)
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return U3DKeyFrame{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// quaternion 校验姿态分量并归一化W、X、Y、Z旋转四元数
|
||||||
|
// 返回: [4]float64 单位四元数, error 非有限或零旋转错误
|
||||||
|
func (frame U3DKeyFrame) quaternion() ([4]float64, error) {
|
||||||
|
var rotation [4]float64
|
||||||
|
for _, values := range [][3]float32{frame.Displacement, frame.Scale} {
|
||||||
|
for _, value := range values {
|
||||||
|
if !finite(float64(value)) {
|
||||||
|
return rotation, fmt.Errorf("invalid U3D key frame transform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var length float64
|
||||||
|
for i, value := range frame.Rotation {
|
||||||
|
rotation[i] = float64(value)
|
||||||
|
length += rotation[i] * rotation[i]
|
||||||
|
}
|
||||||
|
if !finite(length) || length == 0 {
|
||||||
|
return rotation, fmt.Errorf("invalid U3D key frame rotation")
|
||||||
|
}
|
||||||
|
length = math.Sqrt(length)
|
||||||
|
for i := range rotation {
|
||||||
|
rotation[i] /= length
|
||||||
|
}
|
||||||
|
return rotation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix 将关键帧位移、旋转和缩放组合成相对父项的列式仿射矩阵,不使用Time
|
||||||
|
// 返回: [16]float64 先缩放、再旋转、后平移的变换, error 姿态数值错误
|
||||||
|
func (frame U3DKeyFrame) Matrix() ([16]float64, error) {
|
||||||
|
q, err := frame.quaternion()
|
||||||
|
if err != nil {
|
||||||
|
return [16]float64{}, err
|
||||||
|
}
|
||||||
|
w, x, y, z := q[0], q[1], q[2], q[3]
|
||||||
|
matrix := [16]float64{
|
||||||
|
1 - 2*(y*y+z*z), 2 * (x*y + w*z), 2 * (x*z - w*y), 0,
|
||||||
|
2 * (x*y - w*z), 1 - 2*(x*x+z*z), 2 * (y*z + w*x), 0,
|
||||||
|
2 * (x*z + w*y), 2 * (y*z - w*x), 1 - 2*(x*x+y*y), 0,
|
||||||
|
float64(frame.Displacement[0]), float64(frame.Displacement[1]), float64(frame.Displacement[2]), 1,
|
||||||
|
}
|
||||||
|
for axis, scale := range frame.Scale {
|
||||||
|
for component := range 3 {
|
||||||
|
matrix[axis*4+component] *= float64(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matrix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系
|
||||||
|
// 入参: state 动作轨道状态
|
||||||
|
// 返回: [16]float64 节点变换, error 引用、数值或取消错误
|
||||||
|
func (r *u3dRender) trackMatrix(state U3DTrackState) ([16]float64, error) {
|
||||||
|
var track *U3DMotionTrack
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return [16]float64{}, err
|
||||||
|
}
|
||||||
|
if index, exists := r.motionIndex[state.Motion]; exists {
|
||||||
|
motion := &r.model.Motions[index]
|
||||||
|
for i := range motion.Tracks {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return [16]float64{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if motion.Tracks[i].Name == state.Track {
|
||||||
|
if track != nil {
|
||||||
|
return [16]float64{}, fmt.Errorf("ambiguous U3D motion track %q", state.Track)
|
||||||
|
}
|
||||||
|
track = &motion.Tracks[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if track == nil {
|
||||||
|
return [16]float64{}, fmt.Errorf("missing U3D motion track %q in %q", state.Track, state.Motion)
|
||||||
|
}
|
||||||
|
frame, err := track.Sample(r.ctx, state.Time, state.Loop)
|
||||||
|
if err != nil {
|
||||||
|
return [16]float64{}, err
|
||||||
|
}
|
||||||
|
return frame.Matrix()
|
||||||
|
}
|
||||||
+70
-32
@@ -174,42 +174,42 @@ func u3dTriangleNormal(points [3]u3dVector) u3dVector {
|
|||||||
return normal.unit()
|
return normal.unit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// instances 按拓扑顺序构建全部父路径实例,限制共享层级的指数展开
|
// sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景
|
||||||
// 入参: limit 最大实例数
|
// 返回: []int 拓扑顺序, map[string]int 节点索引, error 结构、预算或取消错误
|
||||||
// 返回: error 结构、预算、实例上限或取消错误
|
func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) {
|
||||||
func (r *u3dRender) instances(limit int) error {
|
|
||||||
nodes := r.model.Nodes
|
nodes := r.model.Nodes
|
||||||
if err := r.reserve(uint64(len(nodes)), 160); err != nil {
|
if err := r.reserve(uint64(len(nodes)), 161); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
index := make(map[string]int, len(nodes))
|
index := make(map[string]int, len(nodes))
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
if i%256 == 0 {
|
if i%256 == 0 {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, exists := index[node.Name]; exists || node.Name == "" {
|
if _, exists := index[node.Name]; exists || node.Name == "" {
|
||||||
return fmt.Errorf("invalid U3D render node name")
|
return nil, nil, fmt.Errorf("invalid U3D render node name")
|
||||||
}
|
}
|
||||||
switch node.Kind {
|
switch node.Kind {
|
||||||
case "Group", "Model", "Light", "View":
|
case "Group", "Model", "Light", "View":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported U3D render node kind")
|
return nil, nil, fmt.Errorf("unsupported U3D render node kind")
|
||||||
}
|
}
|
||||||
index[node.Name] = i
|
index[node.Name] = i
|
||||||
}
|
}
|
||||||
counts := make([]int, len(nodes))
|
counts := make([]int, len(nodes))
|
||||||
children := make([][]int, len(nodes))
|
children := make([][]int, len(nodes))
|
||||||
r.byNode = make([][]int, len(nodes))
|
r.byNode = make([][]int, len(nodes))
|
||||||
|
r.connected = make([]bool, len(nodes))
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil {
|
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for k, parent := range node.Parents {
|
for k, parent := range node.Parents {
|
||||||
if k%256 == 0 {
|
if k%256 == 0 {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if j, ok := index[parent.Name]; ok {
|
if j, ok := index[parent.Name]; ok {
|
||||||
@@ -222,7 +222,7 @@ func (r *u3dRender) instances(limit int) error {
|
|||||||
for i, n := range counts {
|
for i, n := range counts {
|
||||||
if i%256 == 0 {
|
if i%256 == 0 {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -231,9 +231,54 @@ func (r *u3dRender) instances(limit int) error {
|
|||||||
}
|
}
|
||||||
for cursor := 0; cursor < len(queue); cursor++ {
|
for cursor := 0; cursor < len(queue); cursor++ {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
i := queue[cursor]
|
i := queue[cursor]
|
||||||
|
for j, parent := range nodes[i].Parents {
|
||||||
|
if j%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parent.Name == "" {
|
||||||
|
r.connected[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if p, exists := index[parent.Name]; exists && r.connected[p] {
|
||||||
|
r.connected[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for childIndex, child := range children[i] {
|
||||||
|
if childIndex%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counts[child]--
|
||||||
|
if counts[child] == 0 {
|
||||||
|
queue = append(queue, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(queue) != len(nodes) {
|
||||||
|
return nil, nil, fmt.Errorf("cyclic U3D node parents")
|
||||||
|
}
|
||||||
|
return queue, index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根
|
||||||
|
// 入参: order 拓扑顺序, index 节点索引, limit 最大实例数
|
||||||
|
// 返回: error 变换、预算、实例上限或取消错误
|
||||||
|
func (r *u3dRender) instances(order []int, index map[string]int, limit int) error {
|
||||||
|
nodes := r.model.Nodes
|
||||||
|
for _, i := range order {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !r.connected[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
state := r.states[nodes[i].Name]
|
state := r.states[nodes[i].Name]
|
||||||
for parentIndex, parent := range nodes[i].Parents {
|
for parentIndex, parent := range nodes[i].Parents {
|
||||||
if parentIndex%256 == 0 {
|
if parentIndex%256 == 0 {
|
||||||
@@ -241,20 +286,27 @@ func (r *u3dRender) instances(limit int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parents := []int{-1}
|
||||||
|
if parent.Name != "" {
|
||||||
|
j, exists := index[parent.Name]
|
||||||
|
if !exists || !r.connected[j] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parents = r.byNode[j]
|
||||||
|
}
|
||||||
var local u3dMatrix
|
var local u3dMatrix
|
||||||
for k, v := range parent.Transform {
|
for k, v := range parent.Transform {
|
||||||
local[k] = float64(v)
|
local[k] = float64(v)
|
||||||
}
|
}
|
||||||
|
if animation, exists := r.animated[nodes[i].Name]; exists {
|
||||||
|
local = local.multiply(animation.matrix)
|
||||||
|
}
|
||||||
if state.Matrix != nil {
|
if state.Matrix != nil {
|
||||||
local = *state.Matrix
|
local = *state.Matrix
|
||||||
}
|
}
|
||||||
if !local.valid() {
|
if !local.valid() {
|
||||||
return fmt.Errorf("unsupported U3D non-affine node transform")
|
return fmt.Errorf("unsupported U3D non-affine node transform")
|
||||||
}
|
}
|
||||||
parents := []int{-1}
|
|
||||||
if j, ok := index[parent.Name]; ok {
|
|
||||||
parents = r.byNode[j]
|
|
||||||
}
|
|
||||||
for _, p := range parents {
|
for _, p := range parents {
|
||||||
if len(r.scene) >= limit {
|
if len(r.scene) >= limit {
|
||||||
return fmt.Errorf("U3D instance count exceeds limit")
|
return fmt.Errorf("U3D instance count exceeds limit")
|
||||||
@@ -284,20 +336,6 @@ func (r *u3dRender) instances(limit int) error {
|
|||||||
r.scene = append(r.scene, u3dInstance{node: i, parent: p, world: world, opacity: opacity, visibility: visibility})
|
r.scene = append(r.scene, u3dInstance{node: i, parent: p, world: world, opacity: opacity, visibility: visibility})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for childIndex, child := range children[i] {
|
|
||||||
if childIndex%256 == 0 {
|
|
||||||
if err := r.ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
counts[child]--
|
|
||||||
if counts[child] == 0 {
|
|
||||||
queue = append(queue, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(queue) != len(nodes) {
|
return r.ctx.Err()
|
||||||
return fmt.Errorf("cyclic U3D node parents")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
@@ -44,10 +44,10 @@ func (r *u3dRender) prepareSections(sections []U3DCrossSection) error {
|
|||||||
if len(sections) == 0 {
|
if len(sections) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := r.reserve(uint64(len(sections)), 352); err != nil {
|
if err := r.reserve(uint64(len(sections)), 1152); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := r.reserve(uint64(len(sections))+3, 112); err != nil {
|
if err := r.reserve(uint64(len(sections))+3, 512); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.sections = make([]u3dSection, len(sections))
|
r.sections = make([]u3dSection, len(sections))
|
||||||
@@ -140,11 +140,10 @@ func (r *u3dRender) sceneDiagonal() (float64, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
node := r.model.Nodes[instance.node]
|
node := r.model.Nodes[instance.node]
|
||||||
index, exists := r.meshes[node.Resource]
|
mesh := r.nodeMesh(&node)
|
||||||
if node.Kind != "Model" || !exists {
|
if node.Kind != "Model" || mesh == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mesh := &r.model.Meshes[index]
|
|
||||||
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
|
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
|
||||||
_, corners := mesh.primitive(i)
|
_, corners := mesh.primitive(i)
|
||||||
if i%256 == 0 {
|
if i%256 == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DTextureLayer 保存单个着色器纹理通道,矩阵按列排列
|
||||||
|
// Blend依次以0至3表示相乘、相加、替换及线性混合,BlendSource为1时使用BlendConstant
|
||||||
|
// Mode依次以0至4表示模型坐标、平面、圆柱、球面及反射映射
|
||||||
|
// Alpha控制是否使用纹理透明度,Repeat低两位分别启用横向及纵向平铺
|
||||||
|
// 平面映射直接取参考位置的XY;圆柱以Z为纵坐标,圆柱与球面的经线从正X轴朝正Y轴递增
|
||||||
|
// 球面纬度从正Z极到负Z极映射为0至1;反射映射使用相机空间球面环境图,图像纵坐标向下
|
||||||
|
type U3DTextureLayer struct {
|
||||||
|
Texture string
|
||||||
|
Intensity, BlendConstant float32
|
||||||
|
Blend, BlendSource uint8
|
||||||
|
Mode, Repeat uint8
|
||||||
|
Alpha bool
|
||||||
|
Transform, WrapTransform [16]float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dTextureSet 保存一次绘制内共用的有序纹理层
|
||||||
|
type u3dTextureSet struct {
|
||||||
|
layers [8]*U3DTextureLayer
|
||||||
|
images [8]*image.NRGBA64
|
||||||
|
wrap [8]*[16]float64
|
||||||
|
normal u3dMatrix
|
||||||
|
mask uint8
|
||||||
|
reflection, perspective bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码
|
||||||
|
// 入参: shader 着色器
|
||||||
|
// 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误
|
||||||
|
func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
|
||||||
|
if shader.Textures == ([8]*U3DTextureLayer{}) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cached := r.textureSets[shader.Textures]; cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 512); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
set := &u3dTextureSet{layers: shader.Textures}
|
||||||
|
for i, layer := range set.layers {
|
||||||
|
if layer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 || !finite(float64(layer.Intensity)) || !finite(float64(layer.BlendConstant)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D texture layer")
|
||||||
|
}
|
||||||
|
for _, matrix := range [2][16]float32{layer.Transform, layer.WrapTransform} {
|
||||||
|
for _, value := range matrix {
|
||||||
|
if !finite(float64(value)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D texture transform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if layer.Mode >= 1 && layer.Mode <= 3 {
|
||||||
|
if err := r.reserve(1, 256); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inverse, err := u3dTextureInverse(layer.WrapTransform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
set.wrap[i] = &inverse
|
||||||
|
}
|
||||||
|
if layer.Mode == 4 && !set.reflection {
|
||||||
|
var ok bool
|
||||||
|
set.normal, ok = r.toCamera.inverse()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid U3D reflection camera")
|
||||||
|
}
|
||||||
|
set.reflection, set.perspective = true, r.camera.Perspective
|
||||||
|
}
|
||||||
|
img, err := r.texture(layer.Texture)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
set.images[i] = img
|
||||||
|
set.mask |= 1 << i
|
||||||
|
}
|
||||||
|
if r.textureSets == nil {
|
||||||
|
r.textureSets = make(map[[8]*U3DTextureLayer]*u3dTextureSet)
|
||||||
|
}
|
||||||
|
r.textureSets[shader.Textures] = set
|
||||||
|
return set, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// coordinates 保存模型纹理坐标或程序化映射的插值输入,避免经线两侧直接插值
|
||||||
|
// 入参: vertex 目标顶点, mesh 几何资源, shading 属性布局, corner 属性索引, normal 世界法线
|
||||||
|
// 返回: error 坐标、索引或维度错误
|
||||||
|
func (s *u3dTextureSet) coordinates(vertex *u3dVertex, mesh *U3DMesh, shading U3DShading, corner U3DCorner, normal u3dVector) error {
|
||||||
|
if vertex.textureMask != 0 {
|
||||||
|
clear(vertex.texture[:])
|
||||||
|
}
|
||||||
|
vertex.textureMask = 0
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
vertex.textureMask = s.mask
|
||||||
|
for i, layer := range s.layers {
|
||||||
|
if layer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if layer.Mode != 0 {
|
||||||
|
if layer.Mode == 4 {
|
||||||
|
vertex.texture[i] = s.normal.normal(normal)
|
||||||
|
if vertex.texture[i] == ([3]float64{}) {
|
||||||
|
return fmt.Errorf("invalid U3D reflection normal")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
|
||||||
|
return fmt.Errorf("invalid U3D texture position index")
|
||||||
|
}
|
||||||
|
for c, value := range mesh.Positions[corner.Position] {
|
||||||
|
if !finite(float64(value)) {
|
||||||
|
return fmt.Errorf("invalid U3D texture position")
|
||||||
|
}
|
||||||
|
vertex.texture[i][c] = float64(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i >= len(shading.TextureDimensions) || uint64(corner.Texture[i]) >= uint64(len(mesh.TextureCoordinates)) {
|
||||||
|
return fmt.Errorf("invalid U3D texture coordinate index")
|
||||||
|
}
|
||||||
|
dimension := shading.TextureDimensions[i]
|
||||||
|
if dimension < 1 || dimension > 4 {
|
||||||
|
return fmt.Errorf("invalid U3D texture coordinate dimensions")
|
||||||
|
}
|
||||||
|
value := [4]float64{0, 0, 0, 1}
|
||||||
|
for component := range dimension {
|
||||||
|
v := float64(mesh.TextureCoordinates[corner.Texture[i]][component])
|
||||||
|
if !finite(v) {
|
||||||
|
return fmt.Errorf("invalid U3D texture coordinate")
|
||||||
|
}
|
||||||
|
value[component] = v
|
||||||
|
}
|
||||||
|
for component, row := range [3]int{0, 1, 3} {
|
||||||
|
total := 0.0
|
||||||
|
for col, v := range value {
|
||||||
|
total += float64(layer.Transform[col*4+row]) * v
|
||||||
|
}
|
||||||
|
if !finite(total) {
|
||||||
|
return fmt.Errorf("invalid U3D transformed texture coordinate")
|
||||||
|
}
|
||||||
|
vertex.texture[i][component] = total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度
|
||||||
|
// 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度
|
||||||
|
// 返回: error 纹理映射或混合错误
|
||||||
|
func (r *u3dRender) textureColor(textures *u3dTextureSet, color *[4]float64, coordinates *[8][3]float64, position u3dVector, override bool) error {
|
||||||
|
if textures == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
opacity := color[3]
|
||||||
|
if err := textures.color(color, coordinates, position); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if override {
|
||||||
|
color[3] = opacity
|
||||||
|
}
|
||||||
|
if r.opacity >= 0 {
|
||||||
|
color[3] *= r.opacity
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// color 按通道顺序合成纹理,在透明度覆盖、透明测试和雾效之前执行
|
||||||
|
// 入参: color 未预乘底色, coordinates 插值后的纹理输入, position 相机空间位置
|
||||||
|
// 返回: error 无效投影坐标或混合结果
|
||||||
|
func (s *u3dTextureSet) color(color *[4]float64, coordinates *[8][3]float64, position u3dVector) error {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i, layer := range s.layers {
|
||||||
|
if layer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uv := coordinates[i]
|
||||||
|
if layer.Mode != 0 {
|
||||||
|
var err error
|
||||||
|
uv, err = s.generated(i, uv, position)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u, v := uv[0]/uv[2], uv[1]/uv[2]
|
||||||
|
if !finite(u) || !finite(v) {
|
||||||
|
return fmt.Errorf("invalid U3D projected texture coordinate")
|
||||||
|
}
|
||||||
|
sample := u3dShaderSample(s.images[i], u, v, layer.Repeat, layer.Alpha)
|
||||||
|
for c := range 3 {
|
||||||
|
sample[c] *= float64(layer.Intensity)
|
||||||
|
}
|
||||||
|
weight := sample[3]
|
||||||
|
if layer.BlendSource == 1 {
|
||||||
|
weight = min(1, max(0, float64(layer.BlendConstant)))
|
||||||
|
}
|
||||||
|
for c := range color {
|
||||||
|
switch layer.Blend {
|
||||||
|
case 0:
|
||||||
|
color[c] *= sample[c]
|
||||||
|
case 1:
|
||||||
|
color[c] += sample[c]
|
||||||
|
case 2:
|
||||||
|
color[c] = sample[c]
|
||||||
|
case 3:
|
||||||
|
color[c] = sample[c]*weight + color[c]*(1-weight)
|
||||||
|
}
|
||||||
|
if !finite(color[c]) {
|
||||||
|
return fmt.Errorf("invalid U3D texture blend result")
|
||||||
|
}
|
||||||
|
color[c] = min(1, max(0, color[c]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dShaderSample 按归一化坐标采样纹理,平铺时跨边界插值,未平铺时钳制边缘
|
||||||
|
// 全透明采样保留原始RGB,供相加、固定权重混合及透明度覆盖使用
|
||||||
|
// 入参: texture 纹理, u 横坐标, v 纵坐标, repeat 平铺位图, alpha 是否使用透明度
|
||||||
|
// 返回: [4]float64 未预乘颜色及透明度
|
||||||
|
func u3dShaderSample(texture *image.NRGBA64, u, v float64, repeat uint8, alpha bool) [4]float64 {
|
||||||
|
if repeat&1 != 0 {
|
||||||
|
u -= math.Floor(u)
|
||||||
|
} else {
|
||||||
|
u = min(1, max(0, u))
|
||||||
|
}
|
||||||
|
if repeat&2 != 0 {
|
||||||
|
v -= math.Floor(v)
|
||||||
|
} else {
|
||||||
|
v = min(1, max(0, v))
|
||||||
|
}
|
||||||
|
w, h := texture.Rect.Dx(), texture.Rect.Dy()
|
||||||
|
x, y := u*float64(w)-.5, v*float64(h)-.5
|
||||||
|
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
|
||||||
|
fx, fy := x-float64(x0), y-float64(y0)
|
||||||
|
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
|
||||||
|
var result [4]float64
|
||||||
|
var transparent [3]float64
|
||||||
|
for row := range 2 {
|
||||||
|
py := min(h-1, max(0, y0+row))
|
||||||
|
if repeat&2 != 0 {
|
||||||
|
py = (y0 + row + h) % h
|
||||||
|
}
|
||||||
|
for col := range 2 {
|
||||||
|
px := min(w-1, max(0, x0+col))
|
||||||
|
if repeat&1 != 0 {
|
||||||
|
px = (x0 + col + w) % w
|
||||||
|
}
|
||||||
|
c := texture.NRGBA64At(px, py)
|
||||||
|
a := 1.0
|
||||||
|
if alpha {
|
||||||
|
a = float64(c.A) / 65535
|
||||||
|
}
|
||||||
|
weight := wx[col] * wy[row]
|
||||||
|
if a == 0 {
|
||||||
|
transparent[0] += float64(c.R) / 65535 * weight
|
||||||
|
transparent[1] += float64(c.G) / 65535 * weight
|
||||||
|
transparent[2] += float64(c.B) / 65535 * weight
|
||||||
|
}
|
||||||
|
result[0] += float64(c.R) / 65535 * a * weight
|
||||||
|
result[1] += float64(c.G) / 65535 * a * weight
|
||||||
|
result[2] += float64(c.B) / 65535 * a * weight
|
||||||
|
result[3] += a * weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result[3] != 0 {
|
||||||
|
for c := range 3 {
|
||||||
|
result[c] /= result[3]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy(result[:3], transparent[:])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DBone 保存相对父骨骼的初始位姿,Orientation按W、X、Y、Z排列
|
||||||
|
// Attributes标志决定链接、关节字段是否存在,以及各轴旋转约束是否启用和限幅
|
||||||
|
// RotationConstraints按X、Y、Z轴排列,每项依次为上限、下限;未指定骨骼姿态时不变形
|
||||||
|
type U3DBone struct {
|
||||||
|
Name, Parent string
|
||||||
|
Attributes uint32
|
||||||
|
Length float32
|
||||||
|
Displacement [3]float32
|
||||||
|
Orientation [4]float32
|
||||||
|
LinkCount uint32
|
||||||
|
LinkLength float32
|
||||||
|
StartJoint, EndJoint U3DBoneJoint
|
||||||
|
RotationConstraints [3][2]float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DBoneJoint 保存骨骼局部坐标系内关节椭圆的中心偏移和两轴尺寸
|
||||||
|
type U3DBoneJoint struct {
|
||||||
|
Center, Scale [2]float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// skeleton 按ECMA-363第9.6.1.1.6节读取骨架,三角网格、点集和线集共用
|
||||||
|
// 入参: r 字段读取器
|
||||||
|
// 返回: []U3DBone 按父项在前排列的骨骼, error 结构、预算或取消错误
|
||||||
|
func (d *u3dDecoder) skeleton(r *u3dValues) ([]U3DBone, error) {
|
||||||
|
count := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if uint64(count) > uint64(len(r.data)-r.pos)/64 {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 256); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bones := make([]U3DBone, int(count))
|
||||||
|
names := make(map[string]bool, int(count))
|
||||||
|
for index := range bones {
|
||||||
|
if index%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bone := &bones[index]
|
||||||
|
bone.Name, bone.Parent = r.text(), r.text()
|
||||||
|
bone.Attributes, bone.Length = r.u32(), r.f32()
|
||||||
|
for component := range bone.Displacement {
|
||||||
|
bone.Displacement[component] = r.f32()
|
||||||
|
}
|
||||||
|
for component := range bone.Orientation {
|
||||||
|
bone.Orientation[component] = r.f32()
|
||||||
|
}
|
||||||
|
if bone.Attributes&1 != 0 {
|
||||||
|
bone.LinkCount, bone.LinkLength = r.u32(), r.f32()
|
||||||
|
}
|
||||||
|
if bone.Attributes&2 != 0 {
|
||||||
|
for _, joint := range []*U3DBoneJoint{&bone.StartJoint, &bone.EndJoint} {
|
||||||
|
joint.Center = [2]float32{r.f32(), r.f32()}
|
||||||
|
joint.Scale = [2]float32{r.f32(), r.f32()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for axis := range bone.RotationConstraints {
|
||||||
|
bone.RotationConstraints[axis] = [2]float32{r.f32(), r.f32()}
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
if bone.Attributes & ^uint32(255) != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone attributes")
|
||||||
|
}
|
||||||
|
if names[bone.Name] {
|
||||||
|
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
|
||||||
|
}
|
||||||
|
if index == 0 && bone.Parent != "" || index != 0 && !names[bone.Parent] {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
|
||||||
|
}
|
||||||
|
names[bone.Name] = true
|
||||||
|
}
|
||||||
|
return bones, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DBoneState 按骨骼名称指定相对父骨骼末端的姿态,根骨骼相对模型原点
|
||||||
|
// Frame可由动作轨道采样得到,Time不参与变形;单位缩放需显式设为[3]float32{1, 1, 1}
|
||||||
|
// 未覆盖项沿用文件或动画求值后的局部姿态,重复名称使用最后一项,不施加逆运动学约束
|
||||||
|
type U3DBoneState struct {
|
||||||
|
Name string
|
||||||
|
Frame U3DKeyFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dSkinBone 保存初始姿态到当前姿态的变换及其逆矩阵
|
||||||
|
type u3dSkinBone struct {
|
||||||
|
matrix, inverse u3dMatrix
|
||||||
|
invertible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dSkinPose 保存同一几何最近求值的姿态和权重,仅用于本次绘制
|
||||||
|
type u3dSkinPose struct {
|
||||||
|
weights *U3DBoneWeights
|
||||||
|
states []U3DBoneState
|
||||||
|
mesh *U3DMesh
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源
|
||||||
|
// 入参: node 模型节点
|
||||||
|
// 返回: *U3DMesh 几何,资源缺失时为空
|
||||||
|
func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh {
|
||||||
|
if mesh := r.posed[node.Name]; mesh != nil {
|
||||||
|
return mesh
|
||||||
|
}
|
||||||
|
if index, exists := r.meshes[node.Resource]; exists {
|
||||||
|
return &r.model.Meshes[index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何
|
||||||
|
// 返回: error 骨架、权重、预算或取消错误
|
||||||
|
func (r *u3dRender) poseNodes() error {
|
||||||
|
if len(r.states) == 0 && len(r.animated) == 0 {
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
var cache map[*U3DMesh]u3dSkinPose
|
||||||
|
var cacheBytes uint64
|
||||||
|
defer func() { r.remaining += cacheBytes }()
|
||||||
|
for i := range r.model.Nodes {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.connected != nil && !r.connected[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node := &r.model.Nodes[i]
|
||||||
|
states := r.states[node.Name].Bones
|
||||||
|
animation := r.animated[node.Name]
|
||||||
|
if len(states) == 0 && len(animation.bones) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mesh := r.nodeMesh(node)
|
||||||
|
if node.Kind != "Model" || mesh == nil {
|
||||||
|
return fmt.Errorf("missing U3D bone state geometry for %q", node.Name)
|
||||||
|
}
|
||||||
|
if r.posed == nil {
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.posed = make(map[string]*U3DMesh)
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 128); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var temporary uint64
|
||||||
|
if len(animation.bones) != 0 {
|
||||||
|
if len(states) == 0 {
|
||||||
|
states = animation.bones
|
||||||
|
} else {
|
||||||
|
count := uint64(len(animation.bones)) + uint64(len(states))
|
||||||
|
if err := r.reserve(count, 64); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temporary = count * 64
|
||||||
|
combined := make([]U3DBoneState, len(animation.bones), int(count))
|
||||||
|
copy(combined, animation.bones)
|
||||||
|
states = append(combined, states...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weights := animation.weights
|
||||||
|
if weights == nil {
|
||||||
|
var err error
|
||||||
|
weights, err = r.skinWeightSource(mesh, node.BoneWeights, math.MaxUint64, math.MaxUint64)
|
||||||
|
if err != nil {
|
||||||
|
r.remaining += temporary
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous, exists := cache[mesh]
|
||||||
|
shared := exists && previous.weights == weights && len(previous.states) == len(states)
|
||||||
|
if shared {
|
||||||
|
for j := range states {
|
||||||
|
if j%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
r.remaining += temporary
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if states[j] != previous.states[j] {
|
||||||
|
shared = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shared {
|
||||||
|
r.posed[node.Name] = previous.mesh
|
||||||
|
r.remaining += temporary
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
source, sets := mesh, node.BoneWeights
|
||||||
|
if animation.weights != nil {
|
||||||
|
geometry := *mesh
|
||||||
|
geometry.BoneWeights = []U3DBoneWeights{*animation.weights}
|
||||||
|
mesh, sets = &geometry, nil
|
||||||
|
}
|
||||||
|
posed, err := r.skinMesh(mesh, sets, states)
|
||||||
|
if err != nil {
|
||||||
|
r.remaining += temporary
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.posed[node.Name] = posed
|
||||||
|
if len(r.model.Nodes) > 1 {
|
||||||
|
if cache == nil {
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheBytes += 1024
|
||||||
|
cache = make(map[*U3DMesh]u3dSkinPose)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
if err := r.reserve(1, 128); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheBytes += 128
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(states)), 64); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheBytes += uint64(len(states)) * 64
|
||||||
|
saved := make([]U3DBoneState, len(states))
|
||||||
|
copy(saved, states)
|
||||||
|
cache[source] = u3dSkinPose{weights: weights, states: saved, mesh: posed}
|
||||||
|
released := uint64(len(previous.states)) * 64
|
||||||
|
r.remaining += released
|
||||||
|
cacheBytes -= released
|
||||||
|
}
|
||||||
|
r.remaining += temporary
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态
|
||||||
|
// 入参: bones 按父项在前排列的骨架, states 显式姿态
|
||||||
|
// 返回: []u3dSkinBone 变形矩阵, error 骨架、姿态或取消错误
|
||||||
|
func (r *u3dRender) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3dSkinBone, error) {
|
||||||
|
index := make(map[string]int, len(bones))
|
||||||
|
for i, bone := range bones {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, exists := index[bone.Name]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
|
||||||
|
}
|
||||||
|
index[bone.Name] = i
|
||||||
|
}
|
||||||
|
overrides := make(map[int]U3DKeyFrame, min(len(states), len(bones)))
|
||||||
|
for i, state := range states {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bone, exists := index[state.Name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("missing U3D bone %q", state.Name)
|
||||||
|
}
|
||||||
|
overrides[bone] = state.Frame
|
||||||
|
}
|
||||||
|
bind, pose := make([]u3dMatrix, len(bones)), make([]u3dMatrix, len(bones))
|
||||||
|
result := make([]u3dSkinBone, len(bones))
|
||||||
|
for i, bone := range bones {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !finite(float64(bone.Length)) || bone.Length < 0 || bone.Attributes & ^uint32(255) != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone parameters")
|
||||||
|
}
|
||||||
|
if bone.Attributes&1 != 0 && bone.LinkCount != 0 {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D bone link deformation")
|
||||||
|
}
|
||||||
|
initial := U3DKeyFrame{Displacement: bone.Displacement, Rotation: bone.Orientation, Scale: [3]float32{1, 1, 1}}
|
||||||
|
base, err := initial.Matrix()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
current := base
|
||||||
|
if frame, exists := overrides[i]; exists {
|
||||||
|
current, err = frame.Matrix()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bind[i], pose[i] = u3dMatrix(base), u3dMatrix(current)
|
||||||
|
if i == 0 {
|
||||||
|
if bone.Parent != "" {
|
||||||
|
return nil, fmt.Errorf("invalid U3D root bone parent")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent, exists := index[bone.Parent]
|
||||||
|
if !exists || parent >= i {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
|
||||||
|
}
|
||||||
|
end := u3dIdentity()
|
||||||
|
end[12] = float64(bones[parent].Length)
|
||||||
|
bind[i] = bind[parent].multiply(end).multiply(bind[i])
|
||||||
|
pose[i] = pose[parent].multiply(end).multiply(pose[i])
|
||||||
|
}
|
||||||
|
inverse, ok := bind[i].inverse()
|
||||||
|
if !ok || !pose[i].valid() {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone transform")
|
||||||
|
}
|
||||||
|
result[i].matrix = pose[i].multiply(inverse)
|
||||||
|
if !result[i].matrix.valid() {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone deformation")
|
||||||
|
}
|
||||||
|
result[i].inverse, result[i].invertible = result[i].matrix.inverse()
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链
|
||||||
|
// 入参: mesh 原始几何, sets 节点权重
|
||||||
|
// 返回: *U3DBoneWeights 有效权重, error 类型、引用或取消错误
|
||||||
|
func (r *u3dRender) skinWeights(mesh *U3DMesh, sets []U3DBoneWeights) (*U3DBoneWeights, error) {
|
||||||
|
return r.skinWeightSet(mesh, sets, math.MaxUint64, math.MaxUint64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出
|
||||||
|
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
|
||||||
|
// 返回: *U3DBoneWeights 权重来源,内容尚未校验, error 类型、引用或取消错误
|
||||||
|
func (r *u3dRender) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
|
||||||
|
kind := uint32(0)
|
||||||
|
for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} {
|
||||||
|
if count != 0 {
|
||||||
|
if kind != 0 {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D mixed geometry deformation")
|
||||||
|
}
|
||||||
|
kind = 1 << i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var selected *U3DBoneWeights
|
||||||
|
for chain, group := range [][]U3DBoneWeights{mesh.BoneWeights, sets} {
|
||||||
|
end := resourceEnd
|
||||||
|
if chain != 0 {
|
||||||
|
end = nodeEnd
|
||||||
|
}
|
||||||
|
var last *U3DBoneWeights
|
||||||
|
for i := range group {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uint64(group[i].Index) < end && group[i].Attributes == kind && (last == nil || group[i].Index > last.Index) {
|
||||||
|
last = &group[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last != nil {
|
||||||
|
selected = last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D deformation without bone weights")
|
||||||
|
}
|
||||||
|
return selected, r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// skinWeightSet 选取并校验指定修饰链位置之前的权重
|
||||||
|
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
|
||||||
|
// 返回: *U3DBoneWeights 有效权重, error 类型、引用、数值或取消错误
|
||||||
|
func (r *u3dRender) skinWeightSet(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
|
||||||
|
selected, err := r.skinWeightSource(mesh, sets, resourceEnd, nodeEnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if selected.Index == 0 || len(selected.Offsets) == 0 || len(selected.Offsets)-1 > len(mesh.Positions) || selected.Offsets[0] != 0 || uint64(selected.Offsets[len(selected.Offsets)-1]) != uint64(len(selected.Weights)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone weight offsets")
|
||||||
|
}
|
||||||
|
for i := 1; i < len(selected.Offsets); i++ {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
start, end := selected.Offsets[i-1], selected.Offsets[i]
|
||||||
|
if end < start || uint64(end) > uint64(len(selected.Weights)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone weight range")
|
||||||
|
}
|
||||||
|
var total float64
|
||||||
|
for j, weight := range selected.Weights[start:end] {
|
||||||
|
if j%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uint64(weight.Bone) >= uint64(len(mesh.Bones)) || !finite(float64(weight.Weight)) || weight.Weight < 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone weight")
|
||||||
|
}
|
||||||
|
total += float64(weight.Weight)
|
||||||
|
}
|
||||||
|
if start != end && math.Abs(total-1) > 1e-6 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D bone weight sum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// influence 返回指定位置的骨骼权重,未提供权重的位置保持不变
|
||||||
|
// 入参: position 作者位置索引
|
||||||
|
// 返回: []U3DBoneWeight 影响列表
|
||||||
|
func (weights *U3DBoneWeights) influence(position uint32) []U3DBoneWeight {
|
||||||
|
if uint64(position)+1 >= uint64(len(weights.Offsets)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return weights.Weights[weights.Offsets[position]:weights.Offsets[position+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化
|
||||||
|
// 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线
|
||||||
|
// 返回: [3]float32 变形结果, error 数值、奇异变换或取消错误
|
||||||
|
func (r *u3dRender) skinVector(value [3]float32, weights []U3DBoneWeight, bones []u3dSkinBone, normal bool) ([3]float32, error) {
|
||||||
|
input := u3dVector{float64(value[0]), float64(value[1]), float64(value[2])}
|
||||||
|
for _, component := range input {
|
||||||
|
if !finite(component) {
|
||||||
|
return [3]float32{}, fmt.Errorf("invalid U3D skin vector")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(weights) == 0 {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
var result u3dVector
|
||||||
|
for i, weight := range weights {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return [3]float32{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if weight.Weight == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bone := &bones[weight.Bone]
|
||||||
|
v := bone.matrix.point(input)
|
||||||
|
if normal {
|
||||||
|
if !bone.invertible {
|
||||||
|
return [3]float32{}, fmt.Errorf("unsupported U3D singular bone normal transform")
|
||||||
|
}
|
||||||
|
m := bone.inverse
|
||||||
|
v = u3dVector{m[0]*input[0] + m[1]*input[1] + m[2]*input[2], m[4]*input[0] + m[5]*input[1] + m[6]*input[2], m[8]*input[0] + m[9]*input[1] + m[10]*input[2]}
|
||||||
|
}
|
||||||
|
for axis := range result {
|
||||||
|
result[axis] += v[axis] * float64(weight.Weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, component := range result {
|
||||||
|
if !finite(component) {
|
||||||
|
return [3]float32{}, fmt.Errorf("invalid U3D skin result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normal {
|
||||||
|
result = result.unit()
|
||||||
|
}
|
||||||
|
for axis := range value {
|
||||||
|
value[axis] = float32(result[axis])
|
||||||
|
if !finite(float64(value[axis])) {
|
||||||
|
return [3]float32{}, fmt.Errorf("U3D skin result exceeds range")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引
|
||||||
|
// 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态
|
||||||
|
// 返回: *U3DMesh 独立姿态几何, error 结构、能力、预算或取消错误
|
||||||
|
func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DBoneState) (*U3DMesh, error) {
|
||||||
|
weights, err := r.skinWeights(mesh, sets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(mesh.Bones)), 768); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(states)), 128); err != nil {
|
||||||
|
r.remaining += uint64(len(mesh.Bones)) * 768
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
temporary := uint64(len(mesh.Bones))*768 + uint64(len(states))*128
|
||||||
|
defer func() { r.remaining += temporary }()
|
||||||
|
bones, err := r.boneTransforms(mesh.Bones, states)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 768); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(mesh.Positions)), 12); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := *mesh
|
||||||
|
result.Positions = make([][3]float32, len(mesh.Positions))
|
||||||
|
for i, position := range mesh.Positions {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Positions[i], err = r.skinVector(position, weights.influence(uint32(i)), bones, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mesh.ExcludeNormals {
|
||||||
|
return &result, r.ctx.Err()
|
||||||
|
}
|
||||||
|
bytes := uint64(len(mesh.Faces))*148 + uint64(len(mesh.Lines))*100 + uint64(len(mesh.Points))*52
|
||||||
|
if err := r.reserve(bytes, 1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points)
|
||||||
|
result.Normals = nil
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
index := make(map[[2]uint32]uint32)
|
||||||
|
defer func() { r.remaining += 1024 + uint64(len(index))*96 }()
|
||||||
|
for i := range len(result.Faces) + len(result.Lines) + len(result.Points) {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, corners := result.primitive(i)
|
||||||
|
for j := range corners {
|
||||||
|
corner := &corners[j]
|
||||||
|
if uint64(corner.Position) >= uint64(len(mesh.Positions)) || uint64(corner.Normal) >= uint64(len(mesh.Normals)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D skin corner index")
|
||||||
|
}
|
||||||
|
key := [2]uint32{corner.Position, corner.Normal}
|
||||||
|
normal, exists := index[key]
|
||||||
|
if !exists {
|
||||||
|
if err := r.reserve(1, 96); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(result.Normals) == cap(result.Normals) {
|
||||||
|
capacity := max(16, uint64(cap(result.Normals))*2)
|
||||||
|
if capacity > math.MaxUint32 {
|
||||||
|
return nil, fmt.Errorf("U3D skin normal count exceeds limit")
|
||||||
|
}
|
||||||
|
if err := r.reserve(capacity, 12); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
grown := make([][3]float32, len(result.Normals), int(capacity))
|
||||||
|
copy(grown, result.Normals)
|
||||||
|
r.remaining += uint64(cap(result.Normals)) * 12
|
||||||
|
result.Normals = grown
|
||||||
|
}
|
||||||
|
value, err := r.skinVector(mesh.Normals[corner.Normal], weights.influence(corner.Position), bones, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
normal = uint32(len(result.Normals))
|
||||||
|
index[key] = normal
|
||||||
|
result.Normals = append(result.Normals, value)
|
||||||
|
}
|
||||||
|
corner.Normal = normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &result, r.ctx.Err()
|
||||||
|
}
|
||||||
+57
-12
@@ -18,34 +18,54 @@ import "fmt"
|
|||||||
|
|
||||||
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
|
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
|
||||||
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
|
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
|
||||||
// 未匹配的名称不影响场景,矩阵为空时保留每条父路径的原始变换
|
// 显式Opacity覆盖材质和纹理合成的透明度,之后仍应用视图附加透明度
|
||||||
|
// 未匹配的名称不影响场景,Matrix和Motion均为空时保留每条父路径的原始变换
|
||||||
|
// Motion以指定轨道替换节点变换,与Matrix互斥,不驱动骨架;显式变换优先于动画修饰器
|
||||||
|
// Bones覆盖动画求值后的指定骨骼姿态,未指定项沿用当前局部变换,不修改共享几何
|
||||||
type U3DNodeState struct {
|
type U3DNodeState struct {
|
||||||
Name string
|
Name string
|
||||||
Opacity *float64
|
Opacity *float64
|
||||||
Visible *bool
|
Visible *bool
|
||||||
Matrix *[16]float64
|
Matrix *[16]float64
|
||||||
|
Motion *U3DTrackState
|
||||||
|
Bones []U3DBoneState
|
||||||
}
|
}
|
||||||
|
|
||||||
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
|
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
|
||||||
// 入参: states 节点覆盖
|
// 入参: groups 按优先级递增排列的节点覆盖分组
|
||||||
// 返回: error 数值、预算或取消错误
|
// 返回: error 数值、预算或取消错误
|
||||||
func (r *u3dRender) nodeStates(states []U3DNodeState) error {
|
func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error {
|
||||||
if len(states) == 0 {
|
var count uint64
|
||||||
|
for _, states := range groups {
|
||||||
|
if uint64(len(states)) > ^uint64(0)-count {
|
||||||
|
return fmt.Errorf("U3D node state count exceeds limit")
|
||||||
|
}
|
||||||
|
count += uint64(len(states))
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := r.reserve(uint64(len(states)), 128); err != nil {
|
if err := r.reserve(count, 160); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.states = make(map[string]U3DNodeState, len(states))
|
r.states = make(map[string]U3DNodeState, int(count))
|
||||||
for i, state := range states {
|
for _, states := range groups {
|
||||||
if i%256 == 0 {
|
for i, state := range states {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if i%256 == 0 {
|
||||||
return err
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
r.states[state.Name] = state
|
||||||
}
|
}
|
||||||
r.states[state.Name] = state
|
|
||||||
}
|
}
|
||||||
for _, state := range r.states {
|
var samples map[U3DTrackState]*[16]float64
|
||||||
|
defer func() {
|
||||||
|
if samples != nil {
|
||||||
|
r.remaining += 1024 + uint64(len(samples))*192
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for name, state := range r.states {
|
||||||
if err := r.ctx.Err(); err != nil {
|
if err := r.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -55,6 +75,31 @@ func (r *u3dRender) nodeStates(states []U3DNodeState) error {
|
|||||||
if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() {
|
if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() {
|
||||||
return fmt.Errorf("invalid U3D node matrix")
|
return fmt.Errorf("invalid U3D node matrix")
|
||||||
}
|
}
|
||||||
|
if state.Motion != nil {
|
||||||
|
if state.Matrix != nil || !finite(state.Motion.Time) {
|
||||||
|
return fmt.Errorf("invalid U3D node motion state")
|
||||||
|
}
|
||||||
|
if samples == nil {
|
||||||
|
if err := r.reserve(1, 1024); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
samples = make(map[U3DTrackState]*[16]float64)
|
||||||
|
}
|
||||||
|
matrix := samples[*state.Motion]
|
||||||
|
if matrix == nil {
|
||||||
|
if err := r.reserve(1, 320); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := r.trackMatrix(*state.Motion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matrix = &value
|
||||||
|
samples[*state.Motion] = matrix
|
||||||
|
}
|
||||||
|
state.Matrix = matrix
|
||||||
|
r.states[name] = state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据
|
||||||
|
type U3DTexture struct {
|
||||||
|
Name string
|
||||||
|
Width, Height uint32
|
||||||
|
ImageType uint8
|
||||||
|
Images []U3DTextureImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DTextureImage 保存压缩格式、通道位图及完整压缩数据,外部图像仅保留候选URL
|
||||||
|
// Compression依次以1至4表示JPEG-24、PNG、JPEG-8和TIFF,ByteCount仅用于内置图像
|
||||||
|
type U3DTextureImage struct {
|
||||||
|
Compression uint8
|
||||||
|
Channels uint8
|
||||||
|
External bool
|
||||||
|
ByteCount uint32
|
||||||
|
Data []byte
|
||||||
|
URLs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dTextureChannels 将亮度通道展开为RGB覆盖位,保留透明度位
|
||||||
|
// 入参: channels 标准通道位图
|
||||||
|
// 返回: uint8 实际覆盖通道
|
||||||
|
func u3dTextureChannels(channels uint8) uint8 {
|
||||||
|
if channels&16 != 0 {
|
||||||
|
channels = channels&^16 | 14
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// textureDeclaration 按ECMA-363第9.8.5.1节读取纹理及通道图像声明
|
||||||
|
// 入参: r 字段读取器, name 纹理名称
|
||||||
|
// 返回: error 格式、预算或取消错误
|
||||||
|
func (d *u3dDecoder) textureDeclaration(r *u3dValues, name string) error {
|
||||||
|
if _, exists := d.textures[name]; exists {
|
||||||
|
return fmt.Errorf("duplicate U3D texture %q", name)
|
||||||
|
}
|
||||||
|
texture := U3DTexture{Name: name, Height: r.u32(), Width: r.u32(), ImageType: r.u8()}
|
||||||
|
if texture.Width == 0 || texture.Height == 0 {
|
||||||
|
return fmt.Errorf("invalid U3D texture dimensions")
|
||||||
|
}
|
||||||
|
switch texture.ImageType {
|
||||||
|
case 1, 14, 15, 16, 17:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid U3D texture image type")
|
||||||
|
}
|
||||||
|
count := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if count == 0 || count > 4 || uint64(count) > uint64(len(r.data)-r.pos)/8 {
|
||||||
|
return fmt.Errorf("invalid U3D continuation image count")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 64); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
texture.Images = make([]U3DTextureImage, int(count))
|
||||||
|
var covered uint8
|
||||||
|
for n := range texture.Images {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image := &texture.Images[n]
|
||||||
|
image.Compression, image.Channels = r.u8(), r.u8()
|
||||||
|
attributes := uint16(r.u8()) | uint16(r.u8())<<8
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
channels := u3dTextureChannels(image.Channels)
|
||||||
|
if image.Compression < 1 || image.Compression > 4 || image.Channels == 0 || image.Channels&^uint8(31) != 0 || attributes > 1 || covered&channels != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
|
||||||
|
return fmt.Errorf("invalid U3D continuation image format")
|
||||||
|
}
|
||||||
|
covered |= channels
|
||||||
|
image.External = attributes == 1
|
||||||
|
if !image.External {
|
||||||
|
image.ByteCount = r.u32()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if uint64(count) > uint64(len(r.data)-r.pos)/2 {
|
||||||
|
return fmt.Errorf("invalid U3D texture URL count")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 16); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image.URLs = make([]string, int(count))
|
||||||
|
for i := range image.URLs {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image.URLs[i] = r.text()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.done(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if covered != u3dTextureChannels(texture.ImageType) {
|
||||||
|
return fmt.Errorf("incomplete U3D texture channels")
|
||||||
|
}
|
||||||
|
if d.textures == nil {
|
||||||
|
d.textures = make(map[string]int)
|
||||||
|
}
|
||||||
|
d.textures[name] = len(d.model.Textures)
|
||||||
|
d.model.Textures = append(d.model.Textures, texture)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// textureContinuation 按声明长度合并纹理分片,不与输入共享存储
|
||||||
|
// 入参: r 字段读取器, name 纹理名称
|
||||||
|
// 返回: error 格式、预算或取消错误
|
||||||
|
func (d *u3dDecoder) textureContinuation(r *u3dValues, name string) error {
|
||||||
|
index := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
texture, exists := d.textures[name]
|
||||||
|
if !exists || uint64(index) >= uint64(len(d.model.Textures[texture].Images)) {
|
||||||
|
return fmt.Errorf("invalid U3D texture continuation reference")
|
||||||
|
}
|
||||||
|
image := &d.model.Textures[texture].Images[index]
|
||||||
|
data := r.take(len(r.data) - r.pos)
|
||||||
|
if image.External || uint64(len(image.Data))+uint64(len(data)) > uint64(image.ByteCount) {
|
||||||
|
return fmt.Errorf("invalid U3D texture continuation length")
|
||||||
|
}
|
||||||
|
if image.Data == nil {
|
||||||
|
if err := d.reserve(uint64(image.ByteCount), 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image.Data = make([]byte, 0, int(image.ByteCount))
|
||||||
|
}
|
||||||
|
for len(data) != 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count := min(65536, len(data))
|
||||||
|
image.Data = append(image.Data, data[:count]...)
|
||||||
|
data = data[count:]
|
||||||
|
}
|
||||||
|
return d.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTextures 确认各通道的内置图像完整,不加载外部引用
|
||||||
|
// 返回: error 缺失分片或取消错误
|
||||||
|
func (d *u3dDecoder) checkTextures() error {
|
||||||
|
for _, texture := range d.model.Textures {
|
||||||
|
for _, image := range texture.Images {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !image.External && uint64(len(image.Data)) != uint64(image.ByteCount) {
|
||||||
|
return fmt.Errorf("incomplete U3D texture %q", texture.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/image/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DTextureResolver 由调用方按原始URL读取外部纹理,地址解析、访问策略和读取限额由调用方控制
|
||||||
|
// 返回的数据在本次绘制结束前不得修改;失败时尝试下一候选地址,取消及超时错误立即返回
|
||||||
|
// 入参: ctx 取消上下文, url 绝对或相对地址
|
||||||
|
// 返回: []byte 压缩图像数据, error 读取错误
|
||||||
|
type U3DTextureResolver func(ctx context.Context, url string) ([]byte, error)
|
||||||
|
|
||||||
|
// u3dImageReader 在图像解码器读取压缩数据时检查取消状态
|
||||||
|
type u3dImageReader struct {
|
||||||
|
ctx context.Context
|
||||||
|
data *bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read 读取顺序图像数据
|
||||||
|
// 入参: p 接收缓冲
|
||||||
|
// 返回: int 已读字节数, error 读取或取消错误
|
||||||
|
func (r *u3dImageReader) Read(p []byte) (int, error) {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.data.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAt 读取指定偏移的图像数据
|
||||||
|
// 入参: p 接收缓冲, offset 字节偏移
|
||||||
|
// 返回: int 已读字节数, error 读取或取消错误
|
||||||
|
func (r *u3dImageReader) ReadAt(p []byte, offset int64) (int, error) {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return r.data.ReadAt(p, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理
|
||||||
|
// 入参: name 纹理名称
|
||||||
|
// 返回: *image.NRGBA64 非预乘纹理, error 资源、解码、预算或取消错误
|
||||||
|
func (r *u3dRender) texture(name string) (*image.NRGBA64, error) {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cached := r.textures[name]; cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 256); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var pixels *image.NRGBA64
|
||||||
|
if name == "" {
|
||||||
|
if err := r.reserve(64, 8); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8))
|
||||||
|
for y := range 8 {
|
||||||
|
for x := range 8 {
|
||||||
|
c := color.NRGBA64{65535, 65535, 65535, 65535}
|
||||||
|
if x/4 != y/4 {
|
||||||
|
c.G, c.B = 26214, 13107
|
||||||
|
}
|
||||||
|
pixels.SetNRGBA64(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index, exists := r.textureIndex[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("missing U3D texture %q", name)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
pixels, err = r.decodeTexture(&r.model.Textures[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("U3D texture %q: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.textures == nil {
|
||||||
|
r.textures = make(map[string]*image.NRGBA64)
|
||||||
|
}
|
||||||
|
r.textures[name] = pixels
|
||||||
|
return pixels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度
|
||||||
|
// 入参: texture 纹理资源
|
||||||
|
// 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误
|
||||||
|
func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
|
||||||
|
switch texture.ImageType {
|
||||||
|
case 1, 14, 15, 16, 17:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid image type")
|
||||||
|
}
|
||||||
|
if texture.Width == 0 || texture.Height == 0 || len(texture.Images) == 0 || len(texture.Images) > 4 {
|
||||||
|
return nil, fmt.Errorf("invalid image dimensions or channel count")
|
||||||
|
}
|
||||||
|
var covered uint8
|
||||||
|
for _, source := range texture.Images {
|
||||||
|
if source.Compression < 1 || source.Compression > 4 {
|
||||||
|
return nil, fmt.Errorf("invalid image compression")
|
||||||
|
}
|
||||||
|
channels := u3dTextureChannels(source.Channels)
|
||||||
|
if source.Channels == 0 || source.Channels&^uint8(31) != 0 || channels&covered != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid image channels")
|
||||||
|
}
|
||||||
|
covered |= channels
|
||||||
|
if source.External && (r.resolveTexture == nil || len(source.URLs) == 0) {
|
||||||
|
return nil, fmt.Errorf("external texture image requires resolution")
|
||||||
|
}
|
||||||
|
if !source.External && uint64(len(source.Data)) != uint64(source.ByteCount) {
|
||||||
|
return nil, fmt.Errorf("invalid image data length")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if covered != u3dTextureChannels(texture.ImageType) {
|
||||||
|
return nil, fmt.Errorf("incomplete image channels")
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(texture.Width)*uint64(texture.Height), 8); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pixels := image.NewNRGBA64(image.Rect(0, 0, int(texture.Width), int(texture.Height)))
|
||||||
|
for y := range pixels.Rect.Dy() {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for x := range pixels.Rect.Dx() {
|
||||||
|
if x%4096 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels.SetNRGBA64(x, y, color.NRGBA64{65535, 65535, 65535, 65535})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, source := range texture.Images {
|
||||||
|
if err := r.textureSource(pixels, source); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixels, r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// textureSource 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明
|
||||||
|
// 入参: pixels 目标纹理, source 通道图像
|
||||||
|
// 返回: error 读取、解码、预算或取消错误
|
||||||
|
func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error {
|
||||||
|
if !source.External {
|
||||||
|
return r.textureChannels(pixels, source)
|
||||||
|
}
|
||||||
|
if r.resolveTexture == nil || len(source.URLs) == 0 {
|
||||||
|
return fmt.Errorf("external texture image requires resolution")
|
||||||
|
}
|
||||||
|
var last error
|
||||||
|
for _, address := range source.URLs {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := r.resolveTexture(r.ctx, address)
|
||||||
|
if r.ctx.Err() != nil {
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
source.Data = data
|
||||||
|
err = r.textureChannels(pixels, source)
|
||||||
|
source.Data = nil
|
||||||
|
}
|
||||||
|
if canceled := r.ctx.Err(); canceled != nil {
|
||||||
|
return canceled
|
||||||
|
}
|
||||||
|
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
last = fmt.Errorf("external texture image %q: %w", address, err)
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
// textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销
|
||||||
|
// 入参: pixels 目标纹理, source 通道图像
|
||||||
|
// 返回: error 格式、预算或取消错误
|
||||||
|
func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImage) error {
|
||||||
|
var config func(io.Reader) (image.Config, error)
|
||||||
|
var decode func(io.Reader) (image.Image, error)
|
||||||
|
switch source.Compression {
|
||||||
|
case 1, 3:
|
||||||
|
config, decode = jpeg.DecodeConfig, jpeg.Decode
|
||||||
|
case 2:
|
||||||
|
config, decode = png.DecodeConfig, png.Decode
|
||||||
|
case 4:
|
||||||
|
config, decode = tiff.DecodeConfig, tiff.Decode
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid image compression")
|
||||||
|
}
|
||||||
|
budget := r.remaining
|
||||||
|
defer func() { r.remaining = budget }()
|
||||||
|
if err := r.reserve(uint64(len(source.Data)), 4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 1<<20); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader := &u3dImageReader{ctx: r.ctx, data: bytes.NewReader(source.Data)}
|
||||||
|
cfg, err := config(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg.Width != pixels.Rect.Dx() || cfg.Height != pixels.Rect.Dy() {
|
||||||
|
return fmt.Errorf("image dimensions differ from texture declaration")
|
||||||
|
}
|
||||||
|
if source.Compression == 3 && cfg.ColorModel != color.GrayModel || source.Compression == 1 && cfg.ColorModel != color.YCbCrModel {
|
||||||
|
return fmt.Errorf("invalid JPEG texture color model")
|
||||||
|
}
|
||||||
|
w, h := uint64(cfg.Width)+16, uint64(cfg.Height)+16
|
||||||
|
if source.Compression == 4 {
|
||||||
|
w, h = max(w, 1024), max(h, 1024)
|
||||||
|
}
|
||||||
|
if err := r.reserve(w*h, 32); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader.data.Reset(source.Data)
|
||||||
|
img, err := decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if img.Bounds() != pixels.Bounds() {
|
||||||
|
return fmt.Errorf("invalid decoded image dimensions")
|
||||||
|
}
|
||||||
|
channels := u3dTextureChannels(source.Channels)
|
||||||
|
grayAlpha := source.Channels == 1 && (cfg.ColorModel == color.GrayModel || cfg.ColorModel == color.Gray16Model)
|
||||||
|
for y := range cfg.Height {
|
||||||
|
for x := range cfg.Width {
|
||||||
|
if x%4096 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := imageNRGBA64At(img, x, y)
|
||||||
|
current := pixels.NRGBA64At(x, y)
|
||||||
|
if channels&8 != 0 {
|
||||||
|
current.R = value.R
|
||||||
|
}
|
||||||
|
if channels&4 != 0 {
|
||||||
|
current.G = value.G
|
||||||
|
}
|
||||||
|
if channels&2 != 0 {
|
||||||
|
current.B = value.B
|
||||||
|
}
|
||||||
|
if channels&1 != 0 {
|
||||||
|
current.A = value.A
|
||||||
|
if grayAlpha {
|
||||||
|
current.A = value.R
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels.SetNRGBA64(x, y, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
+48
-26
@@ -20,10 +20,13 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// u3dVertex 保存裁剪前后的相机坐标及未预乘颜色
|
// u3dVertex 保存裁剪前后的相机坐标、未预乘颜色及纹理插值输入
|
||||||
type u3dVertex struct {
|
type u3dVertex struct {
|
||||||
point u3dVector
|
point u3dVector
|
||||||
color [4]float64
|
color [4]float64
|
||||||
|
texture [8][3]float64
|
||||||
|
textureMask uint8
|
||||||
|
opacityOverride bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
|
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
|
||||||
@@ -51,8 +54,9 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
}
|
}
|
||||||
r.primitives = make(map[u3dPrimitiveKey]struct{})
|
r.primitives = make(map[u3dPrimitiveKey]struct{})
|
||||||
defer func() {
|
defer func() {
|
||||||
r.remaining += 1024 + uint64(len(r.primitives))*512
|
r.remaining += 1024 + uint64(len(r.primitives))*512 + uint64(len(r.texturedPrimitives))*1024
|
||||||
r.primitives = nil
|
r.primitives = nil
|
||||||
|
r.texturedPrimitives = nil
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
normalCount := 0
|
normalCount := 0
|
||||||
@@ -161,15 +165,13 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}}
|
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
|
||||||
var err error
|
var err error
|
||||||
if shaded {
|
if shaded {
|
||||||
style, err = r.style(name)
|
style, err = r.style(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if index, ok := r.shaders[name]; ok {
|
|
||||||
style.shader.RenderPass = r.model.Shaders[index].RenderPass
|
|
||||||
}
|
}
|
||||||
if style.shader.RenderPass&(1<<passIndex) == 0 {
|
if style.shader.RenderPass&(1<<passIndex) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -177,7 +179,23 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
if instance.opacity >= 0 || r.opacity >= 0 {
|
if instance.opacity >= 0 || r.opacity >= 0 {
|
||||||
style.shader.Blend = 0x606
|
style.shader.Blend = 0x606
|
||||||
}
|
}
|
||||||
|
textures, err := r.shaderTextures(style.shader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for i, corner := range face.Corners {
|
for i, corner := range face.Corners {
|
||||||
|
normal := faceNormal
|
||||||
|
if shaded && !mesh.ExcludeNormals && invertible {
|
||||||
|
normal = normals[corner.Normal]
|
||||||
|
}
|
||||||
|
if !front {
|
||||||
|
for j := range normal {
|
||||||
|
normal[j] = -normal[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if !shaded {
|
if !shaded {
|
||||||
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
|
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
|
||||||
if r.mode == U3DRenderIllustration {
|
if r.mode == U3DRenderIllustration {
|
||||||
@@ -192,15 +210,6 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
normal := faceNormal
|
|
||||||
if !mesh.ExcludeNormals && invertible {
|
|
||||||
normal = normals[corner.Normal]
|
|
||||||
}
|
|
||||||
if !front {
|
|
||||||
for j := range normal {
|
|
||||||
normal[j] = -normal[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var diffuse, specular *[4]float32
|
var diffuse, specular *[4]float32
|
||||||
if shading.Attributes&1 != 0 {
|
if shading.Attributes&1 != 0 {
|
||||||
diffuse = &mesh.Diffuse[corner.Diffuse]
|
diffuse = &mesh.Diffuse[corner.Diffuse]
|
||||||
@@ -214,8 +223,9 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
}
|
}
|
||||||
if instance.opacity >= 0 {
|
if instance.opacity >= 0 {
|
||||||
vertices[i].color[3] = instance.opacity
|
vertices[i].color[3] = instance.opacity
|
||||||
|
vertices[i].opacityOverride = true
|
||||||
}
|
}
|
||||||
if r.opacity >= 0 {
|
if r.opacity >= 0 && textures == nil {
|
||||||
vertices[i].color[3] *= r.opacity
|
vertices[i].color[3] *= r.opacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +245,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
|
|||||||
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
|
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
|
||||||
// 返回: error 数值、资源或取消错误
|
// 返回: error 数值、资源或取消错误
|
||||||
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
||||||
if shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
|
if shader.Textures == ([8]*U3DTextureLayer{}) && shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
|
||||||
return r.ctx.Err()
|
return r.ctx.Err()
|
||||||
}
|
}
|
||||||
var storage [2][12]u3dVertex
|
var storage [2][12]u3dVertex
|
||||||
@@ -259,13 +269,7 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
|
|||||||
}
|
}
|
||||||
if (d >= 0) != (previousDistance >= 0) {
|
if (d >= 0) != (previousDistance >= 0) {
|
||||||
t := u3dClipFraction(previousDistance, d)
|
t := u3dClipFraction(previousDistance, d)
|
||||||
var intersection u3dVertex
|
intersection := u3dInterpolate(previous, vertex, t)
|
||||||
for i := range intersection.point {
|
|
||||||
intersection.point[i] = previous.point[i]*(1-t) + vertex.point[i]*t
|
|
||||||
}
|
|
||||||
for i := range intersection.color {
|
|
||||||
intersection.color[i] = previous.color[i]*(1-t) + vertex.color[i]*t
|
|
||||||
}
|
|
||||||
if output == len(storage[next]) {
|
if output == len(storage[next]) {
|
||||||
return fmt.Errorf("invalid U3D clipped polygon")
|
return fmt.Errorf("invalid U3D clipped polygon")
|
||||||
}
|
}
|
||||||
@@ -298,6 +302,10 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
|
|||||||
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
|
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
|
||||||
// 返回: error 数值、预算或取消错误
|
// 返回: error 数值、预算或取消错误
|
||||||
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
||||||
|
textures, err := r.shaderTextures(shader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var x, y, reciprocal [3]float64
|
var x, y, reciprocal [3]float64
|
||||||
for i, v := range triangle {
|
for i, v := range triangle {
|
||||||
reciprocal[i] = 1
|
reciprocal[i] = 1
|
||||||
@@ -385,13 +393,27 @@ func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass
|
|||||||
color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c])
|
color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c])
|
||||||
}
|
}
|
||||||
position := u3dVector{}
|
position := u3dVector{}
|
||||||
if pass.Attributes&1 != 0 {
|
if pass.Attributes&1 != 0 || textures != nil && textures.reflection {
|
||||||
position = u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth}
|
position = u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth}
|
||||||
if r.camera.Perspective {
|
if r.camera.Perspective {
|
||||||
position[0] *= depth
|
position[0] *= depth
|
||||||
position[1] *= depth
|
position[1] *= depth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if textures != nil {
|
||||||
|
var uv [8][3]float64
|
||||||
|
for layer := range textures.layers {
|
||||||
|
if textures.mask&(1<<layer) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for c := range uv[layer] {
|
||||||
|
uv[layer][c] = triangle[0].texture[layer][c] + w1*(triangle[1].texture[layer][c]-triangle[0].texture[layer][c]) + w2*(triangle[2].texture[layer][c]-triangle[0].texture[layer][c])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.textureColor(textures, &color, &uv, position, triangle[0].opacityOverride); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
visible, err := u3dFragmentColor(&color, position, &shader, &pass)
|
visible, err := u3dFragmentColor(&color, position, &shader, &pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层
|
||||||
|
// 入参: layers 按后至前排列的纹理层
|
||||||
|
// 返回: error 参数、资源、预算或取消错误
|
||||||
|
func (r *u3dRender) viewLayers(layers []U3DViewLayer) error {
|
||||||
|
for _, layer := range layers {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, value := range [...]float32{layer.Blend, layer.Rotation, layer.Location[0], layer.Location[1], layer.Scale[0], layer.Scale[1]} {
|
||||||
|
if !finite(float64(value)) {
|
||||||
|
return fmt.Errorf("invalid U3D view layer values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
texture, err := r.texture(layer.Texture)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
blend := min(1, max(0, float64(layer.Blend)))
|
||||||
|
if blend == 0 || layer.Scale[0] == 0 || layer.Scale[1] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sin, cos := math.Sincos(float64(layer.Rotation))
|
||||||
|
x, y := float64(layer.Location[0]), float64(layer.Location[1])
|
||||||
|
if r.view.Attributes&1 != 0 {
|
||||||
|
x *= float64(r.output.Rect.Dx())
|
||||||
|
y *= float64(r.output.Rect.Dy())
|
||||||
|
}
|
||||||
|
sx, sy := float64(layer.Scale[0]), float64(layer.Scale[1])
|
||||||
|
rx, ry := float64(layer.Registration[0]), float64(layer.Registration[1])
|
||||||
|
m := RasterMatrix{cos * sx, -sin * sx, sin * sy, cos * sy, x - cos*sx*rx - sin*sy*ry, y + sin*sx*rx - cos*sy*ry}
|
||||||
|
bounds := rasterImageBounds(texture.Bounds(), m, r.output.Bounds())
|
||||||
|
for py := bounds.Min.Y; py < bounds.Max.Y; py++ {
|
||||||
|
for px := bounds.Min.X; px < bounds.Max.X; px++ {
|
||||||
|
if px%4096 == 0 || px == bounds.Min.X {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx, cy := float64(px)+.5, float64(py)+.5
|
||||||
|
if cx < r.viewport[0] || cy < r.viewport[1] || cx >= r.viewport[0]+r.viewport[2] || cy >= r.viewport[1]+r.viewport[3] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dx, dy := cx-x, cy-y
|
||||||
|
u, v := (cos*dx-sin*dy)/sx+rx, (sin*dx+cos*dy)/sy+ry
|
||||||
|
if u < 0 || v < 0 || u >= float64(texture.Rect.Dx()) || v >= float64(texture.Rect.Dy()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := u3dTextureSample(texture, u, v)
|
||||||
|
pixel := &r.frame[py*r.output.Rect.Dx()+px]
|
||||||
|
alpha := value[3] * blend
|
||||||
|
for i := range pixel.color {
|
||||||
|
pixel.color[i] = value[i]*blend + pixel.color[i]*(1-alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dTextureSample 在像素中心进行双线性采样,使用预乘颜色避免透明边缘色晕
|
||||||
|
// 入参: texture 纹理, x 横向像素坐标, y 纵向像素坐标
|
||||||
|
// 返回: [4]float64 预乘颜色及透明度
|
||||||
|
func u3dTextureSample(texture *image.NRGBA64, x, y float64) [4]float64 {
|
||||||
|
x, y = x-.5, y-.5
|
||||||
|
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
|
||||||
|
fx, fy := x-float64(x0), y-float64(y0)
|
||||||
|
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
|
||||||
|
var result [4]float64
|
||||||
|
for row := range 2 {
|
||||||
|
for col := range 2 {
|
||||||
|
weight := wx[col] * wy[row]
|
||||||
|
c := texture.NRGBA64At(min(max(0, x0+col), texture.Rect.Max.X-1), min(max(0, y0+row), texture.Rect.Max.Y-1))
|
||||||
|
alpha := float64(c.A) / 65535
|
||||||
|
result[0] += float64(c.R) / 65535 * alpha * weight
|
||||||
|
result[1] += float64(c.G) / 65535 * alpha * weight
|
||||||
|
result[2] += float64(c.B) / 65535 * alpha * weight
|
||||||
|
result[3] += alpha * weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DBoneWeight 保存某骨骼对一个位置的影响系数
|
||||||
|
type U3DBoneWeight struct {
|
||||||
|
Bone uint32
|
||||||
|
Weight float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DBoneWeights 保存修饰链中的权重,Index为链内位置,Attributes的1、2、4分别表示网格、线集、点集
|
||||||
|
// 第i个位置的权重为Weights[Offsets[i]:Offsets[i+1]],空区间表示没有骨骼影响
|
||||||
|
type U3DBoneWeights struct {
|
||||||
|
Index, Attributes uint32
|
||||||
|
InverseQuantization float32
|
||||||
|
Offsets []uint32
|
||||||
|
Weights []U3DBoneWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// boneWeights 读取ECMA-363第9.7.4节权重修饰器,末项权重由其余各项之和确定
|
||||||
|
// 入参: values 字段读取器, name 所属链名, chain 链类型, index 链内位置
|
||||||
|
// 返回: error 结构、数值、预算或取消错误
|
||||||
|
func (d *u3dDecoder) boneWeights(values *u3dValues, name string, chain int, index uint32) error {
|
||||||
|
r := u3dResourceReader{values: values, decoder: d}
|
||||||
|
defer r.release()
|
||||||
|
weights := U3DBoneWeights{Index: index, Attributes: r.u32(), InverseQuantization: r.f32()}
|
||||||
|
positions := r.u32()
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if weights.Attributes != 1 && weights.Attributes != 2 && weights.Attributes != 4 || weights.InverseQuantization < 0 {
|
||||||
|
return fmt.Errorf("invalid U3D bone weight parameters")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(positions)+1, 4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
weights.Offsets = make([]uint32, int(positions)+1)
|
||||||
|
for position := range positions {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count := r.value(0, false)
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := len(weights.Weights)
|
||||||
|
needed := uint64(start) + uint64(count)
|
||||||
|
if needed > math.MaxUint32 {
|
||||||
|
return fmt.Errorf("U3D bone weight count exceeds limit")
|
||||||
|
}
|
||||||
|
if needed > uint64(cap(weights.Weights)) {
|
||||||
|
capacity := max(needed, uint64(cap(weights.Weights))*2)
|
||||||
|
if err := d.reserve(capacity, 8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
grown := make([]U3DBoneWeight, int(needed), int(capacity))
|
||||||
|
copy(grown, weights.Weights)
|
||||||
|
d.remaining += uint64(cap(weights.Weights)) * 8
|
||||||
|
weights.Weights = grown
|
||||||
|
} else {
|
||||||
|
weights.Weights = weights.Weights[:int(needed)]
|
||||||
|
}
|
||||||
|
list := weights.Weights[start:]
|
||||||
|
for i := range list {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list[i].Bone = r.value(1, false)
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var total float32
|
||||||
|
for i := 0; i+1 < len(list); i++ {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list[i].Weight = float32(float64(r.value(2, false)) * float64(weights.InverseQuantization))
|
||||||
|
total += list[i].Weight
|
||||||
|
if err := r.err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !finite(float64(total)) || total > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D bone weight sum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(list) != 0 {
|
||||||
|
list[len(list)-1].Weight = 1 - total
|
||||||
|
}
|
||||||
|
weights.Offsets[position+1] = uint32(needed)
|
||||||
|
}
|
||||||
|
if err := r.done(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chain == 1 {
|
||||||
|
d.meshes[name].mesh.BoneWeights = append(d.meshes[name].mesh.BoneWeights, weights)
|
||||||
|
} else {
|
||||||
|
node := &d.model.Nodes[d.nodes[name]]
|
||||||
|
node.BoneWeights = append(node.BoneWeights, weights)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkBoneWeights 在几何和节点声明齐备后校验权重引用,允许节点引用后续资源
|
||||||
|
// 返回: error 位置、骨骼引用或取消错误
|
||||||
|
func (d *u3dDecoder) checkBoneWeights() error {
|
||||||
|
for _, name := range d.meshOrder {
|
||||||
|
mesh := d.meshes[name]
|
||||||
|
if err := d.checkMeshBoneWeights(mesh, mesh.mesh.BoneWeights); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, node := range d.model.Nodes {
|
||||||
|
if err := d.checkMeshBoneWeights(d.meshes[node.Resource], node.BoneWeights); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkMeshBoneWeights 校验适用于指定几何类型的权重,不把未匹配的类型强加到几何上
|
||||||
|
// 入参: mesh 几何声明,可为空, sets 权重修饰器
|
||||||
|
// 返回: error 位置、骨骼引用或取消错误
|
||||||
|
func (d *u3dDecoder) checkMeshBoneWeights(mesh *u3dMeshDeclaration, sets []U3DBoneWeights) error {
|
||||||
|
for _, weights := range sets {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mesh == nil {
|
||||||
|
if len(weights.Weights) != 0 {
|
||||||
|
return fmt.Errorf("missing U3D bone weight geometry")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kind := uint32(1)
|
||||||
|
if mesh.points {
|
||||||
|
kind = 4
|
||||||
|
} else if mesh.line != nil {
|
||||||
|
kind = 2
|
||||||
|
}
|
||||||
|
if weights.Attributes != kind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if uint64(len(weights.Offsets)-1) > uint64(len(mesh.mesh.Positions)) {
|
||||||
|
return fmt.Errorf("invalid U3D bone weight position count")
|
||||||
|
}
|
||||||
|
for index, weight := range weights.Weights {
|
||||||
|
if index%1024 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uint64(weight.Bone) >= uint64(len(mesh.mesh.Bones)) {
|
||||||
|
return fmt.Errorf("invalid U3D bone weight index")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+24
-10
@@ -84,7 +84,7 @@ func (r *Reader) versionFileDependencies(ctx context.Context, entry string, part
|
|||||||
|
|
||||||
// versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径
|
// versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径
|
||||||
// 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目, generated 本次自产页面及引用
|
// 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目, generated 本次自产页面及引用
|
||||||
// 返回: *versionEditGraph 依赖图, error 读取或结构错误
|
// 返回: *versionEditGraph 依赖图, error 读取、结构或取消错误
|
||||||
func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte, generated map[string]editorGeneratedReferences) (*versionEditGraph, error) {
|
func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte, generated map[string]editorGeneratedReferences) (*versionEditGraph, error) {
|
||||||
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
|
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
|
||||||
queue := []string{entry}
|
queue := []string{entry}
|
||||||
@@ -104,12 +104,12 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
|
|||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
data, err = r.readFile(name)
|
data, err = r.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("version file %s: %w", name, err)
|
return nil, fmt.Errorf("version file %s: %w", name, err)
|
||||||
}
|
}
|
||||||
@@ -119,17 +119,20 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
|
|||||||
if root.name.Local == "Res" {
|
if root.name.Local == "Res" {
|
||||||
base = root.attr("BaseLoc")
|
base = root.attr("BaseLoc")
|
||||||
}
|
}
|
||||||
var visit func(*editorXML)
|
var visit func(*editorXML) error
|
||||||
visit = func(node *editorXML) {
|
visit = func(node *editorXML) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if node != root && !node.matchesOFD(node.name.Local) {
|
if node != root && !node.matchesOFD(node.name.Local) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
parent := ""
|
parent := ""
|
||||||
if node.parent != nil {
|
if node.parent != nil {
|
||||||
parent = node.parent.name.Local
|
parent = node.parent.name.Local
|
||||||
}
|
}
|
||||||
if parent == "Extension" && node.name.Local == "Data" {
|
if parent == "Extension" && node.name.Local == "Data" {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
add := func(attribute, value string, kind int) {
|
add := func(attribute, value string, kind int) {
|
||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
@@ -154,10 +157,18 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
visit(child)
|
if err := visit(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
visit(root)
|
if err := visit(root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return graph, nil
|
return graph, nil
|
||||||
}
|
}
|
||||||
@@ -408,7 +419,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
|
|
||||||
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
|
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
|
||||||
// 入参: paths 原路径到新路径的映射
|
// 入参: paths 原路径到新路径的映射
|
||||||
// 返回: []byte 更新后的XML, error 属性编码错误
|
// 返回: []byte 更新后的XML,未改动时借用原文, error 属性编码错误
|
||||||
func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
headers := make(map[*editorXML][]versionEditLink)
|
headers := make(map[*editorXML][]versionEditLink)
|
||||||
@@ -444,6 +455,9 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
|||||||
patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]})
|
patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(patches) == 0 {
|
||||||
|
return file.data, nil
|
||||||
|
}
|
||||||
return editorPatchXML(file.data, patches), nil
|
return editorPatchXML(file.data, patches), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-10
@@ -441,8 +441,8 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池
|
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
|
||||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 原资源文件
|
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 独立资源文件
|
||||||
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
|
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
|
||||||
used := make(map[string]bool)
|
used := make(map[string]bool)
|
||||||
promoted := make(map[string]bool)
|
promoted := make(map[string]bool)
|
||||||
@@ -487,10 +487,13 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
for _, page := range e.pages {
|
for _, page := range e.pages {
|
||||||
collectActionReferences(page.Actions, used)
|
collectActionReferences(page.Actions, used)
|
||||||
var original map[string]GraphicObject
|
var original map[string]GraphicObject
|
||||||
|
var originalLayers map[string]string
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
original = make(map[string]GraphicObject)
|
original = make(map[string]GraphicObject)
|
||||||
|
originalLayers = make(map[string]string)
|
||||||
if source := e.source.pages[page.ID]; source != nil && source.original != nil {
|
if source := e.source.pages[page.ID]; source != nil && source.original != nil {
|
||||||
for _, layer := range source.original.Content.Layer {
|
for _, layer := range source.original.Content.Layer {
|
||||||
|
originalLayers[layer.ID] = layer.DrawParam
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
original[editorObjectID(object)] = object
|
original[editorObjectID(object)] = object
|
||||||
}
|
}
|
||||||
@@ -498,6 +501,12 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
|
if layer.DrawParam != "" {
|
||||||
|
used[layer.DrawParam] = true
|
||||||
|
if e.source != nil && originalLayers[layer.ID] != layer.DrawParam {
|
||||||
|
promoted[layer.DrawParam] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
before, exists := original[editorObjectID(object)]
|
before, exists := original[editorObjectID(object)]
|
||||||
collectObjectReferences(object, used)
|
collectObjectReferences(object, used)
|
||||||
@@ -514,14 +523,31 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
}
|
}
|
||||||
normalizeResourceReferences(used)
|
normalizeResourceReferences(used)
|
||||||
normalizeResourceReferences(promoted)
|
normalizeResourceReferences(promoted)
|
||||||
for i := len(e.resources) - 1; i >= 0; i-- {
|
definitions := make(map[string]int)
|
||||||
resource := e.resources[i]
|
var pending []string
|
||||||
if resource.definition() != "" && used[resource.definition()] {
|
for index, resource := range e.resources {
|
||||||
for _, id := range resource.references {
|
if id := resource.definition(); id != "" {
|
||||||
used[id] = true
|
definitions[id] = index
|
||||||
if e.source != nil {
|
if used[id] {
|
||||||
promoted[id] = true
|
pending = append(pending, id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for len(pending) != 0 {
|
||||||
|
id := pending[len(pending)-1]
|
||||||
|
pending = pending[:len(pending)-1]
|
||||||
|
index, ok := definitions[id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(definitions, id)
|
||||||
|
for _, reference := range e.resources[index].references {
|
||||||
|
used[reference] = true
|
||||||
|
if e.source != nil {
|
||||||
|
promoted[reference] = true
|
||||||
|
}
|
||||||
|
if _, ok := definitions[reference]; ok {
|
||||||
|
pending = append(pending, reference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user