feat(编辑渲染): 完善三维转换,优化编辑渲染压缩

This commit is contained in:
xiaoqidun committed 2026-10-07 10:26:20 +08:00
1 parent e8344e6387
commit 839f65e3d7
47 files changed
+4857 -82

No files matched your search

+8
View File
@@ -43,6 +43,14 @@ type RasterBackend interface {
Render(*RasterPage) (image.Image, error) Render(*RasterPage) (image.Image, error)
} }
// RasterBufferBackend 可选的目标缓冲绘制能力,不改变后端和采样精度
// 目标须为原点为零、尺寸与页面一致的紧密RGBA缓冲,不得与输入图像共享数据
// 绘制前清空目标像素,不保留目标引用;失败时目标可能包含部分结果
type RasterBufferBackend interface {
RasterBackend
RenderInto(*RasterPage, *image.RGBA) error
}
// PageCompiler 统一页面编译、文字提取和对象度量,不得修改源数据 // PageCompiler 统一页面编译、文字提取和对象度量,不得修改源数据
// 三项能力需采用相同的字体和几何语义,避免绘制、搜索与选区不一致 // 三项能力需采用相同的字体和几何语义,避免绘制、搜索与选区不一致
// renderer提供文档、字体配置和文字回调,实现中不得递归调用对应的Renderer方法 // renderer提供文档、字体配置和文字回调,实现中不得递归调用对应的Renderer方法
+3
View File
@@ -164,6 +164,9 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
return nil, err return nil, err
} }
} }
if err := snapshot.compressPatternPaints(); err != nil {
return nil, err
}
return &snapshot, nil return &snapshot, nil
} }
+164
View File
@@ -0,0 +1,164 @@
// 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 (
"crypto/sha256"
"reflect"
"slices"
)
const (
compressionPatternMinimum = 4096
compressionPatternLimit = 4096
compressionPatternUses = 65536
)
// compressionPatternPaint 保存可共享的底纹颜色,不修改输入对象
type compressionPatternPaint struct {
fill *FillColor
stroke *StrokeColor
count int
id string
}
// compressionPatternUse 记录输出快照内需要替换的直接对象
type compressionPatternUse struct {
page, layer, object int
paint *compressionPatternPaint
}
// compressPatternPaints 将新文档中重复的大底纹颜色提取为绘制参数,仅修改输出快照
// 返回: error 编码或取消错误
func (e *Editor) compressPatternPaints() error {
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected || e.source != nil {
return nil
}
paints := make(map[[32]byte]*compressionPatternPaint)
var uses []compressionPatternUse
for pi, page := range e.pages {
for li, layer := range page.Content.Layer {
if layer.DrawParam != "" {
continue
}
for oi, object := range layer.Objects {
if err := e.output.ctx.Err(); err != nil {
return err
}
if len(uses) == compressionPatternUses {
break
}
fill, stroke := compressionObjectPattern(object)
if fill == nil && stroke == nil {
continue
}
hash := sha256.New()
var size int64
x := newOFDXML(convertWriter{writer: hash, count: &size, context: e.output.ctx})
x.ctx = e.output.ctx
x.color("FillColor", fill)
x.color("StrokeColor", (*FillColor)(stroke))
if err := x.finish(); err != nil {
return err
}
if size < compressionPatternMinimum {
continue
}
key := [32]byte(hash.Sum(nil))
paint := paints[key]
if paint == nil {
if len(paints) == compressionPatternLimit {
continue
}
paint = &compressionPatternPaint{fill: fill, stroke: stroke}
paints[key] = paint
} else if !reflect.DeepEqual(paint.fill, fill) || !reflect.DeepEqual(paint.stroke, stroke) {
continue
}
paint.count++
uses = append(uses, compressionPatternUse{pi, li, oi, paint})
}
}
}
var copied bool
lastPage, lastLayer := -1, -1
for _, use := range uses {
if err := e.output.ctx.Err(); err != nil {
return err
}
if use.paint.count < 2 {
continue
}
if !copied {
e.pages = slices.Clone(e.pages)
e.resources = slices.Clone(e.resources)
copied = true
}
if use.paint.id == "" {
id, err := e.addEditorDrawParam(DrawParam{FillColor: use.paint.fill, StrokeColor: use.paint.stroke})
if err != nil {
return err
}
use.paint.id = id
}
if use.page != lastPage {
e.pages[use.page].Content.Layer = slices.Clone(e.pages[use.page].Content.Layer)
lastPage, lastLayer = use.page, -1
}
layer := &e.pages[use.page].Content.Layer[use.layer]
if use.layer != lastLayer {
layer.Objects = slices.Clone(layer.Objects)
lastLayer = use.layer
}
object := &layer.Objects[use.object]
if object.Type == "PathObject" {
object.PathObject.DrawParam = use.paint.id
object.PathObject.FillColor, object.PathObject.StrokeColor = nil, nil
} else {
object.TextObject.DrawParam = use.paint.id
object.TextObject.FillColor, object.TextObject.StrokeColor = nil, nil
}
}
return nil
}
// compressionObjectPattern 筛选没有原文及绘制参数继承的底纹对象
// 入参: object 图形对象
// 返回: *FillColor 填充颜色, *StrokeColor 描边颜色,不适用时均为空
func compressionObjectPattern(object GraphicObject) (*FillColor, *StrokeColor) {
if object.origin != nil {
return nil, nil
}
var fill *FillColor
var stroke *StrokeColor
switch object.Type {
case "PathObject":
if object.PathObject.DrawParam != "" {
return nil, nil
}
fill, stroke = object.PathObject.FillColor, object.PathObject.StrokeColor
case "TextObject":
if object.TextObject.DrawParam != "" {
return nil, nil
}
fill, stroke = object.TextObject.FillColor, object.TextObject.StrokeColor
default:
return nil, nil
}
if fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil {
return fill, stroke
}
return nil, nil
}
+1
View File
@@ -50,6 +50,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected { if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected {
return nil return nil
} }
generated = e.generatedVectorReferences(parts, generated)
names := make(map[string]bool) names := make(map[string]bool)
if reader != nil { if reader != nil {
for name := range reader.fileIndex { for name := range reader.fileIndex {
+1
View File
@@ -84,6 +84,7 @@ type editorResource struct {
vectorKey [32]byte vectorKey [32]byte
vectorStates map[string]editorCompositeState vectorStates map[string]editorCompositeState
references []string references []string
usage *editorVectorUsage
subset *editorFontSubset subset *editorFontSubset
states map[string]editorCompositeState states map[string]editorCompositeState
draw *DrawParam draw *DrawParam
+9 -2
View File
@@ -170,6 +170,14 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象 // 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象
// 返回: error 校验或取消错误,失败时不提交页面内容 // 返回: error 校验或取消错误,失败时不提交页面内容
func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error { func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
return (&editorValidation{Editor: e}).appendOwnedObjects(ctx, page, objects)
}
// appendOwnedObjects 在资源不变的构建会话内复用校验结果,接收对象所有权
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识的独立对象
// 返回: error 校验或取消错误
func (v *editorValidation) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
e := v.Editor
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -182,7 +190,6 @@ func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []Gra
if err := e.prepareSourceIDs(); err != nil { if err := e.prepareSourceIDs(); err != nil {
return err return err
} }
validation := &editorValidation{Editor: e}
for i, object := range objects { for i, object := range objects {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
@@ -190,7 +197,7 @@ func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []Gra
if object.origin != nil || editorObjectID(object) != "" { if object.origin != nil || editorObjectID(object) != "" {
return fmt.Errorf("owned objects must not have identifiers or original content") return fmt.Errorf("owned objects must not have identifiers or original content")
} }
prepared, err := validation.prepareObject("", object) prepared, err := v.prepareObject("", object)
if err != nil { if err != nil {
return err return err
} }
+36 -2
View File
@@ -39,7 +39,7 @@ type editorValidation struct {
*Editor *Editor
patterns map[*Pattern]bool patterns map[*Pattern]bool
composites map[string]bool composites map[string]bool
vectors map[string]*editorCompositeNode vectors map[string]bool
paths map[string]bool paths map[string]bool
} }
@@ -142,7 +142,7 @@ func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style Patter
return err return err
} }
} }
objects, _, err := e.selectedObjects(page, ids) objects, indexes, err := e.selectedObjects(page, ids)
if err != nil { if err != nil {
return err return err
} }
@@ -150,17 +150,51 @@ func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style Patter
objects[i] = cloneEditorData(objects[i]) objects[i] = cloneEditorData(objects[i])
object := &objects[i] object := &objects[i]
var fill *FillColor var fill *FillColor
var draw string
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
draw = object.TextObject.DrawParam
fill = object.TextObject.FillColor fill = object.TextObject.FillColor
if stroke { if stroke {
fill = (*FillColor)(object.TextObject.StrokeColor) fill = (*FillColor)(object.TextObject.StrokeColor)
} }
case "PathObject", "Path": case "PathObject", "Path":
draw = object.PathObject.DrawParam
fill = object.PathObject.FillColor fill = object.PathObject.FillColor
if stroke { if stroke {
fill = (*FillColor)(object.PathObject.StrokeColor) fill = (*FillColor)(object.PathObject.StrokeColor)
} }
default:
return fmt.Errorf("object %q does not use a pattern", ids[i])
}
if fill == nil {
base, err := e.editorDrawParam(e.pages[page].Content.Layer[indexes[i].layer].DrawParam, make(map[string]bool))
if err != nil {
return err
}
param, err := e.editorDrawParam(draw, make(map[string]bool))
if err != nil {
return err
}
paint := mergeDrawParam(*base, param)
fill = paint.FillColor
if stroke {
fill = (*FillColor)(paint.StrokeColor)
}
fill = cloneEditorData(fill)
if object.Type == "TextObject" {
if stroke {
object.TextObject.StrokeColor = (*StrokeColor)(fill)
} else {
object.TextObject.FillColor = fill
}
} else if object.Type == "PathObject" || object.Type == "Path" {
if stroke {
object.PathObject.StrokeColor = (*StrokeColor)(fill)
} else {
object.PathObject.FillColor = fill
}
}
} }
if fill == nil || fill.Pattern == nil { if fill == nil || fill.Pattern == nil {
return fmt.Errorf("object %q does not use a pattern", ids[i]) return fmt.Errorf("object %q does not use a pattern", ids[i])
+8 -7
View File
@@ -1097,21 +1097,18 @@ func (v *editorValidation) validateVector(id string) error {
if v.composites[id] { if v.composites[id] {
return fmt.Errorf("cyclic composite resource %q", id) return fmt.Errorf("cyclic composite resource %q", id)
} }
if v.vectors[id] {
return nil
}
if v.composites == nil { if v.composites == nil {
v.composites = make(map[string]bool) v.composites = make(map[string]bool)
v.vectors = make(map[string]*editorCompositeNode)
} }
v.composites[id] = true v.composites[id] = true
defer delete(v.composites, id) defer delete(v.composites, id)
node := v.vectors[id] node, err := v.compositeDefinition(id)
if node == nil {
var err error
node, err = v.compositeDefinition(id)
if err != nil { if err != nil {
return err return err
} }
v.vectors[id] = node
}
unit := node.object.CompositeGraphicUnit unit := node.object.CompositeGraphicUnit
if node.node.attr("Width") == "" || node.node.attr("Height") == "" || !finite(unit.Width) || !finite(unit.Height) || unit.Width < 0 || unit.Height < 0 { if node.node.attr("Width") == "" || node.node.attr("Height") == "" || !finite(unit.Width) || !finite(unit.Height) || unit.Width < 0 || unit.Height < 0 {
return fmt.Errorf("invalid vector dimensions") return fmt.Errorf("invalid vector dimensions")
@@ -1131,5 +1128,9 @@ func (v *editorValidation) validateVector(id string) error {
return err return err
} }
} }
if v.vectors == nil {
v.vectors = make(map[string]bool)
}
v.vectors[id] = true
return nil return nil
} }
+1 -1
View File
@@ -249,7 +249,7 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
return "", err return "", err
} }
draw.ID = e.nextID() draw.ID = e.nextID()
data, err := encodeOFDXML(func(x *ofdXML) { data, err := e.encodeXML(func(x *ofdXML) {
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}) x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
x.start("DrawParams", nil) x.start("DrawParams", nil)
attrs := ofdAttrs{} attrs := ofdAttrs{}
+20 -5
View File
@@ -202,13 +202,16 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" { if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
composite = true composite = true
origin := e.objectOrigin(editorObjectID(object)) origin := e.objectOrigin(editorObjectID(object))
var data []byte
var err error
if origin == nil { if origin == nil {
return nil, nil data, err = e.encodeXML(func(x *ofdXML) { x.object(object, true) })
} } else {
data, err := editorXMLObject(origin.data, origin.node, origin.object, object) data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
if err == nil { if err == nil {
data, err = editorXMLStandalone(data, origin.node) data, err = editorXMLStandalone(data, origin.node)
} }
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -252,18 +255,30 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
if err := progress.report("fonts", len(e.pages)+i, total); err != nil { if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
return nil, err return nil, err
} }
if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe { if resource.usage.matches(resource.name, resource.data) {
refs.merge(resource.usage.refs)
} else if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe {
return nil, nil return nil, nil
} }
} }
for id, usage := range refs.fonts { for id, usage := range refs.fonts {
glyphs := used[id] glyphs := used[id]
if usage.unsafe || len(usage.glyphs) != 0 { if usage.unsafe {
delete(used, id) delete(used, id)
} else if glyphs != nil { } else if glyphs != nil {
for char := range usage.chars { for char := range usage.chars {
glyphs[e.fonts[id].GlyphIndex(char)] = true glyphs[e.fonts[id].GlyphIndex(char)] = true
} }
if len(usage.glyphs) != 0 {
mapped[id] = true
for glyph := range usage.glyphs {
if glyph >= e.fonts[id].NumGlyphs() {
delete(used, id)
break
}
glyphs[glyph] = true
}
}
} }
} }
} }
+73 -1
View File
@@ -16,13 +16,17 @@ package ofdgo
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/xml" "encoding/xml"
"maps" "maps"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
) )
// editorGeneratedReferences 保存本次编码页面及其引用,不跨写出复用 // editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容
const editorVectorUsageLimit = 4096
// editorGeneratedReferences 保存本次输出条目及其引用,不跨写出复用
type editorGeneratedReferences struct { type editorGeneratedReferences struct {
data []byte data []byte
refs *editorResourceRefs refs *editorResourceRefs
@@ -39,9 +43,49 @@ func (r editorGeneratedReferences) matches(data []byte) bool {
return bytes.Equal(data, r.data) return bytes.Equal(data, r.data)
} }
// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效
type editorVectorUsage struct {
name string
digest [32]byte
refs *editorResourceRefs
}
// matches 核对资源路径和完整内容摘要,不以切片地址判断内容
// 入参: name 当前资源路径, data 当前资源内容
// 返回: bool 是否仍可使用编码时的引用
func (u *editorVectorUsage) matches(name string, data []byte) bool {
return u != nil && u.name == name && u.digest == sha256.Sum256(data)
}
// generatedVectorReferences 合并未改变的自产矢量引用,不修改调用方集合
// 入参: parts 输出条目, generated 本次已知引用
// 返回: map[string]editorGeneratedReferences 本次可复用引用
func (e *Editor) generatedVectorReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) map[string]editorGeneratedReferences {
copied := false
for _, resource := range e.resources {
if e.output != nil && e.output.ctx.Err() != nil {
return generated
}
data, ok := parts[resource.name]
if !ok || !resource.usage.matches(resource.name, data) {
continue
}
if !copied {
generated = maps.Clone(generated)
if generated == nil {
generated = make(map[string]editorGeneratedReferences)
}
copied = true
}
generated[resource.name] = editorGeneratedReferences{data: data, refs: resource.usage.refs}
}
return generated
}
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 // editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描
type editorReferenceScan struct { type editorReferenceScan struct {
refs *editorResourceRefs refs *editorResourceRefs
objects map[string]bool
name string name string
base string base string
stack []string stack []string
@@ -53,6 +97,26 @@ type editorReferenceScan struct {
seen bool seen bool
} }
// vectorUsage 保留完整且限额内的自产矢量引用,超限时保存流程重新扫描
// 入参: data 本次编码内容
// 返回: *editorVectorUsage 引用快照,无法复用时为空
func (s *editorReferenceScan) vectorUsage(data []byte) *editorVectorUsage {
if !s.safe || !s.seen || len(s.stack) != 0 {
return nil
}
count := len(s.refs.ids) + len(s.refs.files) + len(s.refs.fonts)
for _, usage := range s.refs.fonts {
count += len(usage.chars) + len(usage.glyphs)
if count > editorVectorUsageLimit {
return nil
}
}
if count > editorVectorUsageLimit {
return nil
}
return &editorVectorUsage{name: s.name, digest: sha256.Sum256(data), refs: s.refs}
}
// accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用 // accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用
// 入参: token 已解析或已编码的XML事件 // 入参: token 已解析或已编码的XML事件
// 返回: bool 是否仍可完整判断引用 // 返回: bool 是否仍可完整判断引用
@@ -99,6 +163,9 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
if attr.Name.Local != "ID" { if attr.Name.Local != "ID" {
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource) s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
} }
if s.objects != nil && editorObjectReference(attr.Name.Local) {
s.objects[attr.Value] = true
}
if attr.Name.Local == "Font" { if attr.Name.Local == "Font" {
usage = s.refs.fontUsage(attr.Value) usage = s.refs.fontUsage(attr.Value)
if token.Name.Local != "TextObject" && token.Name.Local != "Text" { if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
@@ -122,6 +189,11 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
return false return false
} }
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource) s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
if s.objects != nil && (token.Name.Local == "Thumbnail" || token.Name.Local == "Substitution") {
if id := editorResourceID(s.text.String()); id != "" {
s.objects[id] = true
}
}
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" { if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" {
s.refs.fontUsage(s.text.String()).unsafe = true s.refs.fontUsage(s.text.String()).unsafe = true
} }
+1
View File
@@ -60,6 +60,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
return nil, nil return nil, nil
} }
generated = e.generatedVectorReferences(parts, generated)
reader := e.source.reader reader := e.source.reader
names := make(map[string]bool) names := make(map[string]bool)
for name := range reader.fileIndex { for name := range reader.fileIndex {
+5 -1
View File
@@ -91,7 +91,11 @@ func (e *Editor) ApplyAppearance(page int, ids []string, appearance ObjectAppear
// 返回: *DrawParam 绘制参数, error 错误信息 // 返回: *DrawParam 绘制参数, error 错误信息
func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) { func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) {
if resolved { if resolved {
return e.editorDrawParam(id, make(map[string]bool)) draw, err := e.editorDrawParam(id, make(map[string]bool))
if err != nil {
return nil, err
}
return cloneEditorData(draw), nil
} }
for _, resource := range e.resources { for _, resource := range e.resources {
if resource.draw != nil && resource.draw.ID == id { if resource.draw != nil && resource.draw.ID == id {
+29 -3
View File
@@ -17,25 +17,41 @@ package ofdgo
import ( import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt"
"maps"
"slices"
) )
// addOwnedVector 将本库生成的独立对象注册为标准矢量资源,不保留内联复合内容 // addOwnedVector 将本库生成的独立对象注册为标准矢量资源,不保留内联复合内容
// 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度 // 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度
// 返回: string 资源编号, error 校验、编码或取消错误 // 返回: string 资源编号, error 校验、编码或取消错误
func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) { func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) {
validation := &editorValidation{Editor: e} return (&editorValidation{Editor: e}).addOwnedVector(ctx, objects, width, height)
}
// addOwnedVector 在资源不变的构建会话内登记矢量资源,复用资源校验结果
// 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度
// 返回: string 资源编号, error 校验、编码或取消错误
func (v *editorValidation) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) {
e := v.Editor
if !finite(width) || !finite(height) || width < 0 || height < 0 {
return "", fmt.Errorf("invalid vector dimensions")
}
for index, object := range objects { for index, object := range objects {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return "", err return "", err
} }
prepared, err := validation.prepareObject(e.nextID(), object) prepared, err := v.prepareObject(e.nextID(), object)
if err != nil { if err != nil {
return "", err return "", err
} }
objects[index] = prepared objects[index] = prepared
} }
id := e.nextID() id := e.nextID()
name := e.packageName("Res/Composites/Composite_" + id + ".xml")
scan := &editorReferenceScan{refs: &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}, objects: make(map[string]bool), name: name, safe: true}
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
x.references = scan
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}) x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
x.start("CompositeGraphicUnits", nil) x.start("CompositeGraphicUnits", nil)
x.start("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, x.start("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id},
@@ -56,10 +72,20 @@ func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, wi
if err != nil { if err != nil {
return "", err return "", err
} }
resource, err := e.compositeResource(id, data) resource := editorResource{name: name, data: data, composite: id}
if scan.safe && scan.seen && len(scan.stack) == 0 {
resource.references = slices.Sorted(maps.Keys(scan.objects))
resource.usage = scan.vectorUsage(data)
} else {
resource, err = e.compositeResource(id, data)
if err != nil { if err != nil {
return "", err return "", err
} }
}
e.resources = append(e.resources, resource) e.resources = append(e.resources, resource)
if v.vectors == nil {
v.vectors = make(map[string]bool)
}
v.vectors[id] = true
return id, ctx.Err() return id, ctx.Err()
} }
+11
View File
@@ -431,6 +431,17 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
} }
// imageAlphaAt 读取十六位透明度,原生图像避免颜色装箱
// 入参: img 图像, x 横坐标, y 纵坐标
// 返回: uint32 十六位透明度
func imageAlphaAt(img image.Image, x, y int) uint32 {
if source, ok := img.(image.RGBA64Image); ok {
return uint32(source.RGBA64At(x, y).A)
}
_, _, _, alpha := img.At(x, y).RGBA()
return alpha
}
// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱 // imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱
// 入参: img 图片对象, x X坐标, y Y坐标 // 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA64 NRGBA64像素 // 返回: color.NRGBA64 NRGBA64像素
+6 -7
View File
@@ -32,7 +32,7 @@ var ErrPDFPassword = pdfgo.ErrPassword
// Password默认使用PDF安全处理器编码,PasswordUTF8启用UTF-8输入及标准密码准备,空值尝试空密码 // Password默认使用PDF安全处理器编码,PasswordUTF8启用UTF-8输入及标准密码准备,空值尝试空密码
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告 // Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
// 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可提供已解密的引用页面,目标阅读器由调用方维护
@@ -86,12 +86,15 @@ type pdfImporter struct {
cmykSpace string cmykSpace string
iccSpaces map[pdfICCKey]string iccSpaces map[pdfICCKey]string
objects *pdfObjectBuffer objects *pdfObjectBuffer
formResources *pdfFormCache
validation *editorValidation
pages map[pdfgo.Reference]*pdfgo.Page pages map[pdfgo.Reference]*pdfgo.Page
pageIDs map[pdfgo.Reference]string pageIDs map[pdfgo.Reference]string
pageIndexes map[pdfgo.Reference]int pageIndexes map[pdfgo.Reference]int
imageIDs map[pdfImageKey][]pdfImageResource imageIDs map[pdfImageKey][]pdfImageResource
imageContents *renderCache[pdfImageContentKey, pdfImageContent] imageContents *renderCache[pdfImageContentKey, pdfImageContent]
attachmentIDs map[pdfAttachmentKey]string attachmentIDs map[pdfAttachmentKey]string
modelAttachments map[*pdfgo.Stream]string
maskClips map[*pdfgo.SoftMask]pdfgo.Path maskClips map[*pdfgo.SoftMask]pdfgo.Path
pageBox pdfgo.Rectangle pageBox pdfgo.Rectangle
pageWidth float64 pageWidth float64
@@ -308,7 +311,7 @@ func (p *pdfImporter) commitObjects() error {
if err != nil { if err != nil {
return err return err
} }
if err := p.editor.appendOwnedObjects(p.ctx, p.page, objects); err != nil { if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
return err return err
} }
p.objects = nil p.objects = nil
@@ -778,11 +781,7 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale") return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
} }
cell.objects, cell.pendingPath = nil, nil cell.objects, cell.pendingPath = nil, nil
visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image, Reference: p.referencePage} if err := source.Walk(p.ctx, base, cell.visitor()); err != nil {
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return cell.group(mark, walk, visitor)
}
if err := source.Walk(p.ctx, base, visitor); err != nil {
return nil, err return nil, err
} }
if err := cell.flushPath(); err != nil { if err := cell.flushPath(); err != nil {
+501
View File
@@ -0,0 +1,501 @@
// 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"
"errors"
"fmt"
"image"
"math"
"strings"
"github.com/xiaoqidun/pdfgo"
)
// pdfModelAttachmentLimit 限制单次转换保留的共享模型索引数量
const pdfModelAttachmentLimit = 1024
// PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
// 缓存与实例上限为0时采用U3DRenderOptions的默认值
type PDFThreeDRenderOptions struct {
Width, Height int
MaxMemoryBytes int64
MaxInstances int
}
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
// 入参: ctx 取消上下文, page PDF页面, annotation 三维注解
// 返回: error 外观、资源或取消错误
func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error {
source, err := p.reader.ReadThreeD(annotation)
if err != nil {
return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err)
}
var data []byte
id := p.modelAttachments[source.Stream]
if id == "" {
data, err = source.Stream.DecodeContext(ctx)
if err != nil {
return p.interactiveAppearanceFallback(ctx, page, annotation, "model", err)
}
id, err = p.editor.AddAttachment(fmt.Sprintf("Model_%d.%s", p.page+1, strings.ToLower(string(source.Format))), data)
if err != nil {
return err
}
if p.modelAttachments == nil {
p.modelAttachments = make(map[*pdfgo.Stream]string)
}
if len(p.modelAttachments) < pdfModelAttachmentLimit {
p.modelAttachments[source.Stream] = id
}
}
action := Action{Event: "CLICK", GotoA: &GotoA{AttachID: id}}
var paint func(*pdfImporter) error
if source.ActivationPolicy.Activation != "XA" {
appearance, err := p.reader.ReadAnnotationAppearance(annotation)
if err != nil {
return p.interactiveAppearanceFallback(ctx, page, annotation, "appearance", err)
}
if appearance == nil {
return fmt.Errorf("missing PDF 3D normal appearance")
}
if data == nil {
data, err = p.editor.source.reader.AttachmentData(id)
if err != nil {
return err
}
}
paint, err = p.threeDPainter(ctx, source, annotation, data)
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D rendering unavailable; normal appearance retained: " + err.Error()})
}
}
if err := p.paintedAnnotation(ctx, page, annotation, paint, action); err != nil {
return err
}
message := "PDF 3D appearance and original model retained; advanced interaction not transferred to OFD"
if paint != nil {
message = "PDF 3D static view and original model retained; interactive rendering not transferred to OFD"
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: message})
return nil
}
// threeDPainter 准备三维静态图像,复用库层压缩,失败时不提交注解对象
// 入参: ctx 取消上下文, source 三维参数, annotation 注解, data 原始模型
// 返回: func(*pdfImporter) error 绘制回调, error 模型、绘制或资源错误
func (p *pdfImporter) threeDPainter(ctx context.Context, source pdfgo.ThreeD, annotation pdfgo.Annotation, data []byte) (func(*pdfImporter) error, error) {
box := source.ViewBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin
x, y := annotation.Rect.XMin/2+annotation.Rect.XMax/2, annotation.Rect.YMin/2+annotation.Rect.YMax/2
var nodes []pdfCompositeNode
if source.Background.RGB == nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
}
if source.Background.EntireAnnotation {
r := annotation.Rect
path := pdfgo.Path{Segments: []pdfgo.Segment{
{Operator: "M", Points: []pdfgo.Point{{X: r.XMin, Y: r.YMin}}},
{Operator: "L", Points: []pdfgo.Point{{X: r.XMax, Y: r.YMin}}},
{Operator: "L", Points: []pdfgo.Point{{X: r.XMax, Y: r.YMax}}},
{Operator: "L", Points: []pdfgo.Point{{X: r.XMin, Y: r.YMax}}},
{Operator: "C"},
}}
nodes = append(nodes, pdfCompositeNode{path: &pdfgo.PathMark{Path: path, Fill: true, Style: pdfgo.Style{Fill: pdfgo.Paint{RGB: *source.Background.RGB, Alpha: 1}}}})
}
if width > 0 && height > 0 {
if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
}
model, err := DecodeU3D(ctx, data, U3DOptions{})
if err != nil {
return nil, err
}
widthMM := width * math.Hypot(p.matrix[0], p.matrix[1])
heightMM := height * math.Hypot(p.matrix[2], p.matrix[3])
w, h := math.Ceil(widthMM*p.rasterDPI/25.4), math.Ceil(heightMM*p.rasterDPI/25.4)
if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 {
return nil, fmt.Errorf("invalid PDF 3D raster dimensions")
}
img, err := RenderPDFThreeD(ctx, model, source, PDFThreeDRenderOptions{Width: int(w), Height: int(h)})
if err != nil {
return nil, err
}
resource, err := p.reader.CreateImage(ctx, img)
if err != nil {
return nil, err
}
nodes = append(nodes, pdfCompositeNode{image: &pdfgo.ImageMark{Image: resource, Matrix: pdfgo.Matrix{width, 0, 0, height, x + box.XMin, y + box.YMin}, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}})
}
return func(stamp *pdfImporter) error {
if err := ctx.Err(); err != nil {
return err
}
return stamp.compositeObjects(nodes)
}, nil
}
// RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制3DB区域,不执行脚本或动画
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
// 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误
func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, options PDFThreeDRenderOptions) (*image.RGBA, error) {
if ctx == nil {
return nil, fmt.Errorf("invalid PDF 3D render context")
}
if err := ctx.Err(); err != nil {
return nil, err
}
if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
}
presentation := source.Presentation
if presentation.RenderMode != "" && presentation.RenderMode != "Solid" || presentation.Overlay != nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D view render mode or overlay"}
}
if source.Background.RGB == nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
}
box := source.ViewBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid PDF 3D viewport")
}
projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"}
if source.Projection != nil {
projection = *source.Projection
}
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB}
if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil {
return nil, err
}
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
return nil, err
}
r, err := newU3DRender(ctx, model, render)
if err != nil {
return nil, err
}
camera, view, err := r.pdfCamera(source.Camera)
if err != nil {
return nil, err
}
render.View = view
camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1
render.Camera = &camera
passes, err := r.selectView(render)
if err != nil {
return nil, err
}
if err := r.prepareSections(render.Sections); err != nil {
return nil, err
}
scale := 0.0
switch projection.Subtype {
case "P":
if !finite(projection.FieldOfView) || projection.FieldOfView <= 0 || projection.FieldOfView >= 180 {
return nil, &pdfgo.UnsupportedError{Feature: "degenerate 3D perspective field of view"}
}
diameter := projection.PerspectiveDiameter
if projection.PerspectiveBinding != "" {
diameter, err = pdfThreeDBinding(projection.PerspectiveBinding, width, height)
if err != nil {
return nil, err
}
}
if !finite(diameter) || diameter <= 0 {
return nil, fmt.Errorf("invalid PDF 3D perspective diameter")
}
scale = diameter / (2 * math.Tan(projection.FieldOfView*math.Pi/360))
camera.Perspective = true
case "O":
width := source.AnnotationBox.XMax - source.AnnotationBox.XMin
height := source.AnnotationBox.YMax - source.AnnotationBox.YMin
binding, err := pdfThreeDBinding(projection.OrthographicBinding, width, height)
if err != nil {
return nil, err
}
scale = projection.OrthographicScale * binding
default:
return nil, fmt.Errorf("invalid PDF 3D projection subtype")
}
camera.ScaleX = scale * float64(options.Width) / width
camera.ScaleY = scale * float64(options.Height) / height
switch projection.Clipping {
case "XNF":
camera.Near = 0
if projection.Near != nil {
camera.Near = *projection.Near
}
if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 {
return nil, fmt.Errorf("invalid PDF 3D near distance")
}
if projection.Far != nil {
camera.ClipFar = true
camera.Far = *projection.Far
if !finite(camera.Far) {
return nil, fmt.Errorf("invalid PDF 3D far distance")
}
if camera.Far < camera.Near {
passes, camera.Far = nil, 0
camera.ClipFar = false
}
}
case "ANF":
minimum, maximum, found, err := r.depthRange()
if err != nil {
return nil, err
}
if !found {
passes = nil
}
camera.Near = minimum
if camera.Perspective {
if maximum <= 0 {
passes = nil
}
camera.Near = max(1e-300, maximum*1e-9)
if minimum > 0 {
camera.Near = max(1e-300, minimum/2)
}
}
camera.Far = math.Nextafter(maximum, math.Inf(1))
if !finite(camera.Far) || camera.Far <= camera.Near {
camera.Far = 0
}
default:
return nil, fmt.Errorf("invalid PDF 3D clipping style")
}
if _, err := r.selectView(render); err != nil {
return nil, err
}
if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" {
if err := r.reserve(uint64(len(source.Lighting.Lights)), 56); err != nil {
return nil, err
}
render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))}
for i, light := range source.Lighting.Lights {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
direction := light.Direction
if light.CameraAttached {
direction = [3]float64{0, 0, 1}
}
render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached}
}
}
return r.render(render, passes)
}
// pdfThreeDBinding 计算目标区域的标准绑定倍率,不按模型包围盒重新适配
// 入参: binding 绑定方式, width 区域宽度, height 区域高度
// 返回: float64 倍率, error 方式或尺寸错误
func pdfThreeDBinding(binding pdfgo.Name, width, height float64) (float64, error) {
if binding == "Absolute" {
return 1, nil
}
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return 0, fmt.Errorf("invalid PDF 3D binding dimensions")
}
switch binding {
case "W":
return width, nil
case "H":
return height, nil
case "Min":
return min(width, height), nil
case "Max":
return max(width, height), nil
}
return 0, fmt.Errorf("invalid PDF 3D binding")
}
// pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次
// 入参: source 相机来源
// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误
func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, error) {
var camera U3DCamera
selected := -1
for i, node := range r.model.Nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Kind == "View" {
selected = i
break
}
}
if source.Source == "M" {
m := source.Matrix
camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
} else {
if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 {
return camera, "", fmt.Errorf("invalid PDF 3D camera source")
}
if source.Source == "U3D" {
selected = -1
for i, node := range r.model.Nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Name == source.Path[len(source.Path)-1] {
selected = i
break
}
}
}
if selected < 0 || r.model.Nodes[selected].Kind != "View" {
return camera, "", fmt.Errorf("missing PDF 3D camera node")
}
found := false
for _, index := range r.byNode[selected] {
if err := r.ctx.Err(); err != nil {
return camera, "", err
}
current, match := index, true
for j := len(source.Path) - 1; j >= 0; j-- {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return camera, "", err
}
}
if current < 0 || r.model.Nodes[r.scene[current].node].Name != source.Path[j] || r.model.Nodes[r.scene[current].node].Kind != "View" {
match = false
break
}
current = r.scene[current].parent
}
if source.Source == "U3D" && current >= 0 {
match = false
}
if !match {
continue
}
if found {
return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"}
}
camera.ToWorld, found = r.scene[index].world, true
}
if !found {
return camera, "", fmt.Errorf("missing PDF 3D camera path")
}
}
view := ""
if selected >= 0 {
view = r.model.Nodes[selected].Name
}
return camera, view, nil
}
// depthRange 计算可见模型面的相机深度范围,不为自动裁剪复制顶点
// 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误
func (r *u3dRender) depthRange() (float64, float64, bool, error) {
minimum, maximum := math.Inf(1), math.Inf(-1)
for i, instance := range r.scene {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
node := r.model.Nodes[instance.node]
index, ok := r.meshes[node.Resource]
if node.Kind != "Model" || instance.modelVisibility(&node) == 0 || instance.opacity == 0 || !ok {
continue
}
mesh := &r.model.Meshes[index]
matrix := r.toCamera.multiply(instance.world)
for j, face := range mesh.Faces {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
for _, corner := range face.Corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, 0, false, fmt.Errorf("invalid U3D position index")
}
p := mesh.Positions[corner.Position]
z := matrix[2]*float64(p[0]) + matrix[6]*float64(p[1]) + matrix[10]*float64(p[2]) + matrix[14]
if !finite(z) {
return 0, 0, false, fmt.Errorf("invalid U3D camera depth")
}
minimum, maximum = min(minimum, z), max(maximum, z)
}
}
}
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
if section.opacity > 0 {
for _, corner := range section.corners {
minimum, maximum = min(minimum, corner.point[2]), max(maximum, corner.point[2])
}
}
}
if math.IsInf(minimum, 1) {
return 0, 0, false, nil
}
return minimum, maximum, true, nil
}
// pdfThreeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算
// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项
// 返回: error 预算或取消错误
func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *U3DRenderOptions) error {
if len(nodes) == 0 {
return nil
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = u3dDefaultBytes
}
if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 {
return fmt.Errorf("PDF 3D node memory exceeds limit")
}
options.MaxMemoryBytes = limit - int64(len(nodes))*168
options.Nodes = make([]U3DNodeState, len(nodes))
for i, node := range nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
state := U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible}
if node.Matrix != nil {
m := node.Matrix
state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
}
options.Nodes[i] = state
}
return nil
}
+70
View File
@@ -0,0 +1,70 @@
// 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"
"github.com/xiaoqidun/pdfgo"
)
// pdfThreeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点
// 入参: ctx 取消上下文, sections PDF剖切面, options 绘制选项
// 返回: error 方向、预算或取消错误
func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *U3DRenderOptions) error {
if len(sections) == 0 {
return nil
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = u3dDefaultBytes
}
if limit <= 0 || uint64(len(sections)) >= uint64(limit)/136 {
return fmt.Errorf("PDF 3D section memory exceeds limit")
}
options.MaxMemoryBytes = limit - int64(len(sections))*136
options.Sections = make([]U3DCrossSection, len(sections))
for i, source := range sections {
if err := ctx.Err(); err != nil {
return err
}
if source.Axis < 0 || source.Axis > 2 {
return fmt.Errorf("invalid PDF 3D section axis")
}
section := U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity}
section.AxisU[(source.Axis+1)%3], section.AxisV[(source.Axis+2)%3] = 1, 1
for axis, angle := range source.Rotation {
if axis == source.Axis {
continue
}
if !finite(angle) {
return fmt.Errorf("invalid PDF 3D section rotation")
}
sin, cos := math.Sincos(math.Remainder(angle, 360) * math.Pi / 180)
a, b := (axis+1)%3, (axis+2)%3
for _, vector := range []*[3]float64{&section.AxisU, &section.AxisV} {
vector[a], vector[b] = cos*vector[a]-sin*vector[b], sin*vector[a]+cos*vector[b]
}
}
if source.IntersectionVisible {
color := source.IntersectionColor
section.Intersection = &color
}
options.Sections[i] = section
}
return nil
}
+13
View File
@@ -29,6 +29,13 @@ import (
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作 // 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作
// 返回: error 外观或转换错误 // 返回: error 外观或转换错误
func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error { func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error {
return p.paintedAnnotation(ctx, page, annotation, nil, actions...)
}
// paintedAnnotation 复用注解坐标、标志与元数据处理,允许替换静态绘制内容
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作
// 返回: error 外观或转换错误
func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error {
for _, key := range []pdfgo.Name{"AA", "OC", "A"} { for _, key := range []pdfgo.Name{"AA", "OC", "A"} {
if annotation.Subtype == "Widget" && (key == "A" || key == "AA") { if annotation.Subtype == "Widget" && (key == "A" || key == "AA") {
continue continue
@@ -74,6 +81,11 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
stamp.compositeCache = nil stamp.compositeCache = nil
stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject) stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
stamp.pageWidth, stamp.pageHeight = box.W, box.H stamp.pageWidth, stamp.pageHeight = box.W, box.H
if paint != nil {
if err := paint(&stamp); err != nil {
return err
}
} else {
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error { nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
if annotation.Subtype == "Widget" { if annotation.Subtype == "Widget" {
visitor.MarkedContent = func(mark pdfgo.MarkedContentMark) error { visitor.MarkedContent = func(mark pdfgo.MarkedContentMark) error {
@@ -98,6 +110,7 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
if err := stamp.compositeObjects(nodes); err != nil { if err := stamp.compositeObjects(nodes); err != nil {
return err return err
} }
}
if flags&(2|32) == 0 && !invisible { if flags&(2|32) == 0 && !invisible {
p.compositeNodes = stamp.compositeNodes p.compositeNodes = stamp.compositeNodes
p.transferBackdrop = stamp.transferBackdrop p.transferBackdrop = stamp.transferBackdrop
+9
View File
@@ -25,6 +25,15 @@ type pdfObjectBuffer struct {
size int size int
} }
// objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器
// 返回: *editorValidation 当前编辑器的构建校验会话
func (p *pdfImporter) objectValidation() *editorValidation {
if p.validation == nil || p.validation.Editor != p.editor {
p.validation = &editorValidation{Editor: p.editor}
}
return p.validation
}
// appendObject 追加本次导入对象,按需建立独立暂存缓冲 // appendObject 追加本次导入对象,按需建立独立暂存缓冲
// 入参: object 独立导入对象 // 入参: object 独立导入对象
func (p *pdfImporter) appendObject(object GraphicObject) { func (p *pdfImporter) appendObject(object GraphicObject) {
+1 -1
View File
@@ -51,7 +51,7 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
for index := range members { for index := range members {
*editorObjectClips(&members[index]) = nil *editorObjectClips(&members[index]) = nil
} }
id, err := p.editor.addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight) id, err := p.objectValidation().addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+34 -2
View File
@@ -33,6 +33,7 @@ type pdfCompositeNode struct {
children []pdfCompositeNode children []pdfCompositeNode
textObject *pdfgo.TextObject textObject *pdfgo.TextObject
glyph *pdfgo.TextMark glyph *pdfgo.TextMark
formCount int
} }
// pdfCompositePixel 保存非预乘源空间分量及累计透明度 // pdfCompositePixel 保存非预乘源空间分量及累计透明度
@@ -160,7 +161,15 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y { if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil, nil return nil, nil
} }
return c.importer.renderGroupScene(geometry.scene, c.box) target, err := c.acquireCoverage()
if err != nil {
return nil, err
}
coverage, err := c.importer.renderGroupSceneInto(geometry.scene, c.box, target)
if err != nil {
c.releaseCoverage(target)
}
return coverage, err
} }
// geometry 缓存图元或组的实际覆盖场景与像素对齐边界 // geometry 缓存图元或组的实际覆盖场景与像素对齐边界
@@ -431,6 +440,17 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
nodes = append(nodes, pdfCompositeNode{group: &mark, children: children}) nodes = append(nodes, pdfCompositeNode{group: &mark, children: children})
return nil return nil
} }
v.Form = func(_ pdfgo.FormMark, walk func(pdfgo.Visitor) error) error {
children, err := collect(walk)
if err != nil {
return err
}
if len(children) != 0 {
children[0].formCount = len(children)
nodes = append(nodes, children...)
}
return nil
}
err := walk(v) err := walk(v)
return nodes, err return nodes, err
} }
@@ -503,7 +523,19 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
// 返回: error 转换或合成错误 // 返回: error 转换或合成错误
func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error { func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
space := p.compositeSpace space := p.compositeSpace
for i, node := range nodes { for i := 0; i < len(nodes); i++ {
node := nodes[i]
if count := node.formCount; count >= 16 && count <= len(nodes)-i && !p.transferBackdrop {
packed, err := p.compositeForm(nodes[i : i+count])
if err != nil {
return fmt.Errorf("convert form at graphic %d: %w", i+1, err)
}
if packed {
p.compositeNodes = append(p.compositeNodes, nodes[i:i+count]...)
i += count - 1
continue
}
}
direct, err := p.directCompositeNode(node, space) direct, err := p.directCompositeNode(node, space)
if err != nil { if err != nil {
return fmt.Errorf("inspect graphic %d: %w", i+1, err) return fmt.Errorf("inspect graphic %d: %w", i+1, err)
+2 -1
View File
@@ -109,6 +109,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if err != nil || coverage == nil { if err != nil || coverage == nil {
return err return err
} }
defer c.releaseCoverage(coverage)
style, _, _ := node.style() style, _, _ := node.style()
var transfer *pdfgo.TransferFunction var transfer *pdfgo.TransferFunction
imageShape := false imageShape := false
@@ -198,7 +199,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
return err return err
} }
for x := region.Min.X; x < region.Max.X; x++ { for x := region.Min.X; x < region.Max.X; x++ {
_, _, _, a := coverage.At(x, y).RGBA() a := imageAlphaAt(coverage, x, y)
if a == 0 { if a == 0 {
continue continue
} }
+45
View File
@@ -14,6 +14,8 @@
package ofdgo package ofdgo
import "image"
// pdfCompositeTileSize 限定单次合成块的像素边长 // pdfCompositeTileSize 限定单次合成块的像素边长
const pdfCompositeTileSize = 256 const pdfCompositeTileSize = 256
@@ -24,6 +26,49 @@ type pdfCompositeBuffers[T any] [8][]T
type pdfCompositeScratch struct { type pdfCompositeScratch struct {
pixels pdfCompositeBuffers[pdfCompositePixel] pixels pdfCompositeBuffers[pdfCompositePixel]
masks pdfCompositeBuffers[float64] masks pdfCompositeBuffers[float64]
rasters [8]*image.RGBA
}
// acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取
// 返回: *image.RGBA 未清空画布,不支持缓冲绘制的后端返回nil, error 无效绘制区域
func (c *pdfCompositor) acquireCoverage() (*image.RGBA, error) {
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok {
return nil, nil
}
page := RasterPage{Width: c.box.W, Height: c.box.H, DPI: c.importer.rasterDPI}
w, h, err := page.PixelSize()
if err != nil {
return nil, err
}
if c.scratch == nil {
c.scratch = &pdfCompositeScratch{}
}
rect := image.Rect(0, 0, w, h)
for i, stored := range c.scratch.rasters {
if stored != nil && stored.Rect == rect {
c.scratch.rasters[i] = nil
return stored, nil
}
}
return image.NewRGBA(rect), nil
}
// releaseCoverage 归还当前调用不再读取的画布,限制单次合成保留的内存
// 入参: coverage 当前调用的覆盖图像
func (c *pdfCompositor) releaseCoverage(coverage image.Image) {
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok || c.scratch == nil {
return
}
buffer, ok := coverage.(*image.RGBA)
if !ok || buffer == nil || cap(buffer.Pix) > 4*pdfCompositeTileSize*pdfCompositeTileSize {
return
}
for i, stored := range c.scratch.rasters {
if stored == nil {
c.scratch.rasters[i] = buffer
return
}
}
} }
// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲 // acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
+2 -1
View File
@@ -80,6 +80,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
c.releasePixels(result) c.releasePixels(result)
return nil, err return nil, err
} }
defer c.releaseCoverage(coverage)
step := 25.4 / c.importer.rasterDPI step := 25.4 / c.importer.rasterDPI
for i := range result { for i := range result {
if i%pdfCompositeTileSize == 0 { if i%pdfCompositeTileSize == 0 {
@@ -90,7 +91,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
} }
shape := 0.0 shape := 0.0
if coverage != nil { if coverage != nil {
_, _, _, a := coverage.At(i%c.width, i/c.width).RGBA() a := imageAlphaAt(coverage, i%c.width, i/c.width)
shape = float64(a) / 65535 shape = float64(a) / 65535
} }
if nodes[0].image != nil && shape != 0 { if nodes[0].image != nil && shape != 0 {
+142
View File
@@ -0,0 +1,142 @@
// 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"
"crypto/sha256"
"encoding/xml"
)
// pdfFormBufferLimit 限制表单编码暂存容量,不限制资源大小
const pdfFormBufferLimit = 8 << 20
// pdfFormResource 保存生成内容及资源编号,复用前核对完整内容
type pdfFormResource struct {
content []byte
id string
}
// pdfFormCache 隔离编辑器的表单资源,复用编码缓冲而不共享缓存内容
type pdfFormCache struct {
editor *Editor
entries renderCache[[32]byte, pdfFormResource]
buffer bytes.Buffer
}
// encode 按原顺序编码对象分块,不为缓存检查整理连续对象数组
// 入参: ctx 取消上下文, objects 表单对象, width 页面宽度, height 页面高度
// 返回: []byte 下次编码或重置前有效的内容, error 编码或取消错误
func (c *pdfFormCache) encode(ctx context.Context, objects *pdfObjectBuffer, width, height float64) ([]byte, error) {
c.buffer.Reset()
x := newOFDXML(&c.buffer)
x.ctx = ctx
x.root("Content", ofdAttrs{{Name: xml.Name{Local: "Width"}, Value: ofdNumber(width)},
{Name: xml.Name{Local: "Height"}, Value: ofdNumber(height)}})
for _, part := range objects.parts {
for _, object := range part {
x.object(object, false)
if x.err != nil {
return nil, x.err
}
}
}
x.end("Content")
if err := x.finish(); err != nil {
return nil, err
}
return c.buffer.Bytes(), nil
}
// reset 清空临时内容,释放超限缓冲,不修改已登记资源
func (c *pdfFormCache) reset() {
if c.buffer.Cap() > pdfFormBufferLimit {
c.buffer = bytes.Buffer{}
} else {
c.buffer.Reset()
}
}
// compositeForm 将无需背景合成的普通表单保存为矢量资源,保留图元的实际坐标和裁剪
// 入参: nodes 普通表单连续图元
// 返回: bool 是否已保存为复合对象, error 检查或转换错误
func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
if pdfCompositeHasTransfer(nodes) {
return false, nil
}
for _, node := range nodes {
if err := p.ctx.Err(); err != nil {
return false, err
}
direct, err := p.directCompositeNode(node, p.compositeSpace)
if err != nil || !direct {
return false, err
}
}
if err := p.flushPath(); err != nil {
return false, err
}
parent := p.objects
p.objects = nil
defer func() { p.objects = parent }()
visitor := p.visitor()
for _, node := range nodes {
if err := node.emit(visitor); err != nil {
return false, err
}
}
if err := p.flushPath(); err != nil {
return false, err
}
if err := p.ctx.Err(); err != nil {
return false, err
}
if p.objects.len() == 0 {
return true, nil
}
if p.formResources == nil || p.formResources.editor != p.editor {
p.formResources = &pdfFormCache{editor: p.editor, entries: renderCache[[32]byte, pdfFormResource]{limit: 32 << 20}}
}
cache := p.formResources
defer cache.reset()
content, err := cache.encode(p.ctx, p.objects, p.pageWidth, p.pageHeight)
if err != nil {
return false, err
}
key := sha256.Sum256(content)
resource, found := cache.entries.get(key)
if !found || !bytes.Equal(resource.content, content) {
objects, err := p.objects.data(p.ctx)
if err != nil {
return false, err
}
id, err := p.objectValidation().addOwnedVector(p.ctx, objects, p.pageWidth, p.pageHeight)
if err != nil {
return false, err
}
resource = pdfFormResource{id: id}
if len(content) <= cache.entries.limit-len(id)-128 {
resource.content = bytes.Clone(content)
cache.entries.put(key, resource, cap(resource.content)+len(id)+128)
}
}
p.objects = parent
p.appendObject(GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: resource.id,
}})
parent = p.objects
return true, nil
}
+13
View File
@@ -158,11 +158,24 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
} }
evenOdd = false evenOdd = false
} }
var clipped GeometryPath
var linear bool
var clipErr error
if rectangle {
clipped, linear, clipErr = clipLinearFillGeometry(path, clip)
}
if clipErr != nil {
return nil, Box{}, clipErr
}
if linear {
path = clipped
} else {
path, err = clipGeometry(geometry, path, &clip) path, err = clipGeometry(geometry, path, &clip)
if err != nil { if err != nil {
return nil, Box{}, err return nil, Box{}, err
} }
} }
}
if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, nil, IdentityMatrix, nil); err != nil { if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, nil, IdentityMatrix, nil); err != nil {
return nil, Box{}, err return nil, Box{}, err
} }
+15
View File
@@ -435,6 +435,13 @@ func (p *pdfImporter) groupSceneBounds(scene *RasterPage) (*RasterPage, Box, err
// 入参: source 原始场景, box 页面裁取区域 // 入参: source 原始场景, box 页面裁取区域
// 返回: image.Image 透明图像, error 后端渲染错误 // 返回: image.Image 透明图像, error 后端渲染错误
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) { func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
return p.renderGroupSceneInto(source, box, nil)
}
// renderGroupSceneInto 在指定区域绘制场景,按所选后端能力复用独占目标
// 入参: source 原始场景, box 页面裁取区域, target 可选目标缓冲
// 返回: image.Image 透明图像, error 后端渲染错误
func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target *image.RGBA) (image.Image, error) {
scene := *source scene := *source
scene.DPI = p.rasterDPI scene.DPI = p.rasterDPI
scene.Commands = append([]RasterCommand(nil), source.Commands...) scene.Commands = append([]RasterCommand(nil), source.Commands...)
@@ -457,6 +464,14 @@ func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image
if backend == nil { if backend == nil {
return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable) return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable)
} }
if target != nil {
if buffered, ok := backend.(RasterBufferBackend); ok {
if err := buffered.RenderInto(&scene, target); err != nil {
return nil, err
}
return target, nil
}
}
return backend.Render(&scene) return backend.Render(&scene)
} }
+23 -19
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"math" "math"
"net/url" "net/url"
@@ -134,33 +135,19 @@ func (p *pdfImporter) movieAnnotation(ctx context.Context, page *pdfgo.Page, ann
// 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, strict 严格检查开关 // 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, strict 严格检查开关
// 返回: error 外观、资源或不可等价转换错误 // 返回: error 外观、资源或不可等价转换错误
func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error { func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error {
if err := ctx.Err(); err != nil {
return err
}
if strict { if strict {
return &pdfgo.UnsupportedError{Feature: "interactive " + string(annotation.Subtype) + " conversion"} return &pdfgo.UnsupportedError{Feature: "interactive " + string(annotation.Subtype) + " conversion"}
} }
var actions []Action var actions []Action
if annotation.Subtype == "3D" { if annotation.Subtype == "3D" {
model, err := p.reader.ReadThreeD(annotation) return p.threeDAnnotation(ctx, page, annotation)
if err != nil {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D metadata unavailable; static appearance retained: " + err.Error()})
return p.appearanceAnnotation(ctx, page, annotation)
}
data, err := model.Stream.Decode()
if err != nil {
return err
}
id, err := p.editor.AddAttachment(fmt.Sprintf("Model_%d.%s", p.page+1, strings.ToLower(string(model.Format))), data)
if err != nil {
return err
}
actions = append(actions, Action{Event: "CLICK", GotoA: &GotoA{AttachID: id}})
} else { } else {
media, err := p.reader.ReadRichMedia(ctx, annotation) media, err := p.reader.ReadRichMedia(ctx, annotation)
if err != nil { if err != nil {
if ctx.Err() != nil { return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err)
return ctx.Err()
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF rich media metadata unavailable; static appearance retained: " + err.Error()})
return p.appearanceAnnotation(ctx, page, annotation)
} }
config := media.Configurations[media.ActiveConfiguration] config := media.Configurations[media.ActiveConfiguration]
var playing *pdfgo.FileSpecification var playing *pdfgo.FileSpecification
@@ -245,6 +232,23 @@ func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Pag
return nil return nil
} }
// interactiveAppearanceFallback 保留交互注解的有效正常外观,取消时不继续转换
// 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, kind 失败资源类型, cause 原始错误
// 返回: error 取消或外观转换错误
func (p *pdfImporter) interactiveAppearanceFallback(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, kind string, cause error) error {
if err := ctx.Err(); err != nil {
return err
}
if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) {
return cause
}
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("PDF %s %s unavailable; normal appearance retained: %v", annotation.Subtype, kind, cause)})
return nil
}
// mediaData 读取媒体或附件,宽松模式缺少外部读取器时报告资源不可用 // mediaData 读取媒体或附件,宽松模式缺少外部读取器时报告资源不可用
// 入参: ctx 取消上下文, file PDF文件说明 // 入参: ctx 取消上下文, file PDF文件说明
// 返回: []byte 原始文件, bool 是否可用, error 解码、外部读取或严格模式错误 // 返回: []byte 原始文件, bool 是否可用, error 解码、外部读取或严格模式错误
+1 -4
View File
@@ -252,10 +252,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err return err
} }
visitor := pdfgo.Visitor{Path: p.path, Image: p.image, Warning: p.warning, Reference: p.referencePage} visitor := p.visitor()
visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return p.group(group, walk, visitor)
}
for index := range mark.Glyphs { for index := range mark.Glyphs {
if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil { if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil {
return err return err
+70
View File
@@ -0,0 +1,70 @@
// 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 (
"crypto/sha256"
"encoding/binary"
"math"
"github.com/tdewolff/canvas"
)
// canvasGeometryDigest 按完整坐标、路径顺序及几何容差计算摘要
// 入参: paths 非空路径序列
// 返回: [32]byte 几何摘要
func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
hash := sha256.New()
var data [512]byte
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
hash.Write(data[:24])
for _, path := range paths {
values := path.Data()
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
hash.Write(data[:8])
for len(values) > 0 {
count := min(len(values), len(data)/8)
for i, value := range values[:count] {
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
}
hash.Write(data[:count*8])
values = values[count:]
}
}
var key [32]byte
hash.Sum(key[:0])
return key
}
// settleCanvasPath 复用复杂路径的填充规范化结果,返回独立副本
// 入参: path 页面坐标路径, rule 填充规则
// 返回: *canvas.Path 规范化填充轮廓
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
if len(path.Data()) < 256 {
return path.Settle(rule)
}
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
cache := r.canvasSettledPaths()
if result, ok := cache.get(key); ok {
return result.Copy()
}
result := path.Settle(rule)
if cost := len(result.Data())*8 + 256; cost <= cache.limit {
cache.put(key, result.Copy(), cost)
}
return result
}
+24
View File
@@ -55,6 +55,30 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
return nil, err return nil, err
} }
img := image.NewRGBA(image.Rect(0, 0, w, h)) img := image.NewRGBA(image.Rect(0, 0, w, h))
return b.renderImage(page, img)
}
// RenderInto 在调用方独占的紧密RGBA缓冲中重新绘制,不修改页面或保留缓冲
// 入参: page 只读页面, target 原点为零且尺寸匹配的目标,不与输入图像共享数据
// 返回: error 参数或绘制错误,绘制失败时目标可能包含部分结果
func (b CanvasBackend) RenderInto(page *RasterPage, target *image.RGBA) error {
w, h, err := page.PixelSize()
if err != nil {
return err
}
if target == nil || target.Rect != image.Rect(0, 0, w, h) || target.Stride != 4*w || len(target.Pix) < 4*w*h {
return fmt.Errorf("invalid canvas target buffer")
}
clear(target.Pix[:4*w*h])
_, err = b.renderImage(page, target)
return err
}
// renderImage 将页面绘制到已清空且尺寸匹配的目标
// 入参: page 只读页面, img 独占目标
// 返回: image.Image 目标图像, error 绘制错误
func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Image, error) {
h := img.Rect.Dy()
space := b.ColorSpace space := b.ColorSpace
if space == nil { if space == nil {
space = canvas.DefaultColorSpace space = canvas.DefaultColorSpace
+33 -24
View File
@@ -15,10 +15,7 @@
package ofdgo package ofdgo
import ( import (
"crypto/sha256"
"encoding/binary"
"image" "image"
"math"
"slices" "slices"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -195,27 +192,7 @@ func (r *Renderer) intersectCachedClipPath(parent, current *canvas.Path) *canvas
if parent == nil || current == nil || len(parent.Data())+len(current.Data()) < 256 { if parent == nil || current == nil || len(parent.Data())+len(current.Data()) < 256 {
return intersectClipPath(parent, current) return intersectClipPath(parent, current)
} }
hash := sha256.New() key := canvasGeometryDigest(parent, current)
var data [512]byte
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
hash.Write(data[:24])
for _, path := range []*canvas.Path{parent, current} {
values := path.Data()
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
hash.Write(data[:8])
for len(values) > 0 {
count := min(len(values), len(data)/8)
for i, value := range values[:count] {
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
}
hash.Write(data[:count*8])
values = values[count:]
}
}
var key [32]byte
hash.Sum(key[:0])
cache := r.canvasClipIntersections() cache := r.canvasClipIntersections()
if path, ok := cache.get(key); ok { if path, ok := cache.get(key); ok {
return path.Copy() return path.Copy()
@@ -310,6 +287,8 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil { if path == nil || clip == nil {
return path return path
} }
path = reduceRepeatedNonZeroPath(path)
clip = reduceRepeatedNonZeroPath(clip)
if slices.Equal(path.Data(), clip.Data()) { if slices.Equal(path.Data(), clip.Data()) {
return path return path
} }
@@ -340,6 +319,36 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
return path.And(clip) return path.And(clip)
} }
// reduceRepeatedNonZeroPath 将完全相同的闭合轮廓约为一份,保持非零填充区域
// 入参: path 非零填充路径
// 返回: *canvas.Path 等价填充路径
func reduceRepeatedNonZeroPath(path *canvas.Path) *canvas.Path {
if path == nil {
return path
}
data := path.Data()
if len(data) < 8 || data[0] != canvas.MoveToCmd {
return path
}
end := 0
scanner := path.Scanner()
for scanner.Scan() {
if scanner.Cmd() == canvas.MoveToCmd && end != 0 {
break
}
end += len(scanner.Values()) + 2
}
if end == len(data) || data[end-1] != canvas.CloseCmd || len(data)%end != 0 {
return path
}
for i := end; i < len(data); i += end {
if !slices.Equal(data[:end], data[i:i+end]) {
return path
}
}
return canvas.NewPathFromData(slices.Clone(data[:end]))
}
// applyFillClipPath 为多段描边直接填充裁剪,不将相消轮廓用于几何测量 // applyFillClipPath 为多段描边直接填充裁剪,不将相消轮廓用于几何测量
// 入参: path 非零填充描边, clip 裁剪路径 // 入参: path 非零填充描边, clip 裁剪路径
// 返回: *canvas.Path 绘制轮廓 // 返回: *canvas.Path 绘制轮廓
+19
View File
@@ -400,6 +400,25 @@ func geometryFromCanvasPath(path *canvas.Path) *GeometryPath {
return &result return &result
} }
// clipLinearFillGeometry 裁剪直线填充轮廓,保留原填充规则,不用于区域布尔运算
// 入参: path 闭合绘制轮廓, clip 凸裁剪窗口
// 返回: GeometryPath 填充轮廓, bool 是否适用, error 路径错误
func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error) {
a, err := geometryToCanvasPath(&path)
if err != nil {
return nil, false, err
}
b, err := geometryToCanvasPath(&clip)
if err != nil {
return nil, false, err
}
result, ok := intersectLinearCanvasContours(a, b)
if !ok {
return nil, false, nil
}
return *geometryFromCanvasPath(result), true, nil
}
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴 // actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
// 入参: matrix 页面变换 // 入参: matrix 页面变换
// 返回: canvas.Matrix 路径变换 // 返回: canvas.Matrix 路径变换
+4 -1
View File
@@ -253,13 +253,16 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
} }
fp := paths.Merge() fp := paths.Merge()
fillRule := canvas.NonZero fillRule := canvas.NonZero
if fillClip != nil && !fp.Bounds().Overlaps(fillClip.Bounds()) {
fp = &canvas.Path{}
}
if obj.Rule == "Even-Odd" { if obj.Rule == "Even-Odd" {
fillRule = canvas.EvenOdd fillRule = canvas.EvenOdd
if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) { if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) {
fillClip = nil fillClip = nil
} }
if fillClip != nil || style.fillPattern != nil { if fillClip != nil || style.fillPattern != nil {
fp = fp.Settle(fillRule) fp = r.settleCanvasPath(fp, fillRule)
} }
} }
if fillClip != nil { if fillClip != nil {
+24
View File
@@ -33,6 +33,15 @@ type canvasClipCacheKey struct{}
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存 // canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
type canvasPathCacheKey struct{} type canvasPathCacheKey struct{}
// canvasFillCacheKey 区分按填充规则规范化的共享路径缓存
type canvasFillCacheKey struct{}
// canvasFillKey 保存完整几何摘要及填充规则
type canvasFillKey struct {
digest [32]byte
rule canvas.FillRule
}
// canvasPathKey 按完整路径文本与几何容差区分解析结果 // canvasPathKey 按完整路径文本与几何容差区分解析结果
type canvasPathKey struct { type canvasPathKey struct {
data string data string
@@ -78,6 +87,21 @@ func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path]
return cache return cache
} }
// canvasSettledPaths 获取共享填充轮廓缓存,限制结果保留量
// 返回: *renderCache[canvasFillKey, *canvas.Path] 填充轮廓缓存
func (r *Renderer) canvasSettledPaths() *renderCache[canvasFillKey, *canvas.Path] {
key := canvasFillCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[canvasFillKey, *canvas.Path])
}
if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any)
}
cache := &renderCache[canvasFillKey, *canvas.Path]{limit: 16 << 20}
r.sharedBackendStates[key] = cache
return cache
}
// canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量 // canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量
// 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存 // 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存
func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] { func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] {
+589
View File
@@ -0,0 +1,589 @@
// 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"
"encoding/binary"
"fmt"
"io"
"math"
)
// u3dDefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销
const u3dDefaultBytes = 256 << 20
// U3DOptions 控制U3D解码规模,MaxDecodedBytes为0时采用256MiB上限
type U3DOptions struct {
MaxDecodedBytes int64
}
// U3DModel 保存独立于PDF的静态U3D场景,不执行动作或读取外部文件
type U3DModel struct {
Major, Minor int16
DefinedUnits bool
Units float64
Meshes []U3DMesh
Nodes []U3DNode
Materials []U3DMaterial
Shaders []U3DShader
Lights []U3DLight
Views []U3DViewResource
Metadata []U3DMetadata
}
// U3DMetadata 保留块所属名称与原始元数据,数据不与输入共享存储
type U3DMetadata struct {
Block uint32
Name string
Data []byte
}
// U3DNode 保存组、模型、光源或视图节点,父节点矩阵按列存储
type U3DNode struct {
Name, Kind, Resource string
Parents []U3DParent
Visibility uint32
Shaders [][]string
View *U3DView
}
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
type U3DParent struct {
Name string
Transform [16]float32
}
// U3DMaterial 保存材质参数,Attributes各位决定相应参数是否生效
type U3DMaterial struct {
Name string
Attributes uint32
Ambient, Diffuse, Specular, Emissive [3]float32
Reflectivity, Opacity float32
}
// U3DShader 保存无纹理着色器参数,不提前应用混合、透明测试或光照
type U3DShader struct {
Name, Material string
Attributes, AlphaFunction, Blend uint32
RenderPass uint32
AlphaReference float32
}
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
type U3DLight struct {
Name string
Attributes uint32
Kind byte
Color, Attenuation [3]float32
SpotAngle, Intensity float32
}
// u3dBlock 保存校验后的块边界,数据切片仅在解码期间引用输入
type u3dBlock struct {
kind uint32
data, meta []byte
}
// u3dDecoder 保存单次解码预算及场景索引,不跨调用缓存输入
type u3dDecoder struct {
ctx context.Context
model U3DModel
remaining uint64
plain bool
meshes map[string]*u3dMeshDeclaration
meshOrder []string
nodes map[string]int
palettes map[uint32]map[string]bool
}
// DecodeU3D 按ECMA-363解码静态基础网格、组、材质、着色器和光源
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景
// 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel, error) {
if ctx == nil || options.MaxDecodedBytes < 0 {
return nil, fmt.Errorf("invalid U3D decode options")
}
if err := ctx.Err(); err != nil {
return nil, err
}
limit := options.MaxDecodedBytes
if limit == 0 {
limit = u3dDefaultBytes
}
d := u3dDecoder{ctx: ctx, remaining: uint64(limit), meshes: make(map[string]*u3dMeshDeclaration), nodes: make(map[string]int), palettes: make(map[uint32]map[string]bool)}
header, next, err := u3dReadBlock(data, 0)
if err != nil {
return nil, err
}
if header.kind != 0x00443355 {
return nil, fmt.Errorf("invalid U3D file header")
}
r := u3dValues{data: header.data}
version, profile := r.u32(), r.u32()
d.model.Major, d.model.Minor = int16(version), int16(version>>16)
if d.model.Major > 0 || profile & ^uint32(14) != 0 {
return nil, fmt.Errorf("unsupported U3D version or profile")
}
declarationEnd := uint64(r.u32())
fileSize := uint64(r.u32())
fileSize |= uint64(r.u32()) << 32
if r.u32() != 106 {
return nil, fmt.Errorf("unsupported U3D character encoding")
}
d.plain = profile&4 != 0
d.model.DefinedUnits, d.model.Units = profile&8 != 0, 1
if d.model.DefinedUnits {
unitBytes := r.take(8)
if r.err == nil {
d.model.Units = math.Float64frombits(binary.LittleEndian.Uint64(unitBytes))
if !finite(d.model.Units) || d.model.Units <= 0 {
return nil, fmt.Errorf("invalid U3D units")
}
}
}
if err := r.done(); err != nil {
return nil, err
}
if fileSize != uint64(len(data)) || declarationEnd < uint64(next) || declarationEnd > fileSize || declarationEnd%4 != 0 {
return nil, fmt.Errorf("invalid U3D file size")
}
if err := d.metadata(header, ""); err != nil {
return nil, err
}
priority := uint32(0)
for offset := next; offset < len(data); {
if err := ctx.Err(); err != nil {
return nil, err
}
block, end, err := u3dReadBlock(data, offset)
if err != nil {
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
}
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
}
if block.kind == 0xffffff15 {
r := u3dValues{data: block.data}
value := r.u32()
if err := r.done(); err != nil {
return nil, err
}
if value == 0 || value < priority || value > 0x7fffffff {
return nil, fmt.Errorf("invalid U3D block priority")
}
priority = value
err = d.metadata(block, "")
} else if block.kind == 0xffffff14 {
err = d.chain(block)
} else {
_, err = d.block(block, "", -1, 0)
}
if err != nil {
return nil, fmt.Errorf("U3D block %#x at %d: %w", block.kind, offset, err)
}
offset = end
}
for _, name := range d.meshOrder {
mesh := d.meshes[name]
if !mesh.decoded && mesh.counts[1] != 0 {
return nil, fmt.Errorf("missing U3D base mesh %q", name)
}
d.model.Meshes = append(d.model.Meshes, mesh.mesh)
}
if err := d.checkParents(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return &d.model, nil
}
// u3dReadBlock 按64位长度计算校验块数据与零填充,避免长度溢出
// 入参: data 块序列, offset 块起点
// 返回: u3dBlock 块, int 下一块起点, error 边界或填充错误
func u3dReadBlock(data []byte, offset int) (u3dBlock, int, error) {
if offset < 0 || offset > len(data) || len(data)-offset < 12 {
return u3dBlock{}, 0, io.ErrUnexpectedEOF
}
header := data[offset : offset+12]
size, meta := uint64(binary.LittleEndian.Uint32(header[4:])), uint64(binary.LittleEndian.Uint32(header[8:]))
start := uint64(offset) + 12
metaStart := start + ((size + 3) &^ 3)
end := metaStart + ((meta + 3) &^ 3)
if end > uint64(len(data)) {
return u3dBlock{}, 0, io.ErrUnexpectedEOF
}
for _, padding := range [][]byte{data[start+size : metaStart], data[metaStart+meta : end]} {
for _, value := range padding {
if value != 0 {
return u3dBlock{}, 0, fmt.Errorf("invalid U3D padding")
}
}
}
return u3dBlock{binary.LittleEndian.Uint32(header), data[start : start+size], data[metaStart : metaStart+meta]}, int(end), nil
}
// reserve 在分配解码数组前检查总预算及取消状态
// 入参: count 元素数量, size 元素规模上界
// 返回: error 预算或取消错误
func (d *u3dDecoder) reserve(count, size uint64) error {
if err := d.ctx.Err(); err != nil {
return err
}
if size == 0 || count > d.remaining/size || count > uint64(^uint(0)>>1)/size {
return fmt.Errorf("U3D decoded data exceeds limit")
}
d.remaining -= count * size
return nil
}
// metadata 校验元数据结构并保留原始数据,不解释或执行其中的动作
// 入参: block 数据块, name 所属资源名
// 返回: error 结构、预算或取消错误
func (d *u3dDecoder) metadata(block u3dBlock, name string) error {
if len(block.meta) == 0 {
return nil
}
if err := d.reserve(uint64(len(block.meta))+128, 1); err != nil {
return err
}
r := u3dValues{data: block.meta}
count := r.u32()
if uint64(count) > uint64(len(block.meta))/8 {
return fmt.Errorf("invalid U3D metadata count")
}
for range count {
if err := d.ctx.Err(); err != nil {
return err
}
flags := r.u32()
r.text()
if flags&1 != 0 {
size := r.u32()
if uint64(size) > uint64(len(r.data)-r.pos) {
return io.ErrUnexpectedEOF
}
r.take(int(size))
} else {
r.text()
}
if r.err != nil {
return r.err
}
}
if err := r.done(); err != nil {
return err
}
d.model.Metadata = append(d.model.Metadata, U3DMetadata{block.kind, name, bytes.Clone(block.meta)})
return nil
}
// chain 解析单层修饰链并校验名称、链类型与修饰顺序
// 入参: block 修饰链块
// 返回: error 结构、能力或资源错误
func (d *u3dDecoder) chain(block u3dBlock) error {
if err := d.reserve(256, 1); err != nil {
return err
}
r := u3dValues{data: block.data}
name, kind, flags := r.text(), r.u32(), r.u32()
if kind > 2 || flags & ^uint32(3) != 0 {
return fmt.Errorf("invalid U3D modifier chain")
}
if flags&1 != 0 {
for range 4 {
r.f32()
}
}
if flags&2 != 0 {
for range 6 {
r.f32()
}
}
for r.pos%4 != 0 && r.err == nil {
if r.u8() != 0 {
return fmt.Errorf("invalid U3D chain padding")
}
}
count := r.u32()
if r.err != nil {
return r.err
}
if uint64(count) > uint64(len(r.data)-r.pos)/12 {
return fmt.Errorf("invalid U3D modifier count")
}
for index := uint32(0); index < count; index++ {
if err := d.ctx.Err(); err != nil {
return err
}
nested, next, err := u3dReadBlock(r.data, r.pos)
if err != nil {
return err
}
actual, err := d.block(nested, name, int(kind), index)
if err != nil {
return err
}
if actual != name {
return fmt.Errorf("U3D modifier name mismatch")
}
r.pos = next
}
if err := r.done(); err != nil {
return err
}
return d.metadata(block, name)
}
// block 解析支持的静态场景块,不丢弃未实现的修饰器或几何
// 入参: block 数据块, owner 所属链名, chain 链类别,负值表示独立块, index 修饰位置
// 返回: string 资源名, error 格式、能力或资源错误
func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32) (string, error) {
if err := d.ctx.Err(); err != nil {
return "", err
}
if err := d.reserve(uint64(len(block.data))+256, 1); err != nil {
return "", err
}
r := u3dValues{data: block.data}
name := r.text()
var err error
switch block.kind {
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
if chain >= 0 && (chain != 0 || index != 0) {
return name, fmt.Errorf("invalid U3D node chain")
}
err = d.node(&r, block.kind, name)
case 0xffffff31:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D mesh chain")
}
err = d.meshDeclaration(&r, name)
case 0xffffff3b:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D mesh continuation location")
}
err = d.baseMesh(&r, name)
case 0xffffff45:
if chain != 0 || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D shading modifier chain")
}
err = d.shading(&r, name)
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D palette resource location")
}
err = d.resource(&r, block.kind, name)
default:
return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
}
if err != nil {
return name, err
}
if err := r.done(); err != nil {
return name, err
}
return name, d.metadata(block, name)
}
// node 读取节点与父变换,允许引用后续声明的父节点
// 入参: r 字段读取器, kind 节点类型, name 节点名
// 返回: error 结构或预算错误
func (d *u3dDecoder) node(r *u3dValues, kind uint32, name string) error {
if _, exists := d.nodes[name]; exists {
return fmt.Errorf("duplicate U3D node %q", name)
}
node := U3DNode{Name: name, Kind: "Group"}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/66 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 96); err != nil {
return err
}
node.Parents = make([]U3DParent, int(count))
for index := range node.Parents {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
node.Parents[index].Name = r.text()
for element := range node.Parents[index].Transform {
node.Parents[index].Transform[element] = r.f32()
}
}
if kind == 0xffffff22 {
node.Kind, node.Resource, node.Visibility = "Model", r.text(), r.u32()
if node.Visibility > 3 {
return fmt.Errorf("invalid U3D model visibility")
}
} else if kind == 0xffffff23 {
node.Kind, node.Resource = "Light", r.text()
} else if kind == 0xffffff24 {
node.Kind, node.Resource = "View", r.text()
var err error
node.View, err = d.view(r)
if err != nil {
return err
}
}
d.nodes[name] = len(d.model.Nodes)
d.model.Nodes = append(d.model.Nodes, node)
return r.err
}
// shading 读取网格着色列表,保留列表内各着色器的先后次序
// 入参: r 字段读取器, name 目标节点名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) shading(r *u3dValues, name string) error {
index, exists := d.nodes[name]
if !exists || d.model.Nodes[index].Kind != "Model" {
return fmt.Errorf("missing U3D shading target")
}
if r.u32() != 1 {
return fmt.Errorf("unsupported U3D non-mesh shading modifier")
}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/4 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 24); err != nil {
return err
}
lists := make([][]string, int(count))
for index := range lists {
n := r.u32()
if uint64(n) > uint64(len(r.data)-r.pos)/2 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(n), 16); err != nil {
return err
}
lists[index] = make([]string, int(n))
for j := range lists[index] {
if j%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
lists[index][j] = r.text()
}
}
d.model.Nodes[index].Shaders = lists
return r.err
}
// resource 读取独立材质、着色器、光源和视图资源
// 入参: r 字段读取器, kind 资源类型, name 资源名
// 返回: error 结构或能力错误
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
palette := d.palettes[kind]
if palette == nil {
palette = make(map[string]bool)
d.palettes[kind] = palette
}
if palette[name] {
return fmt.Errorf("duplicate U3D resource %q", name)
}
palette[name] = true
switch kind {
case 0xffffff52:
return d.viewResource(r, name)
case 0xffffff54:
material := U3DMaterial{Name: name, Attributes: r.u32()}
if material.Attributes & ^uint32(63) != 0 {
return fmt.Errorf("invalid U3D material attributes")
}
for _, color := range []*[3]float32{&material.Ambient, &material.Diffuse, &material.Specular, &material.Emissive} {
for index := range color {
color[index] = r.f32()
}
}
material.Reflectivity, material.Opacity = r.f32(), r.f32()
d.model.Materials = append(d.model.Materials, material)
case 0xffffff53:
shader := U3DShader{Name: name, Attributes: r.u32(), AlphaReference: r.f32(), AlphaFunction: r.u32(), Blend: r.u32(), RenderPass: r.u32()}
channels, alpha := r.u32(), r.u32()
shader.Material = r.text()
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")
}
if channels != 0 {
return fmt.Errorf("unsupported U3D textured shader")
}
d.model.Shaders = append(d.model.Shaders, shader)
case 0xffffff51:
light := U3DLight{Name: name, Attributes: r.u32(), Kind: r.u8()}
if light.Attributes & ^uint32(7) != 0 || light.Kind > 3 {
return fmt.Errorf("invalid U3D light parameters")
}
for index := range light.Color {
light.Color[index] = r.f32()
}
if r.f32() != 1 {
return fmt.Errorf("invalid U3D light reserved parameter")
}
for index := range light.Attenuation {
light.Attenuation[index] = r.f32()
}
light.SpotAngle, light.Intensity = r.f32(), r.f32()
d.model.Lights = append(d.model.Lights, light)
}
return r.err
}
// checkParents 迭代检查场景父子环路,不递归展开多父节点的实例组合
// 返回: error 环路或取消错误
func (d *u3dDecoder) checkParents() error {
state := make([]byte, len(d.model.Nodes))
type frame struct{ node, parent int }
var stack []frame
for index := range d.model.Nodes {
if state[index] != 0 {
continue
}
stack = append(stack[:0], frame{node: index})
state[index] = 1
for len(stack) != 0 {
if err := d.ctx.Err(); err != nil {
return err
}
current := &stack[len(stack)-1]
parents := d.model.Nodes[current.node].Parents
if current.parent == len(parents) {
state[current.node] = 2
stack = stack[:len(stack)-1]
continue
}
parent, exists := d.nodes[parents[current.parent].Name]
current.parent++
if !exists || state[parent] == 2 {
continue
}
if state[parent] == 1 {
return fmt.Errorf("cyclic U3D node parents")
}
state[parent] = 1
stack = append(stack, frame{node: parent})
}
}
return nil
}
+301
View File
@@ -0,0 +1,301 @@
// 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 (
"encoding/binary"
"fmt"
"io"
"math"
"math/bits"
"unicode/utf8"
)
// u3dValues 读取U3D未压缩字段,不允许越过当前块
type u3dValues struct {
data []byte
pos int
err error
}
// u3dBits 按ECMA-363第10章逆解16位算术区间,字节内按低位优先读取
type u3dBits struct {
data []byte
pos uint64
low, high, code uint32
err error
}
// u3dHistogram 保存一个动态压缩上下文的频次及前缀和
type u3dHistogram struct {
counts []uint32
tree []uint32
total uint32
}
// take 读取定长字段,失败后不继续推进位置
// 入参: size 字节数
// 返回: []byte 字段数据,失败时为空
func (r *u3dValues) take(size int) []byte {
if r.err != nil {
return nil
}
if size < 0 || size > len(r.data)-r.pos {
r.err = io.ErrUnexpectedEOF
return nil
}
value := r.data[r.pos : r.pos+size]
r.pos += size
return value
}
// u8 读取一个未压缩字节
// 返回: byte 数值
func (r *u3dValues) u8() byte {
value := r.take(1)
if value == nil {
return 0
}
return value[0]
}
// u32 读取小端32位整数
// 返回: uint32 数值
func (r *u3dValues) u32() uint32 {
value := r.take(4)
if value == nil {
return 0
}
return binary.LittleEndian.Uint32(value)
}
// f32 读取有限单精度数值
// 返回: float32 数值
func (r *u3dValues) f32() float32 {
value := math.Float32frombits(r.u32())
if !finite(float64(value)) && r.err == nil {
r.err = fmt.Errorf("invalid U3D floating-point value")
}
return value
}
// text 读取带16位字节长度的UTF-8字符串
// 返回: string 文本
func (r *u3dValues) text() string {
length := r.take(2)
if length == nil {
return ""
}
data := r.take(int(binary.LittleEndian.Uint16(length)))
if r.err == nil && !utf8.Valid(data) {
r.err = fmt.Errorf("invalid U3D UTF-8 string")
}
return string(data)
}
// done 检查字段是否恰好覆盖当前块
// 返回: error 读取或多余字段错误
func (r *u3dValues) done() error {
if r.err != nil {
return r.err
}
if r.pos != len(r.data) {
return fmt.Errorf("unexpected U3D block data")
}
return nil
}
// newU3DBits 初始化当前块的独立算术解码状态
// 入参: data 块数据
// 返回: *u3dBits 解码器
func newU3DBits(data []byte) *u3dBits {
r := &u3dBits{data: data, high: 0xffff}
for range 16 {
r.code = r.code<<1 | r.bit()
}
return r
}
// bit 读取低位优先比特,区间预读最多允许16个末尾零位
// 返回: uint32 比特
func (r *u3dBits) bit() uint32 {
if r.err != nil {
return 0
}
limit := uint64(len(r.data)) * 8
var value uint32
if r.pos < limit {
value = uint32(r.data[r.pos/8]>>(r.pos%8)) & 1
} else if r.pos >= limit+16 {
r.err = io.ErrUnexpectedEOF
return 0
}
r.pos++
return value
}
// cumulative 求当前编码值在上下文累计频次中的位置
// 入参: total 总频次
// 返回: uint32 累计频次位置
func (r *u3dBits) cumulative(total uint32) uint32 {
if r.err != nil {
return 0
}
if total == 0 || r.code < r.low || r.code > r.high {
r.err = fmt.Errorf("invalid U3D arithmetic interval")
return 0
}
return uint32((uint64(r.code-r.low+1)*uint64(total) - 1) / uint64(r.high-r.low+1))
}
// advance 收缩符号区间并执行同位及下溢归一化
// 入参: start 累计频次, count 符号频次, total 总频次
func (r *u3dBits) advance(start, count, total uint32) {
if r.err != nil {
return
}
if count == 0 || start >= total || count > total-start {
r.err = fmt.Errorf("invalid U3D symbol frequency")
return
}
width := uint64(r.high-r.low) + 1
r.high = r.low + uint32(width*uint64(start+count)/uint64(total)) - 1
r.low += uint32(width * uint64(start) / uint64(total))
for r.err == nil {
var offset uint32
switch {
case r.high < 0x8000:
case r.low >= 0x8000:
offset = 0x8000
case r.low >= 0x4000 && r.high < 0xc000:
offset = 0x4000
default:
return
}
r.low = (r.low - offset) << 1
r.high = (r.high-offset)<<1 | 1
r.code = (r.code-offset)<<1 | r.bit()
}
}
// byteValue 逆解静态256符号上下文中的反序字节
// 返回: byte 原始字节
func (r *u3dBits) byteValue() byte {
value := r.cumulative(256)
r.advance(value, 1, 256)
return bits.Reverse8(byte(value))
}
// u32 逆解四个小端字节,不重置压缩区间
// 返回: uint32 原始整数
func (r *u3dBits) u32() uint32 {
var value uint32
for shift := range 4 {
value |= uint32(r.byteValue()) << (8 * shift)
}
return value
}
// index 读取静态上下文的网格索引,大范围按标准使用未压缩整数
// 入参: count 索引范围
// 返回: uint32 索引
func (r *u3dBits) index(count uint32) uint32 {
if count > 0x3ffe {
return r.u32()
}
value := r.cumulative(count)
r.advance(value, 1, count)
return value
}
// dynamic 读取动态上下文数值,逃逸后的字面量仍使用当前算术区间
// 入参: h 频次上下文
// 返回: uint32 原始整数
func (r *u3dBits) dynamic(h *u3dHistogram) uint32 {
if h.total == 0 {
h.counts = make([]uint32, 1)
h.tree = make([]uint32, 2)
h.add(0)
}
index := r.cumulative(h.total)
symbol, start := h.find(index)
if r.err != nil {
return 0
}
r.advance(start, h.counts[symbol], h.total)
h.add(symbol)
if symbol != 0 {
return symbol - 1
}
value := r.u32()
if value < 0xffff && r.err == nil {
h.add(value + 1)
}
return value
}
// find 以树状前缀和定位符号,避免按符号数量线性扫描
// 入参: position 累计频次位置
// 返回: uint32 符号, uint32 符号前缀频次
func (h *u3dHistogram) find(position uint32) (uint32, uint32) {
index, prefix := 0, uint32(0)
for step := 1 << (bits.Len(uint(len(h.tree))) - 1); step != 0; step >>= 1 {
next := index + step
if next < len(h.tree) && h.tree[next] <= position-prefix {
prefix += h.tree[next]
index = next
}
}
return uint32(index), prefix
}
// add 更新动态频次,并按ECMA-363阈值缩减历史频次
// 入参: symbol 符号,超过16位的值不加入上下文
func (h *u3dHistogram) add(symbol uint32) {
if symbol > 0xffff {
return
}
if h.total >= 0x1fff {
for index := range h.counts {
h.counts[index] >>= 1
}
h.counts[0]++
h.rebuild()
}
if int(symbol) >= len(h.counts) {
length := min(65536, max(int(symbol)+1, len(h.counts)*2))
h.counts = append(h.counts, make([]uint32, length-len(h.counts))...)
h.rebuild()
}
h.counts[symbol]++
h.total++
for index := int(symbol) + 1; index < len(h.tree); index += index & -index {
h.tree[index]++
}
}
// rebuild 在线性时间内重建动态频次索引
func (h *u3dHistogram) rebuild() {
h.tree = make([]uint32, len(h.counts)+1)
h.total = 0
for index, count := range h.counts {
h.total += count
i := index + 1
h.tree[i] += count
if next := i + (i & -i); next < len(h.tree) {
h.tree[next] += h.tree[i]
}
}
}
+326
View File
@@ -0,0 +1,326 @@
// 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"
)
// U3DDirectionalLight 定义无限远光源,Direction为发射方向,CameraRelative使用相机坐标
type U3DDirectionalLight struct {
Color, Direction [3]float64
CameraRelative bool
}
// U3DLighting 替换场景灯光,空灯光列表表示无光源,DiffuseOffset加到受光材质的漫反射颜色
// 颜色及漫反射增量的各分量取值为0到1;不修改材质自发光或不受光着色器
type U3DLighting struct {
Lights []U3DDirectionalLight
DiffuseOffset [3]float64
}
// u3dRenderLight 保存世界空间的光源位置与正Z发射方向
type u3dRenderLight struct {
value U3DLight
position, direction u3dVector
}
// u3dRenderStyle 保存当前着色器的有效材质参数
type u3dRenderStyle struct {
shader U3DShader
ambient, diffuse, specular, emissive [3]float64
opacity, exponent float64
}
// collectLights 收集本轮根节点下启用的光源实例
// 返回: error 参数、预算或取消错误
func (r *u3dRender) collectLights() error {
if r.lighting != nil {
return nil
}
r.lights = r.lights[:0]
for i, instance := range r.scene {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
node := r.model.Nodes[instance.node]
if !r.members[i] || node.Kind != "Light" {
continue
}
j, ok := r.lightIndex[node.Resource]
if !ok {
continue
}
light := r.model.Lights[j]
if light.Attributes&1 == 0 {
continue
}
if light.Attributes > 7 || light.Kind > 3 {
return fmt.Errorf("invalid U3D light")
}
for _, v := range light.Color {
if !finite(float64(v)) {
return fmt.Errorf("invalid U3D light color")
}
}
if light.Kind != 0 && !finite(float64(light.Intensity)) {
return fmt.Errorf("invalid U3D light intensity")
}
if light.Kind >= 2 {
for _, v := range light.Attenuation {
if !finite(float64(v)) {
return fmt.Errorf("invalid U3D light attenuation")
}
}
}
if light.Kind == 3 && (!finite(float64(light.SpotAngle)) || light.SpotAngle <= 0 || light.SpotAngle > 180) {
return fmt.Errorf("invalid U3D spot angle")
}
world := instance.world
item := u3dRenderLight{value: light, position: u3dVector{world[12], world[13], world[14]}, direction: (u3dVector{world[8], world[9], world[10]}).unit()}
if (light.Kind == 1 || light.Kind == 3) && item.direction == (u3dVector{}) {
return fmt.Errorf("invalid U3D light direction")
}
if len(r.lights) == cap(r.lights) {
capacity := max(8, cap(r.lights)*2)
if err := r.reserve(uint64(capacity), 128); err != nil {
return err
}
values := make([]u3dRenderLight, len(r.lights), capacity)
copy(values, r.lights)
r.remaining += uint64(cap(r.lights)) * 128
r.lights = values
}
r.lights = append(r.lights, item)
}
return nil
}
// overrideLighting 校验并建立替代光源,不依赖模型光源节点
// 入参: lighting 灯光覆盖,空值保留模型灯光
// 返回: error 参数、预算或取消错误
func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
if lighting == nil {
return nil
}
for _, v := range lighting.DiffuseOffset {
if !finite(v) || v < 0 || v > 1 {
return fmt.Errorf("invalid U3D diffuse offset")
}
}
if err := r.reserve(uint64(len(lighting.Lights)), 128); err != nil {
return err
}
r.lights = make([]u3dRenderLight, len(lighting.Lights))
for i, light := range lighting.Lights {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
for j, v := range light.Color {
if !finite(v) || v < 0 || v > 1 || !finite(light.Direction[j]) {
return fmt.Errorf("invalid U3D directional light")
}
}
direction := u3dVector(light.Direction).unit()
if light.CameraRelative && r.camera.ToWorld != ([16]float64{}) {
m := r.camera.ToWorld
direction = u3dVector{m[0]*direction[0] + m[4]*direction[1] + m[8]*direction[2], m[1]*direction[0] + m[5]*direction[1] + m[9]*direction[2], m[2]*direction[0] + m[6]*direction[1] + m[10]*direction[2]}.unit()
}
if direction == (u3dVector{}) {
return fmt.Errorf("invalid U3D directional light direction")
}
r.lights[i] = u3dRenderLight{value: U3DLight{Attributes: 3, Kind: 1, Intensity: 1}, direction: direction}
}
r.lighting = lighting
return nil
}
// style 解析着色器与材质,未启用的材质项使用固定默认值
// 入参: name 着色器名
// 返回: 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}
if index, ok := r.shaders[name]; ok {
style.shader = r.model.Shaders[index]
}
shader := style.shader
if shader.Attributes > 7 || shader.Blend < 0x604 || shader.Blend > 0x607 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Attributes&2 != 0 && !finite(float64(shader.AlphaReference)) {
return style, fmt.Errorf("invalid U3D render shader")
}
if index, ok := r.materials[shader.Material]; ok {
material := r.model.Materials[index]
if material.Attributes > 63 {
return style, fmt.Errorf("invalid U3D render material")
}
for i, item := range []struct {
source [3]float32
target *[3]float64
}{{material.Ambient, &style.ambient}, {material.Diffuse, &style.diffuse}, {material.Specular, &style.specular}, {material.Emissive, &style.emissive}} {
if material.Attributes&(1<<i) != 0 {
for j, v := range item.source {
if !finite(float64(v)) {
return style, fmt.Errorf("invalid U3D material color")
}
item.target[j] = float64(v)
}
}
}
if material.Attributes&16 != 0 {
if !finite(float64(material.Reflectivity)) {
return style, fmt.Errorf("invalid U3D reflectivity")
}
style.exponent = min(1, max(0, float64(material.Reflectivity))) * 128
}
if material.Attributes&32 != 0 {
if !finite(float64(material.Opacity)) {
return style, fmt.Errorf("invalid U3D opacity")
}
style.opacity = min(1, max(0, float64(material.Opacity)))
}
}
return style, nil
}
// shade 按世界坐标计算逐顶点漫反射、镜面与环境光
// 入参: position 顶点, normal 法线, style 材质, diffuse 顶点漫反射, specular 顶点镜面颜色
// 返回: [4]float64 直通RGBA, error 参数或取消错误
func (r *u3dRender) shade(position, normal u3dVector, style u3dRenderStyle, diffuse, specular *[4]float32) ([4]float64, error) {
color := [4]float64{style.diffuse[0], style.diffuse[1], style.diffuse[2], style.opacity}
if style.shader.Attributes&4 != 0 {
if diffuse != nil {
for i, v := range diffuse {
if !finite(float64(v)) {
return color, fmt.Errorf("invalid U3D vertex color")
}
if i < 3 {
color[i] = float64(v)
style.diffuse[i] = float64(v)
style.ambient[i] = float64(v)
} else {
color[3] *= min(1, max(0, float64(v)))
}
}
}
if specular != nil {
for i, v := range specular[:3] {
if !finite(float64(v)) {
return color, fmt.Errorf("invalid U3D vertex specular color")
}
style.specular[i] = float64(v)
}
}
}
if style.shader.Attributes&1 == 0 {
return color, nil
}
if r.lighting != nil {
for c := range 3 {
style.diffuse[c] += r.lighting.DiffuseOffset[c]
}
}
copy(color[:3], style.emissive[:])
eye := (u3dVector{r.camera.ToWorld[12], r.camera.ToWorld[13], r.camera.ToWorld[14]}).sub(position).unit()
if !r.camera.Perspective {
eye = (u3dVector{-r.camera.ToWorld[8], -r.camera.ToWorld[9], -r.camera.ToWorld[10]}).unit()
if r.camera.ToWorld == ([16]float64{}) {
eye = u3dVector{0, 0, -1}
}
}
for i, light := range r.lights {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return color, err
}
}
value := light.value
if value.Kind == 0 {
for c := range 3 {
color[c] += style.ambient[c] * float64(value.Color[c])
}
continue
}
incoming := u3dVector{-light.direction[0], -light.direction[1], -light.direction[2]}
factor := float64(value.Intensity)
if value.Kind >= 2 {
incoming = light.position.sub(position)
distance := math.Hypot(math.Hypot(incoming[0], incoming[1]), incoming[2])
denominator := float64(value.Attenuation[0]) + float64(value.Attenuation[1])*distance + float64(value.Attenuation[2])*distance*distance
if !finite(denominator) || denominator == 0 {
return color, fmt.Errorf("invalid U3D point attenuation")
}
factor /= denominator
incoming = incoming.unit()
if value.Kind == 3 {
cosine := -incoming.dot(light.direction)
cutoff := math.Cos(float64(value.SpotAngle) * math.Pi / 360)
if cosine < cutoff {
continue
}
if value.Attributes&4 != 0 && cutoff < 1 {
factor *= min(1, max(0, (cosine-cutoff)/(1-cutoff)))
}
}
}
diffuseAmount := max(0, normal.dot(incoming))
specularAmount := 0.0
if diffuseAmount > 0 && style.exponent > 0 && value.Attributes&2 != 0 {
reflection := u3dVector{2*diffuseAmount*normal[0] - incoming[0], 2*diffuseAmount*normal[1] - incoming[1], 2*diffuseAmount*normal[2] - incoming[2]}
specularAmount = math.Pow(max(0, reflection.dot(eye)), style.exponent)
}
for c := range 3 {
lightColor := float64(value.Color[c])
if r.lighting != nil {
lightColor = r.lighting.Lights[i].Color[c]
}
color[c] += lightColor * factor * (style.diffuse[c]*diffuseAmount + style.specular[c]*specularAmount)
}
}
for _, v := range color {
if !finite(v) {
return color, fmt.Errorf("invalid U3D lighting result")
}
}
return color, nil
}
// u3dAlphaTest 按ECMA-363枚举比较透明度,不套用图形接口的枚举顺序
// 入参: function 比较函数, value 透明度, reference 参考值
// 返回: bool 是否通过
func u3dAlphaTest(function uint32, value, reference float64) bool {
switch function {
case 0x610:
return false
case 0x611:
return value < reference
case 0x612:
return value > reference
case 0x613:
return value == reference
case 0x614:
return value != reference
case 0x615:
return value <= reference
case 0x616:
return value >= reference
case 0x617:
return true
}
return false
}
+231
View File
@@ -0,0 +1,231 @@
// 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"
)
// frustumPlanes 返回保留正侧的六个相机视锥面
// 返回: [6][4]float64 近、远、左右及上下平面
func (r *u3dRender) frustumPlanes() [6][4]float64 {
w, h := r.viewport[2]/2, r.viewport[3]/2
planes := [6][4]float64{{0, 0, 1, -r.camera.Near}, {0, 0, -1, r.camera.Far}, {r.camera.ScaleX, 0, 0, w + r.camera.ShiftX}, {-r.camera.ScaleX, 0, 0, w - r.camera.ShiftX}, {0, r.camera.ScaleY, 0, h - r.camera.ShiftY}, {0, -r.camera.ScaleY, 0, h + r.camera.ShiftY}}
if r.camera.Perspective {
for i := 2; i < 6; i++ {
planes[i][2], planes[i][3] = planes[i][3], 0
}
}
return planes
}
// u3dPlaneDistance 计算未归一化的平面有符号距离
// 入参: plane 平面方程, point 点坐标
// 返回: float64 有符号距离
func u3dPlaneDistance(plane [4]float64, point u3dVector) float64 {
return plane[0]*point[0] + plane[1]*point[1] + plane[2]*point[2] + plane[3]
}
// u3dClipFraction 计算异侧有限距离的交点比例,避免绝对值求和溢出
// 入参: a 起点距离, b 终点距离
// 返回: float64 插值比例
func u3dClipFraction(a, b float64) float64 {
a, b = math.Abs(a), math.Abs(b)
scale := max(a, b)
if scale == 0 {
return 0
}
a, b = a/scale, b/scale
return a / (a + b)
}
// u3dInterpolate 在相机空间插值坐标与未预乘颜色
// 入参: a 起点, b 终点, t 比例
// 返回: u3dVertex 插值顶点
func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
var result u3dVertex
for i := range result.point {
result.point[i] = a.point[i]*(1-t) + b.point[i]*t
}
for i := range result.color {
result.color[i] = a.color[i]*(1-t) + b.color[i]*t
}
return result
}
// sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪
// 入参: triangle 原三角面, index 剖切面索引
// 返回: error 数值、资源或取消错误
func (r *u3dRender) sectionIntersection(triangle [3]u3dVertex, index int) error {
if err := r.ctx.Err(); err != nil {
return err
}
section := r.sections[index]
var distances [3]float64
for i, vertex := range triangle {
distances[i] = u3dPlaneDistance(section.plane, vertex.point)
if !finite(distances[i]) {
return fmt.Errorf("invalid U3D intersection distance")
}
}
if distances == ([3]float64{}) {
return nil
}
var points [3]u3dVertex
count := 0
add := func(vertex u3dVertex) {
for i := range count {
if points[i].point == vertex.point {
return
}
}
if count < len(points) {
points[count], count = vertex, count+1
}
}
for i, vertex := range triangle {
j := (i + 1) % 3
if distances[i] == 0 {
add(vertex)
}
if distances[i] < 0 && distances[j] > 0 || distances[i] > 0 && distances[j] < 0 {
add(u3dInterpolate(vertex, triangle[j], u3dClipFraction(distances[i], distances[j])))
}
}
if count < 2 {
return nil
}
a, b := points[0], points[1]
for i, other := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if i != index {
keep, err := u3dClipLine(&a, &b, other.plane)
if err != nil || !keep {
return err
}
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
keep, err := u3dClipLine(&a, &b, plane)
if err != nil || !keep {
return err
}
}
normal := u3dTriangleNormal([3]u3dVector{triangle[0].point, triangle[1].point, triangle[2].point})
return r.rasterSectionLine(a, b, *section.intersection, normal, triangle[0].point)
}
// u3dClipLine 将线段裁至平面正侧
// 入参: a 起点, b 终点, plane 平面方程
// 返回: bool 是否保留, error 数值错误
func u3dClipLine(a, b *u3dVertex, plane [4]float64) (bool, error) {
da, db := u3dPlaneDistance(plane, a.point), u3dPlaneDistance(plane, b.point)
if !finite(da) || !finite(db) {
return false, fmt.Errorf("invalid U3D line clip distance")
}
if da < 0 && db < 0 {
return false, nil
}
if (da < 0) != (db < 0) {
intersection := u3dInterpolate(*a, *b, u3dClipFraction(da, db))
if da < 0 {
*a = intersection
} else {
*b = intersection
}
}
return true, nil
}
// rasterSectionLine 绘制单像素不透明交线,透视深度使用倒数插值
// 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点
// 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error {
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
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}}
for _, plane := range planes {
if r.camera.Perspective {
plane[2], plane[3] = plane[3], 0
}
keep, err := u3dClipLine(&a, &b, plane)
if err != nil || !keep {
return err
}
}
var x, y, q [2]float64
for i, vertex := range [2]u3dVertex{a, b} {
q[i] = 1
if r.camera.Perspective {
if vertex.point[2] <= 0 {
return fmt.Errorf("invalid U3D line perspective depth")
}
q[i] = 1 / vertex.point[2]
}
x[i] = cx + vertex.point[0]*r.camera.ScaleX*q[i]
y[i] = cy - vertex.point[1]*r.camera.ScaleY*q[i]
if !finite(x[i]) || !finite(y[i]) || !finite(q[i]) {
return fmt.Errorf("invalid U3D projected line")
}
}
steps := math.Ceil(max(math.Abs(x[1]-x[0]), math.Abs(y[1]-y[0])))
if !finite(steps) || steps > float64(max(r.output.Rect.Dx(), r.output.Rect.Dy()))+2 {
return fmt.Errorf("invalid U3D line span")
}
count := max(1, int(steps))
color := [4]float64{rgb[0], rgb[1], rgb[2], 1}
r.order++
for i := 0; i <= count; i++ {
if i%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
t := float64(i) / float64(count)
px, py := math.Floor(x[0]*(1-t)+x[1]*t), math.Floor(y[0]*(1-t)+y[1]*t)
if px < 0 || py < 0 || px >= float64(r.output.Rect.Dx()) || py >= float64(r.output.Rect.Dy()) {
continue
}
depth := a.point[2]*(1-t) + b.point[2]*t
if r.camera.Perspective {
depth = 1 / (q[0]*(1-t) + q[1]*t)
}
ray := u3dVector{(px + .5 - cx) / r.camera.ScaleX, (cy - py - .5) / r.camera.ScaleY, 1}
denominator := normal[2]
numerator := normal.dot(origin) - normal[0]*ray[0] - normal[1]*ray[1]
if r.camera.Perspective {
denominator, numerator = normal.dot(ray), normal.dot(origin)
}
if sampled := numerator / denominator; finite(sampled) && sampled >= r.camera.Near && (r.camera.Far == 0 && !r.camera.ClipFar || sampled <= r.camera.Far) {
depth = sampled
}
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
pixel := int(py)*r.output.Rect.Dx() + int(px)
if depth <= r.frame[pixel].depth {
if err := r.fragment(pixel, depth, color, 0x606); err != nil {
return err
}
}
}
return nil
}
+295
View File
@@ -0,0 +1,295 @@
// 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"
)
// U3DMesh 保存完整基础网格的独立数组,索引对应原始作者网格
type U3DMesh struct {
Name string
ExcludeNormals bool
Positions, Normals [][3]float32
Diffuse, Specular, TextureCoordinates [][4]float32
Shadings []U3DShading
Faces []U3DFace
Quality [3]uint32
InverseQuantization [5]float32
NormalCrease, NormalUpdate, NormalTolerance float32
}
// U3DShading 描述各面使用的颜色及纹理坐标,纹理维数依层排列
type U3DShading struct {
Attributes uint32
TextureDimensions []uint32
OriginalID uint32
}
// U3DFace 保存着色描述索引和依原始绕序排列的三个角点
type U3DFace struct {
Shading uint32
Corners [3]U3DCorner
}
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
type U3DCorner struct {
Position, Normal, Diffuse, Specular uint32
Texture [8]uint32
}
// u3dMeshDeclaration 保存基础网格声明及续块接收状态
type u3dMeshDeclaration struct {
mesh U3DMesh
counts [7]uint32
decoded bool
}
// meshDeclaration 读取完整基础网格声明,不将渐进网格当作完整模型
// 入参: r 字段读取器, name 网格名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
if _, exists := d.meshes[name]; exists {
return fmt.Errorf("duplicate U3D mesh %q", name)
}
index, attributes := r.u32(), r.u32()
if index != 0 || attributes > 1 {
return fmt.Errorf("invalid U3D mesh declaration")
}
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name, ExcludeNormals: attributes == 1}}
for index := range decl.counts {
decl.counts[index] = r.u32()
}
count := decl.counts[6]
if uint64(count) > uint64(len(r.data)-r.pos)/12 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 40); err != nil {
return err
}
decl.mesh.Shadings = make([]U3DShading, int(count))
for index := range decl.mesh.Shadings {
shading := &decl.mesh.Shadings[index]
shading.Attributes = r.u32()
layers := r.u32()
if shading.Attributes > 3 {
return fmt.Errorf("invalid U3D shading attributes")
}
if layers > 8 {
return fmt.Errorf("unsupported U3D texture layer count")
}
if err := d.reserve(uint64(layers), 4); err != nil {
return err
}
shading.TextureDimensions = make([]uint32, int(layers))
for layer := range shading.TextureDimensions {
dimension := r.u32()
if dimension < 1 || dimension > 4 {
return fmt.Errorf("invalid U3D texture coordinate dimension")
}
shading.TextureDimensions[layer] = dimension
}
shading.OriginalID = r.u32()
}
minimum, maximum := r.u32(), r.u32()
for index := range decl.mesh.Quality {
decl.mesh.Quality[index] = r.u32()
}
for index := range decl.mesh.InverseQuantization {
decl.mesh.InverseQuantization[index] = r.f32()
}
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
bones := r.u32()
if r.err != nil {
return r.err
}
if minimum > maximum || maximum != decl.counts[1] {
return fmt.Errorf("invalid U3D mesh resolution")
}
if minimum != maximum || bones != 0 {
return fmt.Errorf("unsupported U3D progressive or skeletal mesh")
}
if minimum == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty mesh")
}
}
}
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
}
// baseMesh 读取基础顶点与压缩面索引,保留各角点的独立属性
// 入参: r 字段读取器, name 网格名
// 返回: error 结构、能力、预算或取消错误
func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
decl := d.meshes[name]
if decl == nil || decl.decoded || decl.counts[1] == 0 || r.u32() != 0 {
return fmt.Errorf("invalid U3D base mesh reference")
}
var counts [6]uint32
for index := range counts {
counts[index] = r.u32()
if counts[index] != decl.counts[index] {
return fmt.Errorf("U3D base mesh does not match full resolution declaration")
}
}
if r.err != nil {
return r.err
}
literalBytes := (uint64(counts[1])+uint64(counts[2]))*12 + (uint64(counts[3])+uint64(counts[4])+uint64(counts[5]))*16
if literalBytes > uint64(len(r.data)-r.pos) {
return io.ErrUnexpectedEOF
}
if err := d.reserve(literalBytes, 1); err != nil {
return err
}
if err := d.reserve(uint64(counts[0]), 148); err != nil {
return err
}
mesh := &decl.mesh
mesh.Positions, mesh.Normals = make([][3]float32, int(counts[1])), make([][3]float32, int(counts[2]))
mesh.Diffuse, mesh.Specular, mesh.TextureCoordinates = make([][4]float32, int(counts[3])), make([][4]float32, int(counts[4])), make([][4]float32, int(counts[5]))
for _, array := range [][][3]float32{mesh.Positions, mesh.Normals} {
for index := range array {
if index%1024 == 0 {
if r.err != nil {
return r.err
}
if err := d.ctx.Err(); err != nil {
return err
}
}
for component := range array[index] {
array[index][component] = r.f32()
}
}
}
for _, array := range [][][4]float32{mesh.Diffuse, mesh.Specular, mesh.TextureCoordinates} {
for index := range array {
if index%1024 == 0 {
if r.err != nil {
return r.err
}
if err := d.ctx.Err(); err != nil {
return err
}
}
for component := range array[index] {
array[index][component] = r.f32()
}
}
}
if r.err != nil {
return r.err
}
mesh.Faces = make([]U3DFace, int(counts[0]))
var histogram u3dHistogram
var compressed *u3dBits
if !d.plain && counts[0] != 0 {
if err := d.reserve(1, 1<<20); err != nil {
return err
}
compressed = newU3DBits(r.data[r.pos:])
}
readIndex := func(count uint32) (uint32, error) {
var value uint32
if compressed == nil {
value = r.u32()
} else {
value = compressed.index(count)
if compressed.err != nil {
return 0, compressed.err
}
}
if r.err != nil {
return 0, r.err
}
if value >= count {
return 0, fmt.Errorf("invalid U3D corner index")
}
return value, nil
}
for index := range mesh.Faces {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
face := &mesh.Faces[index]
if compressed == nil {
face.Shading = r.u32()
} else {
face.Shading = compressed.dynamic(&histogram)
if compressed.err != nil {
return compressed.err
}
}
if r.err != nil {
return r.err
}
if uint64(face.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D face shading index")
}
shading := &mesh.Shadings[face.Shading]
for cornerIndex := range face.Corners {
corner := &face.Corners[cornerIndex]
var err error
if corner.Position, err = readIndex(counts[1]); err != nil {
return err
}
if !mesh.ExcludeNormals {
if corner.Normal, err = readIndex(counts[2]); err != nil {
return err
}
}
if shading.Attributes&1 != 0 {
if corner.Diffuse, err = readIndex(counts[3]); err != nil {
return err
}
}
if shading.Attributes&2 != 0 {
if corner.Specular, err = readIndex(counts[4]); err != nil {
return err
}
}
for layer := range shading.TextureDimensions {
if corner.Texture[layer], err = readIndex(counts[5]); err != nil {
return err
}
}
}
}
if compressed != nil {
if compressed.pos > uint64(len(compressed.data))*8 {
return io.ErrUnexpectedEOF
}
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)
}
decl.decoded = true
return r.done()
}
+443
View File
@@ -0,0 +1,443 @@
// 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"
"image"
"math"
)
// U3DCamera 定义沿正Z方向观察的相机,ScaleX和ScaleY为投影到像素的倍率
// ToWorld按列存储,全零表示单位矩阵;Far为0且ClipFar为假时不设置远裁剪面
// ShiftX和ShiftY为投影原点相对视口中心的像素偏移,向右及向下为正
type U3DCamera struct {
ToWorld [16]float64
Perspective bool
ScaleX, ScaleY, Near, Far float64
ShiftX, ShiftY float64
ClipFar bool
}
// U3DRenderOptions 控制静态绘制,指定Camera时使用整个输出视口
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
type U3DRenderOptions struct {
Width, Height int
View string
Instance int
Camera *U3DCamera
Background *[3]float64
Lighting *U3DLighting
Nodes []U3DNodeState
Sections []U3DCrossSection
MaxMemoryBytes int64
MaxInstances int
}
// u3dPixel 保存不透明底色、最近深度及透明片元链
type u3dPixel struct {
color [3]float64
depth float64
order uint64
head int32
}
// u3dFragment 保存等待按像素深度混合的透明或非覆盖片元
type u3dFragment struct {
color [4]float64
depth float64
order uint64
blend uint32
next int32
}
// u3dRender 保存一次绘制的有限预算,不修改调用方场景
type u3dRender struct {
ctx context.Context
model *U3DModel
remaining uint64
frame []u3dPixel
output *image.RGBA
viewport [4]float64
camera U3DCamera
toCamera u3dMatrix
scene []u3dInstance
byNode [][]int
members []bool
meshes, shaders, materials, lightIndex map[string]int
lights []u3dRenderLight
lighting *U3DLighting
states map[string]U3DNodeState
sections []u3dSection
sectionScratch [2][]u3dVertex
chunks [][]u3dFragment
fragments int
scratch []int
order uint64
}
// RenderU3D 绘制静态无纹理三角面,返回不透明图像,不执行脚本或动画
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
r, err := newU3DRender(ctx, model, options)
if err != nil {
return nil, err
}
passes, err := r.selectView(options)
if err != nil {
return nil, err
}
return r.render(options, passes)
}
// newU3DRender 建立有预算限制的场景实例与资源索引
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*u3dRender, error) {
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 {
return nil, fmt.Errorf("invalid U3D render options")
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = u3dDefaultBytes
}
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit)}
if uint64(options.Width) > uint64(^uint(0)>>1)/uint64(options.Height) {
return nil, fmt.Errorf("invalid U3D render dimensions")
}
pixels := uint64(options.Width) * uint64(options.Height)
if err := r.reserve(pixels, 60); err != nil {
return nil, err
}
maxInstances := options.MaxInstances
if maxInstances == 0 {
maxInstances = 65536
}
if err := r.nodeStates(options.Nodes); err != nil {
return nil, err
}
if err := r.instances(maxInstances); err != nil {
return nil, err
}
if err := r.reserve(uint64(len(r.scene)), 1); err != nil {
return nil, err
}
r.members = make([]bool, len(r.scene))
if err := r.resources(); err != nil {
return nil, err
}
return r, nil
}
// render 在已选择的相机和轮次下生成图像
// 入参: options 输出与灯光背景, passes 绘制轮次
// 返回: *image.RGBA 图像, error 参数、绘制、预算或取消错误
func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*image.RGBA, error) {
if r.sections == nil {
if err := r.prepareSections(options.Sections); err != nil {
return nil, err
}
}
if err := r.overrideLighting(options.Lighting); err != nil {
return nil, err
}
background := [3]float64{1, 1, 1}
if options.Background != nil {
background = *options.Background
}
for i, v := range background {
if !finite(v) {
return nil, fmt.Errorf("invalid U3D background")
}
background[i] = min(1, max(0, v))
}
r.frame = make([]u3dPixel, options.Width*options.Height)
r.output = image.NewRGBA(image.Rect(0, 0, options.Width, options.Height))
for i := range r.frame {
if i%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
r.frame[i].color = background
}
for passIndex, pass := range passes {
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
return nil, err
}
}
for i, pixel := range r.frame {
if i%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
for component, v := range pixel.color {
r.output.Pix[i*4+component] = byte(math.Round(min(1, max(0, v)) * 255))
}
r.output.Pix[i*4+3] = 255
}
if err := r.ctx.Err(); err != nil {
return nil, err
}
return r.output, nil
}
// reserve 在分配绘制缓存前扣减预算并检查取消状态
// 入参: count 元素数量, size 元素预算
// 返回: error 预算或取消错误
func (r *u3dRender) reserve(count, size uint64) error {
if err := r.ctx.Err(); err != nil {
return err
}
if size == 0 || count > r.remaining/size || count > uint64(^uint(0)>>1)/size {
return fmt.Errorf("U3D render memory exceeds limit")
}
r.remaining -= count * size
return nil
}
// selectView 解析视图与相机覆盖,内部视口使用左上角坐标
// 入参: options 绘制选项
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) {
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
selected := -1
for i, node := range r.model.Nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if options.View != "" && node.Name == options.View || options.View == "" && options.Camera == nil && node.Kind == "View" {
selected = i
break
}
}
if options.View != "" && selected < 0 {
return nil, fmt.Errorf("missing U3D view %q", options.View)
}
passes := []U3DViewPass{{}}
if selected >= 0 {
node := r.model.Nodes[selected]
if node.Kind != "View" || node.View == nil {
return nil, fmt.Errorf("invalid U3D view node")
}
view := node.View
if len(view.Backdrops) != 0 || len(view.Overlays) != 0 {
return nil, fmt.Errorf("unsupported U3D textured view layers")
}
found := false
for i, resource := range r.model.Views {
if i%256 == 0 {
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.Instance >= len(r.byNode[selected]) {
return nil, fmt.Errorf("missing U3D camera instance")
}
r.camera.ToWorld = r.scene[r.byNode[selected][options.Instance]].world
if view.Attributes & ^uint32(7) != 0 || view.Attributes&6 >= 4 {
return nil, fmt.Errorf("unsupported U3D view projection")
}
for i, v := range view.Viewport {
if !finite(float64(v)) {
return nil, fmt.Errorf("invalid U3D viewport")
}
r.viewport[(i+2)%4] = float64(v)
}
if view.Attributes&1 != 0 {
r.viewport[0] *= float64(options.Width)
r.viewport[1] *= float64(options.Height)
r.viewport[2] *= float64(options.Width)
r.viewport[3] *= float64(options.Height)
}
if r.viewport[2] <= 0 || r.viewport[3] <= 0 {
return nil, fmt.Errorf("invalid U3D viewport size")
}
r.camera.Near, r.camera.Far = float64(view.Near), float64(view.Far)
if r.camera.Far <= r.camera.Near {
return nil, fmt.Errorf("invalid U3D view clipping range")
}
r.camera.Perspective = view.Attributes&6 == 0
if r.camera.Perspective {
if view.Projection <= 0 || view.Projection >= 180 {
return nil, fmt.Errorf("invalid U3D perspective field of view")
}
r.camera.ScaleY = r.viewport[3] / (2 * math.Tan(float64(view.Projection)*math.Pi/360))
} else {
if view.Projection <= 0 {
return nil, fmt.Errorf("invalid U3D orthographic height")
}
r.camera.ScaleY = r.viewport[3] / float64(view.Projection)
}
r.camera.ScaleX = r.camera.ScaleY
}
}
if options.Camera != nil {
r.camera = *options.Camera
} else if selected < 0 {
return nil, fmt.Errorf("missing U3D camera")
}
if len(passes) > 32 {
return nil, fmt.Errorf("unsupported U3D render pass count")
}
for _, v := range []float64{r.camera.ScaleX, r.camera.ScaleY, r.camera.Near, r.camera.Far, r.camera.ShiftX, r.camera.ShiftY} {
if !finite(v) {
return nil, fmt.Errorf("invalid U3D camera values")
}
}
if r.camera.ScaleX <= 0 || r.camera.ScaleY <= 0 || r.camera.Perspective && r.camera.Near <= 0 || (r.camera.ClipFar || r.camera.Far != 0) && r.camera.Far < r.camera.Near {
return nil, fmt.Errorf("invalid U3D camera projection")
}
matrix := u3dMatrix(r.camera.ToWorld)
if matrix == (u3dMatrix{}) {
matrix = u3dIdentity()
}
var ok bool
r.toCamera, ok = matrix.inverse()
if !ok {
return nil, fmt.Errorf("invalid U3D camera transform")
}
return passes, nil
}
// 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))
if err := r.reserve(count, 96); err != nil {
return err
}
r.meshes = make(map[string]int, len(r.model.Meshes))
r.shaders = make(map[string]int, len(r.model.Shaders))
r.materials = make(map[string]int, len(r.model.Materials))
r.lightIndex = make(map[string]int, len(r.model.Lights))
add := func(index map[string]int, name string, i int) error {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if _, ok := index[name]; ok {
return fmt.Errorf("duplicate U3D render resource %q", name)
}
index[name] = i
return nil
}
for i, v := range r.model.Meshes {
if err := add(r.meshes, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Shaders {
if err := add(r.shaders, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Materials {
if err := add(r.materials, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Lights {
if err := add(r.lightIndex, v.Name, i); err != nil {
return err
}
}
return nil
}
// renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度
// 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮
// 返回: error 场景、预算或取消错误
func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error {
if pass.Attributes > 1 || pass.FogMode > 2 {
return fmt.Errorf("invalid U3D render pass")
}
if pass.Attributes&1 != 0 {
for _, v := range append(pass.FogColor[:], pass.FogNear, pass.FogFar) {
if !finite(float64(v)) {
return fmt.Errorf("invalid U3D fog value")
}
}
if pass.FogMode == 0 && pass.FogFar <= pass.FogNear || pass.FogMode != 0 && pass.FogFar <= 0 {
return fmt.Errorf("invalid U3D fog range")
}
}
for i, instance := range r.scene {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
r.members[i] = pass.Root == "" || r.model.Nodes[instance.node].Name == pass.Root || instance.parent >= 0 && r.members[instance.parent]
}
for i := range r.frame {
if i%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
r.frame[i].depth = math.Inf(1)
r.frame[i].order = 0
r.frame[i].head = -1
}
r.fragments = 0
if err := r.collectLights(); err != nil {
return err
}
for i, instance := range r.scene {
if err := r.ctx.Err(); err != nil {
return err
}
node := &r.model.Nodes[instance.node]
if !r.members[i] || node.Kind != "Model" || instance.modelVisibility(node) == 0 || instance.opacity == 0 {
continue
}
if node.Visibility > 3 {
return fmt.Errorf("invalid U3D model visibility")
}
mesh, ok := r.meshes[node.Resource]
if !ok {
continue
}
if err := r.drawMesh(instance, node, &r.model.Meshes[mesh], pass, passIndex); err != nil {
return err
}
}
if last {
if err := r.drawSectionPlanes(); err != nil {
return err
}
}
return r.resolveFragments()
}
+303
View File
@@ -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"
"math"
)
// u3dVector 保存计算过程中的三维双精度坐标
type u3dVector [3]float64
// u3dMatrix 保存按列排列的仿射矩阵
type u3dMatrix [16]float64
// u3dInstance 保存一个父路径产生的世界实例,parent为负值时连接默认根
type u3dInstance struct {
node, parent int
world u3dMatrix
opacity float64
visibility int8
}
// u3dIdentity 创建单位矩阵
// 返回: u3dMatrix 单位矩阵
func u3dIdentity() u3dMatrix { return u3dMatrix{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1} }
// multiply 连接子节点到父节点的变换
// 入参: other 子变换
// 返回: u3dMatrix 组合变换
func (m u3dMatrix) multiply(other u3dMatrix) u3dMatrix {
var result u3dMatrix
for column := range 4 {
for row := range 4 {
for k := range 4 {
result[column*4+row] += m[k*4+row] * other[column*4+k]
}
}
}
return result
}
// valid 判断矩阵是否为有限仿射变换
// 返回: bool 是否可用于静态绘制
func (m u3dMatrix) valid() bool {
for _, v := range m {
if !finite(v) {
return false
}
}
return m[3] == 0 && m[7] == 0 && m[11] == 0 && m[15] == 1
}
// point 将点从局部坐标映射到目标坐标
// 入参: p 局部点
// 返回: u3dVector 目标点
func (m u3dMatrix) point(p u3dVector) u3dVector {
return u3dVector{m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12], m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13], m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]}
}
// inverse 通过带主元消元计算仿射逆矩阵
// 返回: u3dMatrix 逆矩阵, bool 是否存在有限逆矩阵
func (m u3dMatrix) inverse() (u3dMatrix, bool) {
if !m.valid() {
return u3dMatrix{}, false
}
var rows [3][6]float64
for i := range 3 {
for j := range 3 {
rows[i][j] = m[j*4+i]
}
rows[i][i+3] = 1
}
for column := range 3 {
pivot := column
for row := column + 1; row < 3; row++ {
if math.Abs(rows[row][column]) > math.Abs(rows[pivot][column]) {
pivot = row
}
}
if rows[pivot][column] == 0 {
return u3dMatrix{}, false
}
rows[pivot], rows[column] = rows[column], rows[pivot]
divisor := rows[column][column]
for j := range 6 {
rows[column][j] /= divisor
}
for row := range 3 {
if row != column {
factor := rows[row][column]
for j := range 6 {
rows[row][j] -= factor * rows[column][j]
}
}
}
}
result := u3dIdentity()
for row := range 3 {
for col := range 3 {
result[col*4+row] = rows[row][col+3]
}
result[12+row] = -(result[row]*m[12] + result[4+row]*m[13] + result[8+row]*m[14])
}
return result, result.valid()
}
// normal 以逆矩阵的转置变换并归一化法线
// 入参: n 局部法线,接收者为局部变换的逆矩阵
// 返回: u3dVector 目标法线
func (m u3dMatrix) normal(n u3dVector) u3dVector {
return (u3dVector{m[0]*n[0] + m[1]*n[1] + m[2]*n[2], m[4]*n[0] + m[5]*n[1] + m[6]*n[2], m[8]*n[0] + m[9]*n[1] + m[10]*n[2]}).unit()
}
// unit 归一化向量,零向量保持为零
// 返回: u3dVector 单位向量
func (v u3dVector) unit() u3dVector {
scale := max(math.Abs(v[0]), math.Abs(v[1]), math.Abs(v[2]))
if scale == 0 || !finite(scale) {
return u3dVector{}
}
for i := range v {
v[i] /= scale
}
length := math.Sqrt(v.dot(v))
return u3dVector{v[0] / length, v[1] / length, v[2] / length}
}
// sub 求向量差
// 入参: other 被减向量
// 返回: u3dVector 差向量
func (v u3dVector) sub(other u3dVector) u3dVector {
return u3dVector{v[0] - other[0], v[1] - other[1], v[2] - other[2]}
}
// dot 求向量内积
// 入参: other 另一向量
// 返回: float64 内积
func (v u3dVector) dot(other u3dVector) float64 { return v[0]*other[0] + v[1]*other[1] + v[2]*other[2] }
// cross 求向量叉积
// 入参: other 另一向量
// 返回: u3dVector 叉积
func (v u3dVector) cross(other u3dVector) u3dVector {
return u3dVector{v[1]*other[2] - v[2]*other[1], v[2]*other[0] - v[0]*other[2], v[0]*other[1] - v[1]*other[0]}
}
// u3dTriangleNormal 选取夹角更稳定的顶点计算法线,避免长瘦三角面的相消
// 入参: points 按绕序排列的顶点
// 返回: u3dVector 单位法线,退化三角面返回零向量
func u3dTriangleNormal(points [3]u3dVector) u3dVector {
var normal u3dVector
score := 0.0
for i := range 3 {
a := points[(i+1)%3].sub(points[i]).unit()
b := points[(i+2)%3].sub(points[i]).unit()
candidate := a.cross(b)
if length := max(math.Abs(candidate[0]), math.Abs(candidate[1]), math.Abs(candidate[2])); length > score {
normal, score = candidate, length
}
}
return normal.unit()
}
// instances 按拓扑顺序构建全部父路径实例,限制共享层级的指数展开
// 入参: limit 最大实例数
// 返回: error 结构、预算、实例上限或取消错误
func (r *u3dRender) instances(limit int) error {
nodes := r.model.Nodes
if err := r.reserve(uint64(len(nodes)), 160); err != nil {
return err
}
index := make(map[string]int, len(nodes))
for i, node := range nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if _, exists := index[node.Name]; exists || node.Name == "" {
return fmt.Errorf("invalid U3D render node name")
}
switch node.Kind {
case "Group", "Model", "Light", "View":
default:
return fmt.Errorf("unsupported U3D render node kind")
}
index[node.Name] = i
}
counts := make([]int, len(nodes))
children := make([][]int, len(nodes))
r.byNode = make([][]int, len(nodes))
for i, node := range nodes {
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil {
return err
}
for k, parent := range node.Parents {
if k%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if j, ok := index[parent.Name]; ok {
counts[i]++
children[j] = append(children[j], i)
}
}
}
queue := make([]int, 0, len(nodes))
for i, n := range counts {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if n == 0 {
queue = append(queue, i)
}
}
for cursor := 0; cursor < len(queue); cursor++ {
if err := r.ctx.Err(); err != nil {
return err
}
i := queue[cursor]
state := r.states[nodes[i].Name]
for parentIndex, parent := range nodes[i].Parents {
if parentIndex%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
var local u3dMatrix
for k, v := range parent.Transform {
local[k] = float64(v)
}
if state.Matrix != nil {
local = *state.Matrix
}
if !local.valid() {
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 {
if len(r.scene) >= limit {
return fmt.Errorf("U3D instance count exceeds limit")
}
if err := r.reserve(1, 384); err != nil {
return err
}
world := local
opacity, visibility := -1.0, int8(-1)
if p >= 0 {
world = r.scene[p].world.multiply(local)
opacity, visibility = r.scene[p].opacity, r.scene[p].visibility
}
if state.Opacity != nil {
opacity = *state.Opacity
}
if state.Visible != nil {
visibility = 0
if *state.Visible {
visibility = 1
}
}
if !world.valid() {
return fmt.Errorf("invalid U3D world transform")
}
r.byNode[i] = append(r.byNode[i], len(r.scene))
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 fmt.Errorf("cyclic U3D node parents")
}
return nil
}
+280
View File
@@ -0,0 +1,280 @@
// 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"
)
// U3DCrossSection 定义世界坐标中的剖切面,AxisU与AxisV须为非零正交方向
// 两轴叉积指向被裁去的一侧;平面方形边长取静态场景包围盒对角线
// Opacity为0时不显示平面,Intersection为空时不显示交线,颜色分量范围为0到1
type U3DCrossSection struct {
Center, AxisU, AxisV [3]float64
Opacity float64
Color [3]float64
Intersection *[3]float64
}
// u3dSection 保存相机空间裁剪方程、平面四角与交线颜色
type u3dSection struct {
plane [4]float64
corners [4]u3dVertex
intersection *[3]float64
opacity float64
}
// prepareSections 准备剖切缓存,不复制网格或修改调用方参数
// 入参: sections 世界坐标剖切面
// 返回: error 参数、场景、预算或取消错误
func (r *u3dRender) prepareSections(sections []U3DCrossSection) error {
if len(sections) == 0 {
return nil
}
if err := r.reserve(uint64(len(sections)), 352); err != nil {
return err
}
if err := r.reserve(uint64(len(sections))+3, 112); err != nil {
return err
}
r.sections = make([]u3dSection, len(sections))
for i := range r.sectionScratch {
r.sectionScratch[i] = make([]u3dVertex, len(sections)+3)
}
visible := false
for i, section := range sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if !finite(section.Opacity) || section.Opacity < 0 || section.Opacity > 1 {
return fmt.Errorf("invalid U3D section opacity")
}
visible = visible || section.Opacity > 0
}
diagonal := 0.0
if visible {
var err error
diagonal, err = r.sceneDiagonal()
if err != nil {
return err
}
}
world := u3dMatrix(r.camera.ToWorld)
if world == (u3dMatrix{}) {
world = u3dIdentity()
}
for i, section := range sections {
if err := r.ctx.Err(); err != nil {
return err
}
for _, vector := range [][3]float64{section.Center, section.AxisU, section.AxisV} {
for _, value := range vector {
if !finite(value) {
return fmt.Errorf("invalid U3D section coordinates")
}
}
}
for _, color := range []*[3]float64{&section.Color, section.Intersection} {
if color != nil {
for _, v := range color {
if !finite(v) || v < 0 || v > 1 {
return fmt.Errorf("invalid U3D section color")
}
}
}
}
u, v := u3dVector(section.AxisU).unit(), u3dVector(section.AxisV).unit()
if u == (u3dVector{}) || v == (u3dVector{}) || math.Abs(u.dot(v)) > 1e-12 {
return fmt.Errorf("invalid U3D section axes")
}
normal := u.cross(v).unit()
prepared := &r.sections[i]
for j := range 3 {
prepared.plane[j] = -(normal[0]*world[j*4] + normal[1]*world[j*4+1] + normal[2]*world[j*4+2])
}
prepared.plane[3] = normal.dot(u3dVector(section.Center).sub(u3dVector{world[12], world[13], world[14]}))
for _, coefficient := range prepared.plane {
if !finite(coefficient) {
return fmt.Errorf("invalid U3D section plane")
}
}
prepared.intersection, prepared.opacity = section.Intersection, section.Opacity
for j, signs := range [4][2]float64{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}} {
point := u3dVector(section.Center)
for k := range point {
point[k] += diagonal / 2 * (signs[0]*u[k] + signs[1]*v[k])
}
prepared.corners[j] = u3dVertex{point: r.toCamera.point(point), color: [4]float64{section.Color[0], section.Color[1], section.Color[2], section.Opacity}}
for _, coordinate := range prepared.corners[j].point {
if !finite(coordinate) {
return fmt.Errorf("invalid U3D section corner")
}
}
}
}
return nil
}
// sceneDiagonal 计算静态场景世界包围盒的对角线,包含隐藏节点的几何
// 返回: float64 对角线长度, error 几何、数值或取消错误
func (r *u3dRender) sceneDiagonal() (float64, error) {
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
for _, instance := range r.scene {
if err := r.ctx.Err(); err != nil {
return 0, err
}
node := r.model.Nodes[instance.node]
index, exists := r.meshes[node.Resource]
if node.Kind != "Model" || !exists {
continue
}
mesh := &r.model.Meshes[index]
for i, face := range mesh.Faces {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
for _, corner := range face.Corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, fmt.Errorf("invalid U3D position index")
}
p := mesh.Positions[corner.Position]
world := instance.world.point(u3dVector{float64(p[0]), float64(p[1]), float64(p[2])})
for j, v := range world {
if !finite(v) {
return 0, fmt.Errorf("invalid U3D scene bounds")
}
minimum[j], maximum[j] = min(minimum[j], v), max(maximum[j], v)
}
}
}
}
if math.IsInf(minimum[0], 1) {
return 0, nil
}
delta := maximum.sub(minimum)
diagonal := math.Hypot(math.Hypot(delta[0], delta[1]), delta[2])
if !finite(diagonal) {
return 0, fmt.Errorf("invalid U3D scene diagonal")
}
return diagonal, nil
}
// sectionTriangle 用复用的凸多边形缓存应用全部剖切面,再进行视锥裁剪
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) sectionTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if len(r.sections) == 0 {
return r.clipTriangle(triangle, shader, pass)
}
copy(r.sectionScratch[0], triangle[:])
size, current := 3, 0
for _, section := range r.sections {
if err := r.ctx.Err(); err != nil {
return err
}
next, count := 1-current, 0
previous := r.sectionScratch[current][size-1]
previousDistance := u3dPlaneDistance(section.plane, previous.point)
for i, vertex := range r.sectionScratch[current][:size] {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
distance := u3dPlaneDistance(section.plane, vertex.point)
if !finite(distance) || !finite(previousDistance) {
return fmt.Errorf("invalid U3D section distance")
}
if (distance >= 0) != (previousDistance >= 0) {
var err error
count, err = u3dAppendSectionVertex(r.sectionScratch[next], count, u3dInterpolate(previous, vertex, u3dClipFraction(previousDistance, distance)))
if err != nil {
return err
}
}
if distance >= 0 {
var err error
count, err = u3dAppendSectionVertex(r.sectionScratch[next], count, vertex)
if err != nil {
return err
}
}
previous, previousDistance = vertex, distance
}
if count > 1 && r.sectionScratch[next][0].point == r.sectionScratch[next][count-1].point {
count--
}
size, current = count, next
if size < 3 {
break
}
}
if size < 2 {
return nil
}
for i := 1; i+1 < size; i++ {
if err := r.clipTriangle([3]u3dVertex{r.sectionScratch[current][0], r.sectionScratch[current][i], r.sectionScratch[current][i+1]}, shader, pass); err != nil {
return err
}
}
for i, section := range r.sections {
if section.intersection != nil {
if err := r.sectionIntersection(triangle, i); err != nil {
return err
}
}
}
return nil
}
// u3dAppendSectionVertex 合并连续重复交点,维持凸多边形缓存上限
// 入参: buffer 固定缓存, count 已有顶点数, vertex 新顶点
// 返回: int 顶点数, error 缓存越界错误
func u3dAppendSectionVertex(buffer []u3dVertex, count int, vertex u3dVertex) (int, error) {
if count > 0 && buffer[count-1].point == vertex.point {
return count, nil
}
if count >= len(buffer) {
return count, fmt.Errorf("invalid U3D section polygon")
}
buffer[count] = vertex
return count + 1, nil
}
// drawSectionPlanes 在最后一轮绘制平面方形,不把平面本身当作待剖切几何
// 返回: error 数值、资源或取消错误
func (r *u3dRender) drawSectionPlanes() error {
for _, section := range r.sections {
if err := r.ctx.Err(); err != nil {
return err
}
if section.opacity == 0 {
continue
}
for i := 1; i < 3; i++ {
r.order++
if err := r.clipTriangle([3]u3dVertex{section.corners[0], section.corners[i], section.corners[i+1]}, U3DShader{Blend: 0x606}, U3DViewPass{}); err != nil {
return err
}
}
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
// 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"
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
// 未匹配的名称不影响场景,矩阵为空时保留每条父路径的原始变换
type U3DNodeState struct {
Name string
Opacity *float64
Visible *bool
Matrix *[16]float64
}
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
// 入参: states 节点覆盖
// 返回: error 数值、预算或取消错误
func (r *u3dRender) nodeStates(states []U3DNodeState) error {
if len(states) == 0 {
return nil
}
if err := r.reserve(uint64(len(states)), 128); err != nil {
return err
}
r.states = make(map[string]U3DNodeState, len(states))
for i, state := range states {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
r.states[state.Name] = state
}
for _, state := range r.states {
if err := r.ctx.Err(); err != nil {
return err
}
if state.Opacity != nil && (!finite(*state.Opacity) || *state.Opacity < 0 || *state.Opacity > 1) {
return fmt.Errorf("invalid U3D node opacity")
}
if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() {
return fmt.Errorf("invalid U3D node matrix")
}
}
return nil
}
// modelVisibility 在节点覆盖生效时恢复隐藏模型,保留原有正反面选择
// 入参: node 模型节点
// 返回: uint32 可见面标志
func (instance u3dInstance) modelVisibility(node *U3DNode) uint32 {
if instance.visibility == 0 {
return 0
}
if instance.visibility == 1 && node.Visibility == 0 {
return 3
}
return node.Visibility
}
+464
View File
@@ -0,0 +1,464 @@
// 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"
)
// u3dVertex 保存裁剪前后的相机坐标及未预乘颜色
type u3dVertex struct {
point u3dVector
color [4]float64
}
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误
func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
count := uint64(len(mesh.Positions)) + uint64(len(mesh.Normals))
if err := r.reserve(count, 24); err != nil {
return err
}
defer func() { r.remaining += count * 24 }()
positions := make([]u3dVector, len(mesh.Positions))
normals := make([]u3dVector, len(mesh.Normals))
inverse, invertible := instance.world.inverse()
for i, p := range mesh.Positions {
if i%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
positions[i] = instance.world.point(u3dVector{float64(p[0]), float64(p[1]), float64(p[2])})
for _, v := range positions[i] {
if !finite(v) {
return fmt.Errorf("invalid U3D transformed position")
}
}
}
for i, n := range mesh.Normals {
if i%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
for _, v := range n {
if !finite(float64(v)) {
return fmt.Errorf("invalid U3D normal")
}
}
if invertible {
normals[i] = inverse.normal(u3dVector{float64(n[0]), float64(n[1]), float64(n[2])})
}
}
for index, face := range mesh.Faces {
if index%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(face.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D render shading index")
}
shading := mesh.Shadings[face.Shading]
if shading.Attributes > 3 || len(shading.TextureDimensions) > 8 {
return fmt.Errorf("invalid U3D render shading")
}
var world [3]u3dVector
var vertices [3]u3dVertex
for i, corner := range face.Corners {
if uint64(corner.Position) >= uint64(len(positions)) || !mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular)) {
return fmt.Errorf("invalid U3D render corner index")
}
world[i] = positions[corner.Position]
vertices[i].point = r.toCamera.point(world[i])
for _, v := range vertices[i].point {
if !finite(v) {
return fmt.Errorf("invalid U3D camera position")
}
}
}
var faceNormal u3dVector
if mesh.ExcludeNormals || !invertible {
faceNormal = u3dTriangleNormal(world)
}
p0, p1, p2 := vertices[0].point, vertices[1].point, vertices[2].point
facing := (p1[0]-p0[0])*(p2[1]-p0[1]) - (p1[1]-p0[1])*(p2[0]-p0[0])
if r.camera.Perspective {
facing = p0.unit().dot(p1.unit().cross(p2.unit()))
}
if !finite(facing) {
return fmt.Errorf("invalid U3D face orientation")
}
front := facing < 0
visibility := instance.modelVisibility(node)
if facing == 0 || front && visibility&1 == 0 || !front && visibility&2 == 0 {
continue
}
shaderNames := []string{""}
if uint64(face.Shading) < uint64(len(node.Shaders)) {
shaderNames = node.Shaders[face.Shading]
}
for shaderIndex, name := range shaderNames {
if shaderIndex%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
style, err := r.style(name)
if err != nil {
return err
}
if style.shader.RenderPass&(1<<passIndex) == 0 {
continue
}
if instance.opacity >= 0 {
style.shader.Blend = 0x606
}
for i, corner := range face.Corners {
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
if shading.Attributes&1 != 0 {
diffuse = &mesh.Diffuse[corner.Diffuse]
}
if shading.Attributes&2 != 0 {
specular = &mesh.Specular[corner.Specular]
}
vertices[i].color, err = r.shade(world[i], normal, style, diffuse, specular)
if err != nil {
return err
}
if instance.opacity >= 0 {
vertices[i].color[3] = instance.opacity
}
}
r.order++
if err := r.sectionTriangle(vertices, style.shader, pass); err != nil {
return err
}
}
}
return nil
}
// clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
var storage [2][12]u3dVertex
copy(storage[0][:], triangle[:])
size, current := 3, 0
planes := r.frustumPlanes()
for planeIndex, plane := range planes {
if planeIndex == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
output, next := 0, 1-current
previous := storage[current][size-1]
distance := func(v u3dVertex) float64 {
return plane[0]*v.point[0] + plane[1]*v.point[1] + plane[2]*v.point[2] + plane[3]
}
previousDistance := distance(previous)
for _, vertex := range storage[current][:size] {
d := distance(vertex)
if !finite(d) || !finite(previousDistance) {
return fmt.Errorf("invalid U3D clip distance")
}
if (d >= 0) != (previousDistance >= 0) {
t := u3dClipFraction(previousDistance, d)
var intersection u3dVertex
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]) {
return fmt.Errorf("invalid U3D clipped polygon")
}
storage[next][output] = intersection
output++
}
if d >= 0 {
if output == len(storage[next]) {
return fmt.Errorf("invalid U3D clipped polygon")
}
storage[next][output] = vertex
output++
}
previous, previousDistance = vertex, d
}
if output < 3 {
return nil
}
current, size = next, output
}
for i := 1; i+1 < size; i++ {
if err := r.rasterTriangle([3]u3dVertex{storage[current][0], storage[current][i], storage[current][i+1]}, shader, pass); err != nil {
return err
}
}
return nil
}
// rasterTriangle 使用像素中心和左上边规则光栅化,透视插值保持颜色与深度一致
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、预算或取消错误
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
var x, y, reciprocal [3]float64
for i, v := range triangle {
reciprocal[i] = 1
if r.camera.Perspective {
reciprocal[i] = 1 / v.point[2]
}
x[i] = r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX + v.point[0]*r.camera.ScaleX*reciprocal[i]
y[i] = r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - v.point[1]*r.camera.ScaleY*reciprocal[i]
if !finite(x[i]) || !finite(y[i]) || !finite(reciprocal[i]) {
return fmt.Errorf("invalid U3D projected point")
}
}
area := (x[1]-x[0])*(y[2]-y[0]) - (y[1]-y[0])*(x[2]-x[0])
if !finite(area) {
return fmt.Errorf("invalid U3D projected area")
}
if area == 0 {
return nil
}
if area < 0 {
triangle[1], triangle[2] = triangle[2], triangle[1]
x[1], x[2] = x[2], x[1]
y[1], y[2] = y[2], y[1]
reciprocal[1], reciprocal[2] = reciprocal[2], reciprocal[1]
area = -area
}
width, height := r.output.Rect.Dx(), r.output.Rect.Dy()
left := max(0, math.Ceil(min(x[0], x[1], x[2])-.5))
right := min(float64(width-1), math.Floor(max(x[0], x[1], x[2])-.5))
top := max(0, math.Ceil(min(y[0], y[1], y[2])-.5))
bottom := min(float64(height-1), math.Floor(max(y[0], y[1], y[2])-.5))
if left > right || top > bottom {
return nil
}
var inclusive [3]bool
for i := range 3 {
j := (i + 1) % 3
inclusive[i] = y[j] < y[i] || y[j] == y[i] && x[j] > x[i]
}
for row := int(top); row <= int(bottom); row++ {
if err := r.ctx.Err(); err != nil {
return err
}
for column := int(left); column <= int(right); column++ {
if column%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
px, py := float64(column)+.5, float64(row)+.5
var edge [3]float64
inside := true
for i := range 3 {
j := (i + 1) % 3
edge[i] = (x[j]-x[i])*(py-y[i]) - (y[j]-y[i])*(px-x[i])
if edge[i] < 0 || edge[i] == 0 && !inclusive[i] {
inside = false
break
}
}
if !inside {
continue
}
w1, w2 := edge[2]/area, edge[0]/area
depth := triangle[0].point[2] + w1*(triangle[1].point[2]-triangle[0].point[2]) + w2*(triangle[2].point[2]-triangle[0].point[2])
if r.camera.Perspective {
q := edge[1]/area*reciprocal[0] + w1*reciprocal[1] + w2*reciprocal[2]
if !finite(q) || q <= 0 {
return fmt.Errorf("invalid U3D perspective depth")
}
w1 = w1 * reciprocal[1] / q
w2 = w2 * reciprocal[2] / q
depth = 1 / q
}
pixel := row*width + column
if depth > r.frame[pixel].depth {
continue
}
var color [4]float64
for c := range color {
color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c])
if !finite(color[c]) {
return fmt.Errorf("invalid U3D interpolated color")
}
}
color[3] = min(1, max(0, color[3]))
if shader.Attributes&2 != 0 && !u3dAlphaTest(shader.AlphaFunction, color[3], float64(shader.AlphaReference)) {
continue
}
if pass.Attributes&1 != 0 {
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 {
position[0] *= depth
position[1] *= depth
}
distance := math.Hypot(math.Hypot(position[0], position[1]), position[2])
factor := (float64(pass.FogFar) - distance) / (float64(pass.FogFar) - float64(pass.FogNear))
if pass.FogMode != 0 {
amount := distance * math.Log(100) / float64(pass.FogFar)
if pass.FogMode == 2 {
amount *= amount
}
factor = math.Exp(-amount)
}
factor = min(1, max(0, factor))
for c := range 3 {
color[c] = color[c]*factor + float64(pass.FogColor[c])*(1-factor)
}
}
for c := range 3 {
color[c] = min(1, max(0, color[c]))
}
if err := r.fragment(pixel, depth, color, shader.Blend); err != nil {
return err
}
}
}
return nil
}
// fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度
// 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式
// 返回: error 预算或取消错误
func (r *u3dRender) fragment(pixel int, depth float64, color [4]float64, blend uint32) error {
if !finite(depth) {
return fmt.Errorf("invalid U3D fragment depth")
}
p := &r.frame[pixel]
if blend == 0x606 && color[3] == 1 || blend == 0x607 && color[3] == 0 {
p.depth = depth
p.order = r.order
copy(p.color[:], color[:3])
return nil
}
if blend == 0x606 && color[3] == 0 || blend == 0x607 && color[3] == 1 {
return nil
}
if r.fragments >= math.MaxInt32 {
return fmt.Errorf("U3D fragment count exceeds limit")
}
chunk := r.fragments / 512
if chunk == len(r.chunks) {
if err := r.reserve(1, 512*56+48); err != nil {
return err
}
r.chunks = append(r.chunks, make([]u3dFragment, 512))
}
r.chunks[chunk][r.fragments%512] = u3dFragment{color: color, depth: depth, order: r.order, blend: blend, next: p.head}
p.head = int32(r.fragments)
r.fragments++
return nil
}
// resolveFragments 按每个像素的远近排序,正确处理相交透明面和着色器叠加
// 返回: error 预算或取消错误
func (r *u3dRender) resolveFragments() error {
if r.fragments == 0 {
return r.ctx.Err()
}
get := func(i int) *u3dFragment { return &r.chunks[i/512][i%512] }
for i := range r.frame {
if i%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
pixel := &r.frame[i]
r.scratch = r.scratch[:0]
for index := pixel.head; index >= 0; {
if len(r.scratch)%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
current := int(index)
fragment := get(current)
index = fragment.next
if fragment.depth > pixel.depth || fragment.depth == pixel.depth && fragment.order < pixel.order {
continue
}
if len(r.scratch) == cap(r.scratch) {
capacity := max(8, cap(r.scratch)*2)
if err := r.reserve(uint64(capacity), 8); err != nil {
return err
}
values := make([]int, len(r.scratch), capacity)
copy(values, r.scratch)
r.remaining += uint64(cap(r.scratch)) * 8
r.scratch = values
}
r.scratch = append(r.scratch, current)
}
slices.SortFunc(r.scratch, func(a, b int) int {
x, y := get(a), get(b)
if x.depth > y.depth {
return -1
}
if x.depth < y.depth {
return 1
}
if x.order < y.order {
return -1
}
if x.order > y.order {
return 1
}
return 0
})
for layer, index := range r.scratch {
if layer%1024 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
fragment := get(index)
alpha := fragment.color[3]
if fragment.blend == 0x607 {
alpha = 1 - alpha
}
for c := range 3 {
switch fragment.blend {
case 0x604:
pixel.color[c] += fragment.color[c]
case 0x605:
pixel.color[c] *= fragment.color[c]
default:
pixel.color[c] = fragment.color[c]*alpha + pixel.color[c]*(1-alpha)
}
pixel.color[c] = min(1, max(0, pixel.color[c]))
}
}
}
return r.ctx.Err()
}
+131
View File
@@ -0,0 +1,131 @@
// 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"
)
// U3DView 保存视图节点的投影、裁剪、视口及叠加层参数
type U3DView struct {
Attributes uint32
Near, Far float32
Projection float32
ProjectionVector [3]float32
Viewport [4]float32
Backdrops, Overlays []U3DViewLayer
}
// U3DViewLayer 保存背景或覆盖纹理,注册点使用纹理像素,旋转使用弧度
type U3DViewLayer struct {
Texture string
Blend, Rotation float32
Location [2]float32
Registration [2]int32
Scale [2]float32
}
// U3DViewResource 保存视图的绘制轮次,各轮独立指定根节点及雾效
type U3DViewResource struct {
Name string
Passes []U3DViewPass
}
// U3DViewPass 保存单轮根节点与雾效,Attributes最低位启用雾效
type U3DViewPass struct {
Root string
Attributes, FogMode uint32
FogColor [4]float32
FogNear, FogFar float32
}
// view 按ECMA-363第9.5.4节读取视图节点的可变投影字段
// 入参: r 字段读取器
// 返回: *U3DView 视图, error 结构或预算错误
func (d *u3dDecoder) view(r *u3dValues) (*U3DView, error) {
view := &U3DView{Attributes: r.u32(), Near: r.f32(), Far: r.f32()}
if view.Attributes & ^uint32(7) != 0 {
return nil, fmt.Errorf("invalid U3D view attributes")
}
if view.Attributes&6 < 4 {
view.Projection = r.f32()
} else {
for i := range view.ProjectionVector {
view.ProjectionVector[i] = r.f32()
}
}
for i := range view.Viewport {
view.Viewport[i] = r.f32()
}
for _, layers := range []*[]U3DViewLayer{&view.Backdrops, &view.Overlays} {
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/34 {
return nil, io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 64); err != nil {
return nil, err
}
*layers = make([]U3DViewLayer, int(count))
for i := range *layers {
if err := d.ctx.Err(); err != nil {
return nil, err
}
layer := &(*layers)[i]
layer.Texture = r.text()
layer.Blend, layer.Rotation = r.f32(), r.f32()
for j := range layer.Location {
layer.Location[j] = r.f32()
}
for j := range layer.Registration {
layer.Registration[j] = int32(r.u32())
}
for j := range layer.Scale {
layer.Scale[j] = r.f32()
}
}
}
return view, r.err
}
// viewResource 按ECMA-363第9.8.2节读取绘制轮次与雾效
// 入参: r 字段读取器, name 资源名
// 返回: error 结构或预算错误
func (d *u3dDecoder) viewResource(r *u3dValues, name string) error {
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/34 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 48); err != nil {
return err
}
view := U3DViewResource{Name: name, Passes: make([]U3DViewPass, int(count))}
for i := range view.Passes {
if err := d.ctx.Err(); err != nil {
return err
}
pass := &view.Passes[i]
pass.Root, pass.Attributes, pass.FogMode = r.text(), r.u32(), r.u32()
if pass.Attributes > 1 || pass.FogMode > 2 {
return fmt.Errorf("invalid U3D view fog parameters")
}
for j := range pass.FogColor {
pass.FogColor[j] = r.f32()
}
pass.FogNear, pass.FogFar = r.f32(), r.f32()
}
d.model.Views = append(d.model.Views, view)
return r.err
}