feat(文件拆分): 拆分几何三维基础能力
build.yaml / build (push) Successful in 48s

This commit is contained in:
xiaoqidun committed 2026-10-14 00:44:44 +08:00
1 parent 64f9ec4241
commit 0be872691d
76 files changed
+1926 -1307

No files matched your search

+681
View File
@@ -0,0 +1,681 @@
// 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 threed 提供U3D模型解码、场景求值及软件绘制能力
package threed
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"math"
)
// DefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销
const DefaultBytes = 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
Textures []U3DTexture
Motions []U3DMotion
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
LineShaders [][]string
PointShaders [][]string
View *U3DView
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
}
// 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 保存材质与八个纹理通道,Textures中的空项表示未启用
type U3DShader struct {
Name, Material string
Attributes, AlphaFunction, Blend uint32
RenderPass uint32
AlphaReference float32
Textures [8]*U3DTextureLayer
}
// 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
textures map[string]int
}
// DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源
// 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验
// 渐进网格尚未支持
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
// 入参: 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 = DefaultBytes
}
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 || block.kind == 0xffffff3e || block.kind == 0xffffff3f || block.kind == 0xffffff5c
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.checkTextures(); err != nil {
return nil, err
}
if err := d.checkParents(); err != nil {
return nil, err
}
if err := d.checkBoneWeights(); 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 r.err != nil {
return r.err
}
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
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()
if r.err != nil {
return "", r.err
}
if name == "" {
return "", fmt.Errorf("cannot redefine U3D default resource")
}
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 0xffffff36, 0xffffff37:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D primitive chain")
}
err = d.primitiveDeclaration(&r, name, block.kind == 0xffffff36)
case 0xffffff3e, 0xffffff3f:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D primitive continuation location")
}
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
case 0xffffff43, 0xffffff44:
if (chain != 0 && chain != 1) || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D animation or bone weight modifier chain")
}
if _, exists := d.nodes[name]; chain == 0 && !exists || chain == 1 && d.meshes[name] == nil {
return name, fmt.Errorf("missing U3D animation or bone weight modifier target")
}
if block.kind == 0xffffff43 {
err = d.animation(&r, name, chain, index)
} else {
err = d.boneWeights(&r, name, chain, index)
}
case 0xffffff45:
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, 0xffffff56:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D palette resource location")
}
err = d.resource(&r, block.kind, name)
case 0xffffff55:
if chain >= 0 && (chain != 2 || index != 0) {
return name, fmt.Errorf("invalid U3D texture chain")
}
err = d.textureDeclaration(&r, name)
case 0xffffff5c:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D texture continuation location")
}
err = d.textureContinuation(&r, name)
default:
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")
}
flags := r.u32()
if flags & ^uint32(7) != 0 {
return fmt.Errorf("unsupported U3D shading modifier target")
}
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()
}
}
if flags&1 != 0 {
d.model.Nodes[index].Shaders = lists
}
if flags&2 != 0 {
d.model.Nodes[index].LineShaders = lists
}
if flags&4 != 0 {
d.model.Nodes[index].PointShaders = 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 0xffffff56:
return d.motion(r, name)
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")
}
for channel := range shader.Textures {
if channels&(1<<channel) == 0 {
continue
}
if err := d.reserve(1, 192); err != nil {
return err
}
layer := &U3DTextureLayer{Texture: r.text(), Intensity: r.f32(), Blend: r.u8(), BlendSource: r.u8(), BlendConstant: r.f32(), Mode: r.u8(), Alpha: alpha&(1<<channel) != 0}
for i := range layer.Transform {
layer.Transform[i] = r.f32()
}
for i := range layer.WrapTransform {
layer.WrapTransform[i] = r.f32()
}
layer.Repeat = r.u8()
if r.err != nil {
return r.err
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 {
return fmt.Errorf("invalid U3D texture layer")
}
shader.Textures[channel] = layer
}
d.model.Shaders = append(d.model.Shaders, shader)
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
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"context"
"fmt"
"io"
)
// U3DAnimation 保存ECMA-363第9.7.3节的动画修饰器,Index为链内位置
// Attributes的1、2、4、8分别表示启动、锁定根骨骼、单轨播放和动作间平滑过渡
// TimeScale为整体时间倍率,BlendTime以毫秒计;Motions为空时使用无轨道的默认动作
type U3DAnimation struct {
Index, Attributes uint32
TimeScale, BlendTime float32
Motions []U3DAnimationMotion
}
// U3DAnimationMotion 保存动画中的动作引用,空名称引用默认动作
// Attributes的1、2分别表示循环和同步结束,TimeOffset以毫秒计,TimeScale为动作时间倍率
type U3DAnimationMotion struct {
Name string
Attributes uint32
TimeOffset, TimeScale float32
}
// animation 读取动画参数及有序动作引用,不在解码时调度或求值
// 入参: r 字段读取器, name 所属链名, chain 链类型, index 链内位置
// 返回: error 结构、参数、预算或取消错误
func (d *u3dDecoder) animation(r *u3dValues, name string, chain int, index uint32) error {
animation := U3DAnimation{Index: index, Attributes: r.u32(), TimeScale: r.f32()}
count := r.u32()
if r.err != nil {
return r.err
}
if len(r.data)-r.pos < 4 || uint64(count) > uint64(len(r.data)-r.pos-4)/14 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 32); err != nil {
return err
}
if count != 0 {
animation.Motions = make([]U3DAnimationMotion, int(count))
}
for i := range animation.Motions {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
motion := &animation.Motions[i]
motion.Name, motion.Attributes = r.text(), r.u32()
motion.TimeOffset, motion.TimeScale = r.f32(), r.f32()
if r.err != nil {
return r.err
}
}
animation.BlendTime = r.f32()
if err := r.done(); err != nil {
return err
}
if err := animation.validate(d.ctx); err != nil {
return err
}
if chain == 1 {
mesh := &d.meshes[name].mesh
mesh.Animations = append(mesh.Animations, animation)
} else {
node := &d.model.Nodes[d.nodes[name]]
node.Animations = append(node.Animations, animation)
}
return nil
}
// validate 校验动画属性及有限数值,保留零倍率与反向播放倍率
// 入参: ctx 取消上下文
// 返回: error 参数或取消错误
func (animation U3DAnimation) validate(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return err
}
if animation.Index == 0 || animation.Attributes & ^uint32(15) != 0 || !finite(float64(animation.TimeScale)) || !finite(float64(animation.BlendTime)) || animation.BlendTime < 0 {
return fmt.Errorf("invalid U3D animation parameters")
}
for i, motion := range animation.Motions {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
if motion.Attributes & ^uint32(3) != 0 || !finite(float64(motion.TimeOffset)) || !finite(float64(motion.TimeScale)) {
return fmt.Errorf("invalid U3D animation motion parameters")
}
}
return nil
}
// checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力
// 返回: error 引用、能力或取消错误
func (r *Renderer) checkAnimations() error {
check := func(animations []U3DAnimation, evaluate bool) error {
for _, animation := range animations {
if err := animation.validate(r.ctx); err != nil {
return err
}
for i, motion := range animation.Motions {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if motion.Name == "" {
continue
}
index, exists := r.motionIndex[motion.Name]
if !exists {
return fmt.Errorf("missing U3D animation motion %q", motion.Name)
}
if evaluate && len(animation.Motions) > 1 && len(r.model.Motions[index].Tracks) != 0 {
return fmt.Errorf("unsupported U3D animation evaluation for motion %q", motion.Name)
}
}
}
return nil
}
var active []bool
if r.connected != nil {
if err := r.Reserve(uint64(len(r.model.Meshes)), 1); err != nil {
return err
}
defer func() { r.remaining += uint64(len(active)) }()
active = make([]bool, len(r.model.Meshes))
for i, node := range r.model.Nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if r.connected[i] && node.Kind == "Model" {
if mesh, exists := r.meshes[node.Resource]; exists {
active[mesh] = true
}
}
}
}
for i := range r.model.Meshes {
if err := r.ctx.Err(); err != nil {
return err
}
if err := check(r.model.Meshes[i].Animations, active == nil || active[i]); err != nil {
return err
}
}
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if err := check(r.model.Nodes[i].Animations, r.connected == nil || r.connected[i]); err != nil {
return err
}
}
return r.ctx.Err()
}
+366
View File
@@ -0,0 +1,366 @@
// 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 threed
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
reserve func(uint64, uint64) error
}
// u3dHistogram 保存一个动态压缩上下文的频次及前缀和
type u3dHistogram struct {
counts []uint32
tree []uint32
total uint32
}
// finish 校验压缩块的冲刷字段及结束位置
// 返回: error 截断、冲刷或多余数据错误
func (r *u3dBits) finish() error {
if r.err != nil {
return r.err
}
if r.pos > uint64(len(r.data))*8 {
return io.ErrUnexpectedEOF
}
if r.u32() != 0 {
return fmt.Errorf("invalid U3D compression flush")
}
if r.err != nil {
return r.err
}
if (r.pos-16+7)/8 != uint64(len(r.data)) {
return fmt.Errorf("unexpected U3D compressed block data")
}
return nil
}
// take 读取定长字段,失败后不继续推进位置
// 入参: 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 {
return r.dynamicValue(h, false)
}
// dynamicValue 按字段位宽处理动态上下文的逃逸字面量,分配前检查可选预算
// 入参: h 频次上下文, small 是否为8位字段
// 返回: uint32 解码值
func (r *u3dBits) dynamicValue(h *u3dHistogram, small bool) uint32 {
if r.err != nil {
return 0
}
if h.total == 0 {
if r.reserve != nil {
if err := r.reserve(1, 32); err != nil {
r.err = err
return 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)
r.histogramAdd(h, symbol)
if r.err != nil {
return 0
}
if symbol != 0 {
if small && symbol > 256 {
r.err = fmt.Errorf("invalid U3D compressed byte")
}
return symbol - 1
}
var value uint32
if small {
value = uint32(r.byteValue())
} else {
value = r.u32()
}
if value < 0xffff && r.err == nil {
r.histogramAdd(h, value+1)
}
return value
}
// histogramAdd 在频次数组扩展前计入解码预算
// 入参: h 频次上下文, symbol 待更新符号
func (r *u3dBits) histogramAdd(h *u3dHistogram, symbol uint32) {
if r.err != nil {
return
}
if r.reserve != nil && symbol <= 0xffff && int(symbol) >= len(h.counts) {
length := min(65536, max(int(symbol)+1, len(h.counts)*2))
if err := r.reserve(uint64(length-len(h.counts)), 32); err != nil {
r.err = err
return
}
}
h.add(symbol)
}
// 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]
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// 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 threed
import (
"fmt"
"math"
)
// DepthRange 计算可见模型几何的相机深度范围,不为自动裁剪复制顶点
// 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误
func (r *Renderer) 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 := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(j)
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
for _, corner := range 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
}
+26
View File
@@ -0,0 +1,26 @@
// 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 threed
import (
"math"
)
// finite 判断数值是否有限
// 入参: value 数值
// 返回: bool 是否有限
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
+379
View File
@@ -0,0 +1,379 @@
// 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 threed
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 *Renderer) 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 && node.Resource != "" {
continue
}
light := U3DLight{Attributes: 1, Color: [3]float32{.75, .75, .75}}
if ok {
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 *Renderer) 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
}
// shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮
// 入参: name 着色器名
// 返回: uint32 绘制轮次位图
func (r *Renderer) shaderPass(name string) uint32 {
if index, exists := r.shaders[name]; exists {
return r.model.Shaders[index].RenderPass
}
return 1
}
// style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源
// 入参: name 着色器名
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误
func (r *Renderer) style(name string) (u3dRenderStyle, error) {
style := u3dRenderStyle{shader: U3DShader{Attributes: 1, Blend: 0x606, AlphaFunction: 0x617, RenderPass: 1}, ambient: [3]float64{.75, .75, .75}, opacity: 1}
if index, ok := r.shaders[name]; ok {
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]
style.ambient, style.diffuse = [3]float64{.2, .2, .2}, [3]float64{.8, .8, .8}
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 *Renderer) 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
}
copy(color[:3], style.emissive[:])
if r.mode == U3DRenderShadedIllustration {
for c := range 3 {
color[c] += max(0, style.diffuse[c]) / 4
}
}
if r.lighting != nil {
for c := range 3 {
style.diffuse[c] += r.lighting.DiffuseOffset[c]
}
}
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
}
// u3dFragmentColor 对点、线、面的插值颜色应用透明度测试和相机空间雾效
// 入参: color 待更新的未预乘颜色, position 相机位置, shader 着色器, pass 绘制轮次
// 返回: bool 是否绘制, error 非有限颜色错误
func u3dFragmentColor(color *[4]float64, position u3dVector, shader *U3DShader, pass *U3DViewPass) (bool, error) {
for _, value := range color {
if !finite(value) {
return false, fmt.Errorf("invalid U3D fragment color")
}
}
color[3] = min(1, max(0, color[3]))
if shader.Attributes&2 != 0 && !u3dAlphaTest(shader.AlphaFunction, color[3], float64(shader.AlphaReference)) {
return false, nil
}
if pass.Attributes&1 != 0 {
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]))
}
return true, 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
}
+454
View File
@@ -0,0 +1,454 @@
// 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 threed
import (
"fmt"
"math"
)
// drawPrimitives 绘制作者点集及线集,独立着色且不参与面的背向剔除
// 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误
func (r *Renderer) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error {
if r.depthOnly {
return nil
}
shaded := r.mode.shaded()
for index := range len(mesh.Lines) + len(mesh.Points) {
shadingIndex, corners := mesh.primitive(len(mesh.Faces) + index)
point := index >= len(mesh.Lines)
if index%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(shadingIndex) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D render primitive shading index")
}
shading := mesh.Shadings[shadingIndex]
if shading.Attributes > 3 || len(shading.TextureDimensions) > 8 {
return fmt.Errorf("invalid U3D render primitive shading")
}
var world [2]u3dVector
var vertices [2]u3dVertex
for i, corner := range corners {
if uint64(corner.Position) >= uint64(len(positions)) || shaded && (!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 primitive corner index")
}
world[i] = positions[corner.Position]
vertices[i].point = r.toCamera.point(world[i])
for _, value := range vertices[i].point {
if !finite(value) {
return fmt.Errorf("invalid U3D primitive camera position")
}
}
}
shaderNames := []string{""}
shaders := node.LineShaders
if point {
shaders = node.PointShaders
}
if uint64(shadingIndex) < uint64(len(shaders)) {
shaderNames = shaders[shadingIndex]
}
for _, name := range shaderNames {
if err := r.ctx.Err(); err != nil {
return err
}
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
var err error
if shaded {
style, err = r.style(name)
if err != nil {
return err
}
}
if style.shader.RenderPass&(1<<passIndex) == 0 {
continue
}
if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606
}
textures, err := r.shaderTextures(style.shader)
if err != nil {
return err
}
for i, corner := range corners {
var normal u3dVector
if shaded && !mesh.ExcludeNormals {
normal = normals[corner.Normal]
}
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
return err
}
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if !shaded {
continue
}
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
vertices[i].opacityOverride = true
}
if r.opacity >= 0 && textures == nil {
vertices[i].color[3] *= r.opacity
}
}
if point {
if err := r.modePoint(vertices[0], style.shader, pass); err != nil {
return err
}
continue
}
if r.mode == U3DRenderVertices || r.mode == U3DRenderShadedVertices {
for _, vertex := range vertices {
fresh, err := r.uniquePrimitive(vertex, vertex, style.shader)
if err != nil {
return err
}
if fresh {
if err := r.modePoint(vertex, style.shader, pass); err != nil {
return err
}
}
}
} else if err := r.modeLine(vertices[0], vertices[1], u3dVector{}, u3dVector{}, style.shader, pass); err != nil {
return err
}
for index, section := range r.sections {
if index%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if section.intersection != nil {
if err := r.lineIntersection(vertices, index); err != nil {
return err
}
}
}
}
}
return nil
}
// lineIntersection 绘制作者线段与剖切面的交点,不绘制共面线段
// 入参: line 相机空间线段, index 剖切面索引
// 返回: error 数值、资源或取消错误
func (r *Renderer) lineIntersection(line [2]u3dVertex, index int) error {
section := r.sections[index]
a, b := u3dPlaneDistance(section.plane, line[0].point), u3dPlaneDistance(section.plane, line[1].point)
if !finite(a) || !finite(b) {
return fmt.Errorf("invalid U3D line intersection distance")
}
if a == 0 && b == 0 || a < 0 && b < 0 || a > 0 && b > 0 {
return nil
}
point := u3dInterpolate(line[0], line[1], u3dClipFraction(a, b))
for i, other := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if i != index {
distance := u3dPlaneDistance(other.plane, point.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D line intersection clip")
}
if distance < 0 {
return nil
}
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
distance := u3dPlaneDistance(plane, point.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D projected intersection")
}
if distance < 0 {
return nil
}
}
return r.rasterSectionLine(point, point, *section.intersection, u3dVector{}, u3dVector{})
}
// frustumPlanes 返回保留正侧的六个相机视锥面
// 返回: [6][4]float64 近、远、左右及上下平面
func (r *Renderer) 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
}
result.textureMask = a.textureMask | b.textureMask
result.opacityOverride = a.opacityOverride
if result.textureMask != 0 {
for layer := range result.texture {
if result.textureMask&(1<<layer) == 0 {
continue
}
for c := range result.texture[layer] {
result.texture[layer][c] = a.texture[layer][c]*(1-t) + b.texture[layer][c]*t
}
}
}
return result
}
// sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪
// 入参: triangle 原三角面, index 剖切面索引
// 返回: error 数值、资源或取消错误
func (r *Renderer) 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 *Renderer) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error {
a.color = [4]float64{rgb[0], rgb[1], rgb[2], 1}
b.color = a.color
return r.rasterLine(a, b, normal, origin, U3DShader{Blend: 0x606}, U3DViewPass{})
}
// rasterLine 绘制单像素线段,颜色与深度按透视插值,面深度偏移防止自身遮挡
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
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))
r.order++
previousPixel := -1
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 := math.Floor(x[0] + float64(i)*(x[1]-x[0])/float64(count))
py := math.Floor(y[0] + float64(i)*(y[1]-y[0])/float64(count))
if px < 0 || py < 0 || px >= float64(r.output.Rect.Dx()) || py >= float64(r.output.Rect.Dy()) {
continue
}
pixel := int(py)*r.output.Rect.Dx() + int(px)
if pixel == previousPixel {
continue
}
previousPixel = pixel
depth := a.point[2] + (b.point[2]-a.point[2])*t
weight := t
if r.camera.Perspective {
depth = 1 / (q[0]*(1-t) + q[1]*t)
weight = t * q[1] * depth
}
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)))
if depth <= r.frame[pixel].depth {
vertex := u3dInterpolate(a, b, weight)
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
return err
}
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil {
return err
}
if !visible {
continue
}
if err := r.fragment(pixel, depth, vertex.color, shader.Blend); err != nil {
return err
}
}
}
return nil
}
+471
View File
@@ -0,0 +1,471 @@
// 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 threed
import (
"fmt"
)
// U3D线集增量编码的算术编码上下文
const (
u3dLinePositionSign = iota
u3dLinePositionX
u3dLinePositionY
u3dLinePositionZ
u3dLineNormalCount
u3dLineNormalSign
u3dLineNormalX
u3dLineNormalY
u3dLineNormalZ
u3dLineCount
u3dLineShading
u3dLineNormalIndex
u3dLineDiffuseDuplicate
u3dLineSpecularDuplicate
u3dLineTextureDuplicate
u3dLineDiffuseSign
u3dLineSpecularSign
u3dLineTextureSign
u3dLineColorR
u3dLineColorG
u3dLineColorB
u3dLineColorA
u3dLineTextureU
u3dLineTextureV
u3dLineTextureS
u3dLineTextureT
u3dLineContextCount
)
// u3dLineState 按顶点缓存点集及线集属性预测
type u3dLineState struct {
offsets [11]int
stride int
stats []float64
pointOffsets []uint32
pointPredicted []bool
}
// u3dLineReader 保存当前续块的独立压缩上下文
type u3dLineReader struct {
values *u3dValues
bits *u3dBits
hist [u3dLineContextCount]u3dHistogram
}
// primitiveDeclaration 读取ECMA-363第9.6.2和9.6.3节的点集及线集声明
// 入参: r 字段读取器, name 资源名, points 是否为点集
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) primitiveDeclaration(r *u3dValues, name string, points bool) error {
if _, exists := d.meshes[name]; exists {
return fmt.Errorf("duplicate U3D model resource %q", name)
}
if r.u32() != 0 || r.u32() != 0 {
return fmt.Errorf("invalid U3D primitive declaration")
}
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name}, line: new(u3dLineState), points: points}
for i := range decl.counts {
decl.counts[i] = r.u32()
}
if err := d.meshShadings(r, decl); err != nil {
return err
}
for i := range decl.mesh.Quality {
decl.mesh.Quality[i] = r.u32()
}
for i := range decl.mesh.InverseQuantization {
decl.mesh.InverseQuantization[i] = r.f32()
}
for range 3 {
if r.u32() != 0 {
return fmt.Errorf("invalid U3D reserved primitive parameter")
}
}
bones, err := d.skeleton(r)
if err != nil {
return err
}
decl.mesh.Bones = bones
if err := r.done(); err != nil {
return err
}
if decl.counts[1] == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty primitive set")
}
}
decl.decoded = true
}
state := decl.line
var used [11]bool
used[0] = true
for _, shading := range decl.mesh.Shadings {
used[1] = used[1] || shading.Attributes&1 != 0
used[2] = used[2] || shading.Attributes&2 != 0
for layer := range shading.TextureDimensions {
used[3+layer] = true
}
}
for i, present := range used {
state.offsets[i] = -1
if present {
state.offsets[i] = state.stride
state.stride += 5
}
}
c := decl.counts
primitiveBytes := uint64(100)
if points {
primitiveBytes = 52
}
bytes := uint64(c[0])*primitiveBytes + (uint64(c[1])+uint64(c[2]))*12 + (uint64(c[3])+uint64(c[4])+uint64(c[5]))*16
if err := d.reserve(bytes, 1); err != nil {
return err
}
if err := d.reserve(uint64(c[1]), uint64(state.stride)*8); err != nil {
return err
}
m := &decl.mesh
if points {
m.Points = make([]U3DPoint, 0, int(c[0]))
} else {
m.Lines = make([]U3DLine, 0, int(c[0]))
}
m.Positions, m.Normals = make([][3]float32, 0, int(c[1])), make([][3]float32, 0, int(c[2]))
m.Diffuse, m.Specular, m.TextureCoordinates = make([][4]float32, 0, int(c[3])), make([][4]float32, 0, int(c[4])), make([][4]float32, 0, int(c[5]))
state.stats = make([]float64, int(c[1])*state.stride)
if points {
if err := d.reserve(uint64(c[1])*5+4, 1); err != nil {
return err
}
state.pointOffsets = make([]uint32, 1, int(c[1])+1)
state.pointPredicted = make([]bool, int(c[1]))
}
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
}
// average 取得指定顶点各属性预测,点集法线使用缓存的球面均值
// 入参: position 顶点索引
// 返回: [11][4]float64 法线、漫反射、镜面及八层纹理预测
func (s *u3dLineState) average(position uint32) (values [11][4]float64) {
for slot, offset := range s.offsets {
if offset < 0 {
continue
}
base := int(position)*s.stride + offset
count := s.stats[base+4]
if count != 0 {
for i := range values[slot] {
values[slot][i] = s.stats[base+i] / count
}
}
}
return values
}
// accumulateCorners 累计点或线端的属性,用于后续位置预测
// 入参: mesh 属性数组, shadingIndex 着色索引, corners 新增角点
func (s *u3dLineState) accumulateCorners(mesh *U3DMesh, shadingIndex uint32, corners []U3DCorner) {
shading := mesh.Shadings[shadingIndex]
for _, corner := range corners {
var values [11][4]float32
var used [11]bool
normal := mesh.Normals[corner.Normal]
values[0] = [4]float32{normal[0], normal[1], normal[2], 0}
used[0] = true
if shading.Attributes&1 != 0 {
values[1], used[1] = mesh.Diffuse[corner.Diffuse], true
}
if shading.Attributes&2 != 0 {
values[2], used[2] = mesh.Specular[corner.Specular], true
}
for layer := range shading.TextureDimensions {
values[3+layer], used[3+layer] = mesh.TextureCoordinates[corner.Texture[layer]], true
}
for slot, present := range used {
if !present {
continue
}
base := int(corner.Position)*s.stride + s.offsets[slot]
for i, value := range values[slot] {
s.stats[base+i] += float64(value)
}
s.stats[base+4]++
}
}
}
// value 按指定上下文和位宽读取点集或线集字段
// 入参: context 压缩上下文, small 是否为8位字段
// 返回: uint32 字段值
func (r *u3dLineReader) value(context int, small bool) uint32 {
if r.bits != nil {
return r.bits.dynamicValue(&r.hist[context], small)
}
if small {
return uint32(r.values.u8())
}
return r.values.u32()
}
// index 读取静态索引并校验声明范围
// 入参: count 可用索引数量
// 返回: uint32 索引, error 读取或范围错误
func (r *u3dLineReader) index(count uint32) (uint32, error) {
if count == 0 {
return 0, fmt.Errorf("invalid U3D primitive position range")
}
var value uint32
if r.bits != nil {
value = r.bits.index(count)
} else {
value = r.values.u32()
}
if err := r.err(); err != nil {
return 0, err
}
if value >= count {
return 0, fmt.Errorf("invalid U3D primitive position index")
}
return value, nil
}
// err 返回当前字段或算术解码错误
// 返回: error 读取错误
func (r *u3dLineReader) err() error {
if r.bits != nil {
return r.bits.err
}
return r.values.err
}
// reconstruct 按符号位和独立分量上下文执行逆量化
// 入参: prediction 预测值, scale 逆量化系数, signContext 符号上下文, componentContext 分量首上下文, dimensions 分量数
// 返回: [4]float32 重建属性, error 数值或读取错误
func (r *u3dLineReader) reconstruct(prediction [4]float64, scale float32, signContext, componentContext, dimensions int) (values [4]float32, err error) {
signs := r.value(signContext, true)
if signs >= 1<<dimensions {
return values, fmt.Errorf("invalid U3D primitive difference signs")
}
for i := range dimensions {
difference := float64(r.value(componentContext+i, false)) * float64(scale)
if signs&(1<<i) != 0 {
difference = -difference
}
values[i] = float32(prediction[i] + difference)
if !finite(float64(values[i])) {
return values, fmt.Errorf("invalid U3D reconstructed primitive attribute")
}
}
return values, r.err()
}
// attribute 读取颜色或纹理属性,重复标记引用所属属性池的末项
// 入参: pool 属性池, limit 声明总数, prediction 预测值, scale 逆量化系数, duplicate 重复上下文, sign 符号上下文, component 分量首上下文
// 返回: uint32 属性索引, error 读取或范围错误
func (r *u3dLineReader) attribute(pool *[][4]float32, limit uint32, prediction [4]float64, scale float32, duplicate, sign, component int) (uint32, error) {
flag := r.value(duplicate, true)
if err := r.err(); err != nil {
return 0, err
}
if flag == 2 && len(*pool) > 0 {
return uint32(len(*pool) - 1), nil
}
if flag != 0 || uint64(len(*pool)) >= uint64(limit) {
return 0, fmt.Errorf("invalid U3D primitive attribute pool")
}
value, err := r.reconstruct(prediction, scale, sign, component, 4)
if err != nil {
return 0, err
}
index := uint32(len(*pool))
*pool = append(*pool, value)
return index, nil
}
// primitiveContinuation 按位置分段恢复点集及线集,预测状态跨续块保存
// 入参: r 字段读取器, name 资源名, points 是否为点集
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) primitiveContinuation(r *u3dValues, name string, points bool) error {
decl := d.meshes[name]
if decl == nil || decl.line == nil || decl.points != points || decl.decoded || r.u32() != 0 {
return fmt.Errorf("invalid U3D primitive continuation reference")
}
start, end := r.u32(), r.u32()
mesh := &decl.mesh
if r.err != nil {
return r.err
}
if uint64(start) != uint64(len(mesh.Positions)) || end <= start || end > decl.counts[1] {
return fmt.Errorf("invalid U3D primitive resolution range")
}
reader := &u3dLineReader{values: r}
var charged uint64
if !d.plain {
reader.bits = newU3DBits(r.data[r.pos:])
reader.bits.reserve = func(count, size uint64) error {
if err := d.reserve(count, size); err != nil {
return err
}
charged += count * size
return nil
}
defer func() { d.remaining += charged }()
}
for position := start; position < end; position++ {
if err := d.ctx.Err(); err != nil {
return err
}
split, err := reader.index(max(1, position))
if err != nil {
return err
}
var prediction [4]float64
if position > 0 {
for i, value := range mesh.Positions[split] {
prediction[i] = float64(value)
}
}
value, err := reader.reconstruct(prediction, mesh.InverseQuantization[0], u3dLinePositionSign, u3dLinePositionX, 3)
if err != nil {
return err
}
mesh.Positions = append(mesh.Positions, [3]float32{value[0], value[1], value[2]})
normalStart := uint32(len(mesh.Normals))
normals := reader.value(u3dLineNormalCount, false)
if normals > decl.counts[2]-normalStart {
return fmt.Errorf("invalid U3D primitive normal count")
}
if points && position > 0 && normals > 0 && !decl.line.pointPredicted[split] {
first, last := decl.line.pointOffsets[split], decl.line.pointOffsets[split+1]
prediction, err := d.pointNormalPrediction(mesh, mesh.Points[first:last])
if err != nil {
return err
}
base := int(split)*decl.line.stride + decl.line.offsets[0]
for i, value := range prediction {
decl.line.stats[base+i] = value * float64(last-first)
}
decl.line.pointPredicted[split] = true
}
attributes := decl.line.average(split)
for i := uint32(0); i < normals; i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
value, err := reader.reconstruct(attributes[0], mesh.InverseQuantization[1], u3dLineNormalSign, u3dLineNormalX, 3)
if err != nil {
return err
}
mesh.Normals = append(mesh.Normals, [3]float32{value[0], value[1], value[2]})
}
lines := reader.value(u3dLineCount, false)
if lines > decl.counts[0]-uint32(len(mesh.Lines)+len(mesh.Points)) {
return fmt.Errorf("invalid U3D primitive count")
}
for i := uint32(0); i < lines; i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
line := U3DLine{Shading: reader.value(u3dLineShading, false)}
if uint64(line.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D primitive shading index")
}
corners := line.Corners[:1]
corners[0].Position = position
if !points {
first, err := reader.index(position)
if err != nil {
return err
}
line.Corners[0].Position, line.Corners[1].Position = first, position
corners = line.Corners[:]
}
shading := mesh.Shadings[line.Shading]
for j := range corners {
corner := &corners[j]
normal := reader.value(u3dLineNormalIndex, false)
if normal >= normals {
return fmt.Errorf("invalid U3D primitive local normal index")
}
corner.Normal = normalStart + normal
if shading.Attributes&1 != 0 {
corner.Diffuse, err = reader.attribute(&mesh.Diffuse, decl.counts[3], attributes[1], mesh.InverseQuantization[3], u3dLineDiffuseDuplicate, u3dLineDiffuseSign, u3dLineColorR)
if err != nil {
return err
}
}
if shading.Attributes&2 != 0 {
corner.Specular, err = reader.attribute(&mesh.Specular, decl.counts[4], attributes[2], mesh.InverseQuantization[4], u3dLineSpecularDuplicate, u3dLineSpecularSign, u3dLineColorR)
if err != nil {
return err
}
}
for layer := range shading.TextureDimensions {
corner.Texture[layer], err = reader.attribute(&mesh.TextureCoordinates, decl.counts[5], attributes[3+layer], mesh.InverseQuantization[2], u3dLineTextureDuplicate, u3dLineTextureSign, u3dLineTextureU)
if err != nil {
return err
}
}
}
if err := reader.err(); err != nil {
return err
}
if points {
mesh.Points = append(mesh.Points, U3DPoint{Shading: line.Shading, Corners: [1]U3DCorner{corners[0]}})
} else {
mesh.Lines = append(mesh.Lines, line)
}
decl.line.accumulateCorners(mesh, line.Shading, corners)
}
if points {
decl.line.pointOffsets = append(decl.line.pointOffsets, uint32(len(mesh.Points)))
}
if err := reader.err(); err != nil {
return err
}
}
if b := reader.bits; b != nil {
if err := b.finish(); err != nil {
return err
}
r.pos = len(r.data)
}
if err := r.done(); err != nil {
return err
}
if end == decl.counts[1] {
for i, count := range []int{len(mesh.Lines) + len(mesh.Points), len(mesh.Positions), len(mesh.Normals), len(mesh.Diffuse), len(mesh.Specular), len(mesh.TextureCoordinates)} {
if uint64(count) != uint64(decl.counts[i]) {
return fmt.Errorf("U3D primitive set does not match declaration")
}
}
decl.decoded = true
d.remaining += uint64(len(decl.line.stats)) * 8
decl.line.stats = nil
if points {
d.remaining += uint64(cap(decl.line.pointOffsets))*4 + uint64(len(decl.line.pointPredicted))
decl.line.pointOffsets, decl.line.pointPredicted = nil, nil
}
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"math"
)
// u3dTextureInverse 计算包裹变换的逆矩阵,保留非仿射齐次分量
// 入参: matrix 按列排列的包裹变换
// 返回: [16]float64 逆矩阵, error 奇异矩阵或非有限结果
func u3dTextureInverse(matrix [16]float32) ([16]float64, error) {
var rows [4][8]float64
for row := range 4 {
for col := range 4 {
rows[row][col] = float64(matrix[col*4+row])
if !finite(rows[row][col]) {
return [16]float64{}, fmt.Errorf("invalid U3D texture wrap transform")
}
}
rows[row][row+4] = 1
}
for col := range 4 {
pivot := col
for row := col + 1; row < 4; row++ {
if math.Abs(rows[row][col]) > math.Abs(rows[pivot][col]) {
pivot = row
}
}
if rows[pivot][col] == 0 {
return [16]float64{}, fmt.Errorf("singular U3D texture wrap transform")
}
rows[col], rows[pivot] = rows[pivot], rows[col]
divisor := rows[col][col]
for i := range 8 {
rows[col][i] /= divisor
}
for row := range 4 {
if row == col {
continue
}
factor := rows[row][col]
for i := range 8 {
rows[row][i] -= factor * rows[col][i]
}
}
}
var result [16]float64
for row := range 4 {
for col := range 4 {
value := rows[row][col+4]
if !finite(value) {
return [16]float64{}, fmt.Errorf("invalid U3D inverse texture wrap transform")
}
result[col*4+row] = value
}
}
return result, nil
}
// generated 将插值后的位置或法线映射到纹理,再应用纹理变换
// 经线未定义时取0,球心纬度取1/2,反射方向奇点取图像中心
// 入参: channel 通道索引, input 模型位置或相机法线, position 相机空间位置
// 返回: [3]float64 齐次纹理坐标, error 非有限值或无效投影
func (s *u3dTextureSet) generated(channel int, input [3]float64, position u3dVector) ([3]float64, error) {
layer := s.layers[channel]
for _, value := range input {
if !finite(value) {
return [3]float64{}, fmt.Errorf("invalid U3D texture mapping input")
}
}
uv := [4]float64{0, 0, 0, 1}
if layer.Mode == 4 {
incident := u3dVector{0, 0, 1}
if s.perspective {
incident = position.unit()
}
normal := u3dVector(input).unit()
factor := 2 * normal.dot(incident)
reflected := incident.sub(u3dVector{factor * normal[0], factor * normal[1], factor * normal[2]})
denominator := 2 * math.Hypot(math.Hypot(reflected[0], reflected[1]), 1-reflected[2])
uv[0], uv[1] = .5, .5
if denominator != 0 {
uv[0] += reflected[0] / denominator
uv[1] -= reflected[1] / denominator
}
} else {
var point [4]float64
for row := range 4 {
point[row] = s.wrap[channel][12+row]
for col, value := range input {
point[row] += s.wrap[channel][col*4+row] * value
}
}
for i := range 3 {
point[i] /= point[3]
if !finite(point[i]) {
return [3]float64{}, fmt.Errorf("invalid U3D texture wrap projection")
}
}
switch layer.Mode {
case 1:
uv[0], uv[1] = point[0], point[1]
case 2, 3:
if point[0] != 0 || point[1] != 0 {
uv[0] = math.Atan2(point[1], point[0]) / (2 * math.Pi)
if uv[0] < 0 {
uv[0]++
}
}
uv[1] = point[2]
if layer.Mode == 3 {
direction := (u3dVector{point[0], point[1], point[2]}).unit()
uv[1] = math.Acos(min(1, max(-1, direction[2]))) / math.Pi
}
}
}
var result [3]float64
for component, row := range [3]int{0, 1, 3} {
for col, value := range uv {
result[component] += float64(layer.Transform[col*4+row]) * value
}
if !finite(result[component]) {
return [3]float64{}, fmt.Errorf("invalid U3D generated texture coordinate")
}
}
return result, nil
}
+344
View File
@@ -0,0 +1,344 @@
// 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 threed
import (
"fmt"
"io"
)
// U3DMesh 保存静态三角面、线段和点的共享属性数组,索引对应原始作者几何
type U3DMesh struct {
Name string
ExcludeNormals bool
Positions, Normals [][3]float32
Diffuse, Specular, TextureCoordinates [][4]float32
Shadings []U3DShading
Faces []U3DFace
Lines []U3DLine
Points []U3DPoint
Bones []U3DBone
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
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
}
// U3DLine 保存着色描述及线段两端的独立属性索引
type U3DLine struct {
Shading uint32
Corners [2]U3DCorner
}
// U3DPoint 保存点集着色描述及独立属性索引
type U3DPoint struct {
Shading uint32
Corners [1]U3DCorner
}
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
type U3DCorner struct {
Position, Normal, Diffuse, Specular uint32
Texture [8]uint32
}
// u3dMeshDeclaration 保存几何声明及续块接收状态
type u3dMeshDeclaration struct {
mesh U3DMesh
counts [7]uint32
decoded bool
line *u3dLineState
points bool
}
// primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组
// 入参: index 图元下标,依次为三角面、线段和点
// 返回: uint32 着色编号, []U3DCorner 原数组中的角点切片
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
if index < len(m.Faces) {
return m.Faces[index].Shading, m.Faces[index].Corners[:]
}
index -= len(m.Faces)
if index < len(m.Lines) {
line := &m.Lines[index]
return line.Shading, line.Corners[:]
}
point := &m.Points[index-len(m.Lines)]
return point.Shading, point.Corners[:]
}
// 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()
}
if err := d.meshShadings(r, decl); err != nil {
return err
}
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()
if r.err != nil {
return r.err
}
if minimum > maximum || maximum != decl.counts[1] {
return fmt.Errorf("invalid U3D mesh resolution")
}
if minimum != maximum {
return fmt.Errorf("unsupported U3D progressive mesh")
}
bones, err := d.skeleton(r)
if err != nil {
return err
}
decl.mesh.Bones = bones
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
}
// meshShadings 读取三角面与线集共用的着色描述
// 入参: r 字段读取器, decl 资源声明
// 返回: error 结构或预算错误
func (d *u3dDecoder) meshShadings(r *u3dValues, decl *u3dMeshDeclaration) error {
if r.err != nil {
return r.err
}
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()
}
return r.err
}
// baseMesh 读取基础顶点与压缩面索引,保留各角点的独立属性
// 入参: r 字段读取器, name 网格名
// 返回: error 结构、能力、预算或取消错误
func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
decl := d.meshes[name]
if decl == nil || decl.line != 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 {
compressed = newU3DBits(r.data[r.pos:])
var charged uint64
compressed.reserve = func(count, size uint64) error {
if err := d.reserve(count, size); err != nil {
return err
}
charged += count * size
return nil
}
defer func() { d.remaining += charged }()
}
readIndex := func(count uint32) (uint32, error) {
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 err := compressed.finish(); err != nil {
return err
}
r.pos = len(r.data)
}
decl.decoded = true
return r.done()
}
+379
View File
@@ -0,0 +1,379 @@
// 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 threed
import (
"fmt"
"math"
)
const (
U3DRenderSolid U3DRenderMode = iota // 实体
U3DRenderSolidWireframe // 实体线框
U3DRenderWireframe // 线框
U3DRenderShadedWireframe // 着色线框
U3DRenderHiddenWireframe // 隐藏线框
U3DRenderVertices // 顶点
U3DRenderShadedVertices // 着色顶点
U3DRenderBoundingBox // 包围盒线框
U3DRenderBoundingBoxFaces // 包围盒面
U3DRenderBoundingBoxOutline // 包围盒面及线框
U3DRenderIllustration // 插图
U3DRenderSolidOutline // 实体轮廓
U3DRenderShadedIllustration // 着色插图
)
// U3DRenderMode 定义静态几何的呈现方式,边线和顶点使用单像素覆盖
type U3DRenderMode uint8
// u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加
type u3dPrimitiveKey struct {
a, b [7]float64
blend uint32
function uint32
reference float32
}
// u3dTexturedPrimitiveKey 额外区分纹理坐标及纹理层,不扩大普通边点缓存
type u3dTexturedPrimitiveKey struct {
base u3dPrimitiveKey
coordinates [2][8][3]float64
layers [8]*U3DTextureLayer
opacityOverride bool
}
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
// 返回: bool 是否首次绘制, error 预算或取消错误
func (r *Renderer) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, error) {
if r.primitives == nil {
return true, nil
}
left := [7]float64{a.point[0], a.point[1], a.point[2], a.color[0], a.color[1], a.color[2], a.color[3]}
right := [7]float64{b.point[0], b.point[1], b.point[2], b.color[0], b.color[1], b.color[2], b.color[3]}
for i := range left {
if left[i] != right[i] {
if left[i] > right[i] {
a, b = b, a
left, right = right, left
}
break
}
}
key := u3dPrimitiveKey{a: left, b: right, blend: shader.Blend}
if shader.Attributes&2 != 0 {
key.function, key.reference = shader.AlphaFunction, shader.AlphaReference
}
if shader.Textures != ([8]*U3DTextureLayer{}) {
textured := u3dTexturedPrimitiveKey{base: key, coordinates: [2][8][3]float64{a.texture, b.texture}, layers: shader.Textures, opacityOverride: a.opacityOverride}
if _, ok := r.texturedPrimitives[textured]; ok {
return false, nil
}
if err := r.Reserve(1, 1024); err != nil {
return false, err
}
if r.texturedPrimitives == nil {
r.texturedPrimitives = make(map[u3dTexturedPrimitiveKey]struct{})
}
r.texturedPrimitives[textured] = struct{}{}
return true, nil
}
if _, ok := r.primitives[key]; ok {
return false, nil
}
if err := r.Reserve(1, 512); err != nil {
return false, err
}
r.primitives[key] = struct{}{}
return true, nil
}
// shaded 判断绘制模式是否使用材质、法线和灯光
// 返回: bool 是否着色
func (mode U3DRenderMode) shaded() bool {
return mode == U3DRenderSolid || mode == U3DRenderSolidWireframe || mode == U3DRenderShadedWireframe || mode == U3DRenderShadedVertices || mode == U3DRenderSolidOutline || mode == U3DRenderShadedIllustration
}
// silhouette 判断绘制模式是否仅描绘轮廓边
// 返回: bool 是否使用轮廓边
func (mode U3DRenderMode) silhouette() bool {
return mode >= U3DRenderIllustration && mode <= U3DRenderShadedIllustration
}
// modeTriangle 根据当前模式绘制原三角面、原始边或顶点,不为裁剪边界生成网格边
// 入参: triangle 相机空间顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if r.depthOnly || r.mode == U3DRenderSolid || r.mode.silhouette() {
return r.sectionTriangle(triangle, shader, pass)
}
if r.mode == U3DRenderSolidWireframe {
if err := r.sectionTriangle(triangle, shader, pass); err != nil {
return err
}
}
if r.mode != U3DRenderShadedWireframe && r.mode != U3DRenderShadedVertices {
for i := range triangle {
triangle[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
}
shader = U3DShader{Blend: 0x606}
}
normal := u3dTriangleNormal([3]u3dVector{triangle[0].point, triangle[1].point, triangle[2].point})
for i, vertex := range triangle {
var err error
if r.mode == U3DRenderVertices || r.mode == U3DRenderShadedVertices {
fresh, uniqueErr := r.uniquePrimitive(vertex, vertex, shader)
if uniqueErr != nil {
return uniqueErr
}
if !fresh {
continue
}
err = r.modePoint(vertex, shader, pass)
} else {
fresh, uniqueErr := r.uniquePrimitive(vertex, triangle[(i+1)%3], shader)
if uniqueErr != nil {
return uniqueErr
}
if !fresh {
continue
}
err = r.modeLine(vertex, triangle[(i+1)%3], normal, triangle[0].point, shader, pass)
}
if err != nil {
return err
}
}
if r.mode != U3DRenderSolidWireframe {
for i, section := range r.sections {
if section.intersection != nil {
if err := r.sectionIntersection(triangle, i); err != nil {
return err
}
}
}
}
return nil
}
// modeLine 对原始网格边应用剖切与视锥裁剪,再按像素深度绘制
// 入参: a 起点, b 终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
keep, err := u3dClipLine(&a, &b, section.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
}
}
return r.rasterLine(a, b, normal, origin, shader, pass)
}
// modePoint 绘制原始顶点,不在裁剪边界补充顶点
// 入参: vertex 顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPass) error {
if err := r.ctx.Err(); err != nil {
return err
}
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
distance := u3dPlaneDistance(section.plane, vertex.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D point clip distance")
}
if distance < 0 {
return nil
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
distance := u3dPlaneDistance(plane, vertex.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D point clip distance")
}
if distance < 0 {
return nil
}
}
q := 1.0
if r.camera.Perspective {
q = 1 / vertex.point[2]
}
x := math.Floor(r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX + vertex.point[0]*r.camera.ScaleX*q)
y := math.Floor(r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - vertex.point[1]*r.camera.ScaleY*q)
if !finite(x) || !finite(y) {
return fmt.Errorf("invalid U3D projected vertex")
}
if x < 0 || y < 0 || x >= float64(r.output.Rect.Dx()) || y >= float64(r.output.Rect.Dy()) {
return nil
}
pixel := int(y)*r.output.Rect.Dx() + int(x)
if vertex.point[2] > r.frame[pixel].depth {
return nil
}
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
return err
}
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil || !visible {
return err
}
r.order++
return r.fragment(pixel, vertex.point[2], vertex.color, shader.Blend)
}
// drawBounds 在模型局部空间计算包围盒,再应用实例变换,不以世界坐标轴重建包围盒
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、数值、预算或取消错误
func (r *Renderer) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
visible := false
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
shading, corners := mesh.primitive(i)
shaders := node.Shaders
if i >= len(mesh.Faces) {
shaders = node.LineShaders
}
if i >= len(mesh.Faces)+len(mesh.Lines) {
shaders = node.PointShaders
}
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(shading) >= uint64(len(shaders)) {
visible = visible || passIndex == 0
} else if !visible {
for _, name := range shaders[shading] {
if r.shaderPass(name)&(1<<passIndex) != 0 {
visible = true
break
}
}
}
for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return fmt.Errorf("invalid U3D bounds position index")
}
for j, value := range mesh.Positions[corner.Position] {
v := float64(value)
if !finite(v) {
return fmt.Errorf("invalid U3D bounds position")
}
minimum[j], maximum[j] = min(minimum[j], v), max(maximum[j], v)
}
}
}
if !visible || math.IsInf(minimum[0], 1) {
return nil
}
opacity := 1.0
if instance.opacity >= 0 {
opacity = instance.opacity
}
if r.opacity >= 0 {
opacity *= r.opacity
}
if r.mode == U3DRenderBoundingBox {
opacity = 0
}
var vertices [8]u3dVertex
for i := range vertices {
point := minimum
for j := range 3 {
if i&(1<<j) != 0 {
point[j] = maximum[j]
}
}
vertices[i].point = r.toCamera.point(instance.world.point(point))
for _, v := range vertices[i].point {
if !finite(v) {
return fmt.Errorf("invalid U3D transformed bounds")
}
}
vertices[i].color = [4]float64{r.faceColor[0], r.faceColor[1], r.faceColor[2], opacity}
}
shader := U3DShader{Blend: 0x606}
var edges [12][2]u3dVector
edgeCount := 0
for index, face := range [6][4]int{{0, 2, 3, 1}, {4, 5, 7, 6}, {0, 1, 5, 4}, {2, 6, 7, 3}, {0, 4, 6, 2}, {1, 3, 7, 5}} {
if index%2 == 1 && minimum[2-index/2] == maximum[2-index/2] {
continue
}
for i := 1; i < 3; i++ {
triangle := [3]u3dVertex{vertices[face[0]], vertices[face[i]], vertices[face[i+1]]}
r.order++
if err := r.sectionTriangle(triangle, shader, pass); err != nil {
return err
}
}
if r.mode == U3DRenderBoundingBoxFaces {
continue
}
normal := u3dTriangleNormal([3]u3dVector{vertices[face[0]].point, vertices[face[1]].point, vertices[face[2]].point})
for i, index := range face {
a, b := vertices[index], vertices[face[(i+1)%4]]
alpha := 1.0
if r.mode == U3DRenderBoundingBoxOutline {
alpha = opacity
duplicate := a.point == b.point
for _, edge := range edges[:edgeCount] {
if edge == ([2]u3dVector{a.point, b.point}) || edge == ([2]u3dVector{b.point, a.point}) {
duplicate = true
break
}
}
if duplicate {
continue
}
edges[edgeCount] = [2]u3dVector{a.point, b.point}
edgeCount++
}
a.color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], alpha}
b.color = a.color
if err := r.modeLine(a, b, normal, a.point, shader, pass); err != nil {
return err
}
}
}
return nil
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"math"
)
// U3DMotion 保存骨骼或节点动作轨道,解码不执行动画
type U3DMotion struct {
Name string
TimeInverseQuantization, RotationInverseQuantization float32
Tracks []U3DMotionTrack
}
// U3DMotionTrack 保存命名轨道及其关键帧,保持文件中的时间与帧次序
type U3DMotionTrack struct {
Name string
DisplacementInverseQuantization, ScaleInverseQuantization float32
Frames []U3DKeyFrame
}
// U3DKeyFrame 保存关键帧时间及相对父项的变换,Rotation按W、X、Y、Z排列
type U3DKeyFrame struct {
Time float32
Displacement, Scale [3]float32
Rotation [4]float32
}
// motion 读取ECMA-363第9.8.6节的动作资源,首尾帧保留未量化数值
// 入参: values 字段读取器, name 资源名
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) motion(values *u3dValues, name string) error {
r := u3dResourceReader{values: values, decoder: d}
defer r.release()
count := r.u32()
motion := U3DMotion{Name: name, TimeInverseQuantization: r.f32(), RotationInverseQuantization: r.f32()}
if err := r.err(); err != nil {
return err
}
if motion.TimeInverseQuantization < 0 || motion.RotationInverseQuantization < 0 {
return fmt.Errorf("invalid U3D motion quantization")
}
if err := d.reserve(uint64(count), 48); err != nil {
return err
}
motion.Tracks = make([]U3DMotionTrack, int(count))
for index := range motion.Tracks {
if err := d.ctx.Err(); err != nil {
return err
}
track := &motion.Tracks[index]
track.Name = r.text()
frames := r.u32()
track.DisplacementInverseQuantization, track.ScaleInverseQuantization = r.f32(), r.f32()
if err := r.err(); err != nil {
return err
}
if track.DisplacementInverseQuantization < 0 || track.ScaleInverseQuantization < 0 {
return fmt.Errorf("invalid U3D motion track quantization")
}
if err := d.reserve(uint64(frames), 44); err != nil {
return err
}
track.Frames = make([]U3DKeyFrame, int(frames))
for i := range track.Frames {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
frame := &track.Frames[i]
if i == 0 || i == len(track.Frames)-1 {
frame.Time = r.f32()
for component := range frame.Displacement {
frame.Displacement[component] = r.f32()
}
for component := range frame.Rotation {
frame.Rotation[component] = r.f32()
}
for component := range frame.Scale {
frame.Scale[component] = r.f32()
}
} else if err := r.motionFrame(frame, track.Frames[i-1], &motion, track); err != nil {
return err
}
if err := r.err(); err != nil {
return err
}
}
}
if err := r.done(); err != nil {
return err
}
d.model.Motions = append(d.model.Motions, motion)
return nil
}
// motionFrame 按前一帧预测时间、位移和缩放,并右乘旋转差分四元数
// 入参: frame 目标帧, previous 前一帧, motion 动作量化参数, track 轨道量化参数
// 返回: error 符号、数值或读取错误
func (r *u3dResourceReader) motionFrame(frame *U3DKeyFrame, previous U3DKeyFrame, motion *U3DMotion, track *U3DMotionTrack) error {
time, err := r.motionDifference([]float32{previous.Time}, motion.TimeInverseQuantization, 0)
if err != nil {
return err
}
frame.Time = time[0]
position, err := r.motionDifference(previous.Displacement[:], track.DisplacementInverseQuantization, 2)
if err != nil {
return err
}
copy(frame.Displacement[:], position[:])
signs := r.value(4, true)
if signs > 15 {
return fmt.Errorf("invalid U3D motion rotation signs")
}
var difference [4]float64
var squared float64
for component := 1; component < 4; component++ {
value := float64(r.value(5, false)) * float64(motion.RotationInverseQuantization)
if signs&(1<<component) != 0 {
value = -value
}
difference[component] = value
squared += value * value
}
if err := r.err(); err != nil {
return err
}
if squared > 1 {
return fmt.Errorf("invalid U3D motion rotation difference")
}
difference[0] = math.Sqrt(1 - squared)
if signs&1 != 0 {
difference[0] = -difference[0]
}
p := previous.Rotation
w, x, y, z := float64(p[0]), float64(p[1]), float64(p[2]), float64(p[3])
a, b, c, e := difference[0], difference[1], difference[2], difference[3]
frame.Rotation = [4]float32{
float32(w*a - x*b - y*c - z*e),
float32(w*b + x*a + y*e - z*c),
float32(w*c - x*e + y*a + z*b),
float32(w*e + x*c - y*b + z*a),
}
for _, value := range frame.Rotation {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D reconstructed rotation")
}
}
scale, err := r.motionDifference(previous.Scale[:], track.ScaleInverseQuantization, 6)
copy(frame.Scale[:], scale[:])
return err
}
// motionDifference 用共享幅值上下文恢复一至三个差分分量
// 入参: previous 前一帧分量, scale 逆量化系数, context 符号上下文,下一项为幅值上下文
// 返回: [3]float32 重建分量, error 符号、数值或读取错误
func (r *u3dResourceReader) motionDifference(previous []float32, scale float32, context int) (values [3]float32, err error) {
signs := r.value(context, true)
if signs >= 1<<len(previous) {
return values, fmt.Errorf("invalid U3D motion difference signs")
}
for index, prior := range previous {
difference := float64(r.value(context+1, false)) * float64(scale)
if signs&(1<<index) != 0 {
difference = -difference
}
values[index] = float32(float64(prior) + difference)
if !finite(float64(values[index])) {
return values, fmt.Errorf("invalid U3D reconstructed motion value")
}
}
return values, r.err()
}
+150
View File
@@ -0,0 +1,150 @@
// 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 threed
import (
"fmt"
"math"
)
// u3dOutline 保存单个模型实例的共享边索引,按插入顺序绘制
type u3dOutline struct {
index map[[2]u3dVector]int
edges []u3dOutlineEdge
}
// u3dOutlineEdge 保存正背面关系及正面法线角度区间,避免多面共享边的成对扫描
type u3dOutlineEdge struct {
a, b u3dVector
reference, tangent u3dVector
minimumNormal, maximumNormal u3dVector
visibleNormal u3dVector
minimum, maximum float64
front, back int
visible, frontVisible bool
}
// outlineFace 收集当前绘制轮次中的原始网格边,正面法线在同一半圆内取角度极值
// 入参: outline 共享边索引, world 世界坐标, vertices 相机顶点, front 是否正面, visible 是否绘制面, shaders 着色列表, passIndex 轮次
// 返回: error 数值、预算或取消错误
func (r *Renderer) outlineFace(outline *u3dOutline, world [3]u3dVector, vertices [3]u3dVertex, front, visible bool, shaders []string, passIndex int) error {
active := false
for i, name := range shaders {
if i%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if r.shaderPass(name)&(1<<passIndex) != 0 {
active = true
break
}
}
if !active {
return nil
}
normal := u3dTriangleNormal(world)
if normal == (u3dVector{}) {
return nil
}
cameraNormal := u3dTriangleNormal([3]u3dVector{vertices[0].point, vertices[1].point, vertices[2].point})
for i := range 3 {
j := (i + 1) % 3
a, b := world[i], world[j]
pa, pb := vertices[i].point, vertices[j].point
for c := range 3 {
if a[c] != b[c] {
if a[c] > b[c] {
a, b, pa, pb = b, a, pb, pa
}
break
}
}
key := [2]u3dVector{a, b}
index, exists := outline.index[key]
if !exists {
if err := r.Reserve(1, 1024); err != nil {
return err
}
index = len(outline.edges)
outline.edges = append(outline.edges, u3dOutlineEdge{a: pa, b: pb})
outline.index[key] = index
}
edge := &outline.edges[index]
if visible && !edge.visible {
edge.visibleNormal = cameraNormal
}
edge.visible = edge.visible || visible
if !front {
edge.back++
continue
}
edge.frontVisible = edge.frontVisible || visible
if edge.front == 0 {
edge.reference = normal
edge.tangent = b.sub(a).unit().cross(normal).unit()
edge.minimumNormal, edge.maximumNormal = cameraNormal, cameraNormal
} else {
angle := math.Atan2(normal.dot(edge.tangent), normal.dot(edge.reference))
if !finite(angle) {
return fmt.Errorf("invalid U3D silhouette angle")
}
if angle < edge.minimum {
edge.minimum, edge.minimumNormal = angle, cameraNormal
}
if angle > edge.maximum {
edge.maximum, edge.maximumNormal = angle, cameraNormal
}
}
edge.front++
}
return nil
}
// drawOutline 绘制外缘、正背面交界和达到夹角的共享边,沿用像素深度去除遮挡
// 入参: outline 共享边索引, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) drawOutline(outline *u3dOutline, pass U3DViewPass) error {
shader := U3DShader{Blend: 0x606}
color := [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
for i, edge := range outline.edges {
if i%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if !edge.visible {
continue
}
angle := (edge.maximum - edge.minimum) * 180 / math.Pi
if edge.front+edge.back != 1 && !(edge.front > 0 && edge.back > 0) && !(edge.front > 1 && angle >= math.Nextafter(r.creaseAngle, math.Inf(-1))) {
continue
}
a, b := u3dVertex{point: edge.a, color: color}, u3dVertex{point: edge.b, color: color}
normals := [2]u3dVector{edge.visibleNormal, edge.visibleNormal}
if edge.frontVisible {
normals = [2]u3dVector{edge.minimumNormal, edge.maximumNormal}
}
for i, normal := range normals {
if i == 1 && normal == normals[0] {
continue
}
if err := r.modeLine(a, b, normal, edge.a, shader, pass); err != nil {
return err
}
}
}
return nil
}
+329
View File
@@ -0,0 +1,329 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"cmp"
"fmt"
"math"
"slices"
)
// u3dAnimationState 保存一次绘制中动画求值的节点变换、骨骼姿态及对应权重
type u3dAnimationState struct {
matrix u3dMatrix
bones []U3DBoneState
weights *U3DBoneWeights
}
// u3dPlaybackKey 标识一条轨道在指定动作时间的采样,不包含节点绑定
type u3dPlaybackKey struct {
track *U3DMotionTrack
time float64
}
// u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果
type u3dPlaybackCache struct {
render *Renderer
bounds map[*U3DMotionTrack][2]float64
samples map[u3dPlaybackKey]U3DKeyFrame
bytes uint64
}
// limits 校验轨道时间并缓存范围,避免共享动作按节点数量重复扫描全部帧
// 入参: track 动作轨道
// 返回: [2]float64 首末时间, error 空轨道、数值、预算或取消错误
func (c *u3dPlaybackCache) limits(track *U3DMotionTrack) ([2]float64, error) {
if err := c.render.ctx.Err(); err != nil {
return [2]float64{}, err
}
if bounds, exists := c.bounds[track]; exists {
return bounds, nil
}
if len(track.Frames) == 0 {
return [2]float64{}, fmt.Errorf("empty U3D animation track")
}
bounds := [2]float64{math.Inf(1), math.Inf(-1)}
for i, frame := range track.Frames {
if i%256 == 0 {
if err := c.render.ctx.Err(); err != nil {
return bounds, err
}
}
if !finite(float64(frame.Time)) {
return bounds, fmt.Errorf("invalid U3D animation key frame time")
}
bounds[0], bounds[1] = min(bounds[0], float64(frame.Time)), max(bounds[1], float64(frame.Time))
}
if c.bounds == nil {
if err := c.render.Reserve(1, 1024); err != nil {
return bounds, err
}
c.bytes += 1024
c.bounds = make(map[*U3DMotionTrack][2]float64)
}
if err := c.render.Reserve(1, 96); err != nil {
return bounds, err
}
c.bytes += 96
c.bounds[track] = bounds
return bounds, nil
}
// sample 复用相同轨道和时间的姿态,缓存只在本次求值期间有效
// 入参: track 动作轨道, time 已换算的动作时间
// 返回: U3DKeyFrame 采样姿态, error 轨道、预算或取消错误
func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFrame, error) {
if err := c.render.ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
key := u3dPlaybackKey{track, time}
if frame, exists := c.samples[key]; exists {
return frame, nil
}
frame, err := track.Sample(c.render.ctx, time, false)
if err != nil {
return frame, err
}
if c.samples == nil {
if err := c.render.Reserve(1, 1024); err != nil {
return frame, err
}
c.bytes += 1024
c.samples = make(map[u3dPlaybackKey]U3DKeyFrame)
}
if err := c.render.Reserve(1, 128); err != nil {
return frame, err
}
c.bytes += 128
c.samples[key] = frame
return frame, nil
}
// prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟
// 入参: time 从初始时刻起算的秒数
// 返回: error 动作、骨架、修饰顺序、预算或取消错误
func (r *Renderer) prepareAnimations(time float64) error {
cache := u3dPlaybackCache{render: r}
defer func() { r.remaining += cache.bytes }()
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if r.connected != nil && !r.connected[i] {
continue
}
node := &r.model.Nodes[i]
mesh := r.nodeMesh(node)
groups := [2][]U3DAnimation{nil, node.Animations}
if node.Kind == "Model" && mesh != nil {
groups[0] = mesh.Animations
}
state := u3dAnimationState{matrix: u3dIdentity()}
used := false
for chain, group := range groups {
ordered := group
if len(group) > 1 {
if err := r.Reserve(uint64(len(group)), 48); err != nil {
return err
}
ordered = slices.Clone(group)
slices.SortFunc(ordered, func(a, b U3DAnimation) int { return cmp.Compare(a.Index, b.Index) })
}
for index, animation := range ordered {
if err := r.ctx.Err(); err != nil {
return err
}
if index != 0 && animation.Index == ordered[index-1].Index {
return fmt.Errorf("duplicate U3D animation chain index")
}
if len(animation.Motions) == 0 || animation.Motions[0].Name == "" {
continue
}
motion := &r.model.Motions[r.motionIndex[animation.Motions[0].Name]]
if len(motion.Tracks) == 0 {
continue
}
used = true
if err := r.sampleAnimation(&state, node, mesh, &animation, motion, chain, time, &cache); err != nil {
return err
}
}
if len(group) > 1 {
r.remaining += uint64(len(group)) * 48
}
}
if used {
if r.animated == nil {
if err := r.Reserve(1, 1024); err != nil {
return err
}
r.animated = make(map[string]u3dAnimationState)
}
if err := r.Reserve(1, 384); err != nil {
return err
}
r.animated[node.Name] = state
}
}
return r.ctx.Err()
}
// animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间
// 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存
// 返回: float64 动作采样时间, error 时间数值或取消错误
func (r *Renderer) animationTime(animation *U3DAnimation, tracks []U3DMotionTrack, bindings map[string]int, time float64, cache *u3dPlaybackCache) (float64, error) {
reference := animation.Motions[0]
if animation.Attributes&1 == 0 {
time = 0
}
time = time*float64(animation.TimeScale)*float64(reference.TimeScale) - float64(reference.TimeOffset)/1000
if !finite(time) {
return 0, fmt.Errorf("invalid U3D animation time")
}
minimum, maximum := math.Inf(1), math.Inf(-1)
for i, track := range tracks {
if err := r.ctx.Err(); err != nil {
return 0, err
}
if bindings != nil {
if index, exists := bindings[track.Name]; !exists || index != i {
continue
}
}
bounds, err := cache.limits(&tracks[i])
if err != nil {
return 0, err
}
minimum, maximum = min(minimum, bounds[0]), max(maximum, bounds[1])
}
if math.IsInf(minimum, 1) {
return 0, fmt.Errorf("empty U3D animation tracks")
}
if reference.Attributes&1 != 0 && maximum > minimum {
time = math.Mod(time-minimum, maximum-minimum)
if time < 0 {
time += maximum - minimum
}
time += minimum
}
return max(minimum, min(time, maximum)), r.ctx.Err()
}
// sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换
// 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存
// 返回: error 轨道、修饰顺序、预算或取消错误
func (r *Renderer) sampleAnimation(state *u3dAnimationState, node *U3DNode, mesh *U3DMesh, animation *U3DAnimation, motion *U3DMotion, chain int, time float64, cache *u3dPlaybackCache) error {
if animation.Attributes&4 != 0 {
if chain == 0 {
return fmt.Errorf("unsupported U3D resource single-track animation")
}
time, err := r.animationTime(animation, motion.Tracks[:1], nil, time, cache)
if err != nil {
return err
}
frame, err := cache.sample(&motion.Tracks[0], time)
if err != nil {
return err
}
matrix, err := frame.Matrix()
if err != nil {
return err
}
if animation.Attributes&2 == 0 {
state.matrix = state.matrix.multiply(u3dMatrix(matrix))
if !state.matrix.valid() {
return fmt.Errorf("invalid U3D animated node transform")
}
}
return nil
}
if node.Kind != "Model" || mesh == nil || len(mesh.Bones) == 0 {
return fmt.Errorf("missing U3D animation skeleton")
}
if state.weights != nil {
return fmt.Errorf("unsupported U3D successive skeletal animations")
}
if err := r.Reserve(uint64(len(mesh.Bones)), 96); err != nil {
return err
}
defer func() { r.remaining += uint64(len(mesh.Bones)) * 96 }()
tracks := make(map[string]int, len(mesh.Bones))
for i, bone := range mesh.Bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if _, exists := tracks[bone.Name]; exists {
return fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
tracks[bone.Name] = -1
}
bound := 0
for i, track := range motion.Tracks {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if index, exists := tracks[track.Name]; exists {
if index >= 0 {
return fmt.Errorf("ambiguous U3D animation track %q", track.Name)
}
tracks[track.Name] = i
bound++
}
}
if bound == 0 {
return nil
}
time, err := r.animationTime(animation, motion.Tracks, tracks, time, cache)
if err != nil {
return err
}
if err := r.Reserve(uint64(len(mesh.Bones)), 64); err != nil {
return err
}
state.bones = make([]U3DBoneState, 0, len(mesh.Bones))
for i, bone := range mesh.Bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if i == 0 && animation.Attributes&2 != 0 {
continue
}
if index := tracks[bone.Name]; index >= 0 {
frame, err := cache.sample(&motion.Tracks[index], time)
if err != nil {
return err
}
state.bones = append(state.bones, U3DBoneState{Name: bone.Name, Frame: frame})
}
}
if len(state.bones) == 0 {
r.remaining += uint64(cap(state.bones)) * 64
state.bones = nil
return nil
}
resourceEnd, nodeEnd := uint64(animation.Index), uint64(0)
if chain != 0 {
resourceEnd, nodeEnd = math.MaxUint64, uint64(animation.Index)
}
state.weights, err = r.skinWeightSet(mesh, node.BoneWeights, resourceEnd, nodeEnd)
return err
}
+71
View File
@@ -0,0 +1,71 @@
// 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 threed
import (
"fmt"
"math"
)
// pointNormalPrediction 按ECMA-363第9.6.2.2.4.4节计算点集球面法线预测
// 入参: mesh 属性数组, points 分裂位置的点
// 返回: u3dVector 预测方向, error 不支持的多方向预测或取消错误
func (d *u3dDecoder) pointNormalPrediction(mesh *U3DMesh, points []U3DPoint) (u3dVector, error) {
var directions [2]u3dVector
var counts [2]int
var previous [3]float32
var direction u3dVector
for i, point := range points {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return u3dVector{}, err
}
}
normal := mesh.Normals[point.Corners[0].Normal]
if i == 0 || normal != previous {
direction = (u3dVector{float64(normal[0]), float64(normal[1]), float64(normal[2])}).unit()
previous = normal
}
if counts[0] == 0 || direction == directions[0] {
directions[0] = direction
counts[0]++
} else if counts[1] == 0 || direction == directions[1] {
directions[1] = direction
counts[1]++
} else {
return u3dVector{}, fmt.Errorf("unsupported U3D point normal prediction with multiple directions")
}
}
if counts[1] == 0 {
return directions[0], nil
}
a, b := directions[0], directions[1]
if a == (u3dVector{}) || b == (u3dVector{}) {
return u3dVector{}, fmt.Errorf("unsupported U3D point normal prediction with zero direction")
}
dot := max(-1, min(1, a.dot(b)))
tangent := b.sub(u3dVector{a[0] * dot, a[1] * dot, a[2] * dot})
length := math.Sqrt(tangent.dot(tangent))
if length == 0 {
return u3dVector{}, fmt.Errorf("unsupported U3D antipodal point normal prediction")
}
angle := math.Atan2(length, dot) * (float64(counts[1]) / float64(len(points)))
sine, cosine := math.Sincos(angle)
var result u3dVector
for i := range result {
result[i] = a[i]*cosine + tangent[i]/length*sine
}
return result.unit(), nil
}
+564
View File
@@ -0,0 +1,564 @@
// 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 threed
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为空时沿用模型灯光,非空时替换全部模型灯光
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
// 纹理合成后先应用节点透明度,再应用Opacity
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
// Time以秒指定动画采样时刻,零值为初始时刻,不随实际时钟推进
// ResolveTexture提供外部纹理,空值不访问外部资源;回调自身的内存与超时由调用方控制
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
type U3DRenderOptions struct {
Width, Height int
View string
Instance int
Camera *U3DCamera
Background *[3]float64
Lighting *U3DLighting
Opacity *float64
Mode U3DRenderMode
AuxiliaryColor *[3]float64
FaceColor *[3]float64
CreaseAngle *float64
Nodes []U3DNodeState
Time float64
Sections []U3DCrossSection
ResolveTexture U3DTextureResolver
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
}
// Renderer 保存一次绘制的有限预算,不修改调用方场景
type Renderer struct {
ctx context.Context
model *U3DModel
remaining uint64
frame []u3dPixel
output *image.RGBA
viewport [4]float64
camera U3DCamera
toCamera u3dMatrix
scene []u3dInstance
byNode [][]int
connected []bool
members []bool
meshes, shaders, materials, lightIndex map[string]int
textureIndex map[string]int
viewIndex, motionIndex map[string]int
textures map[string]*image.NRGBA64
textureSets map[[8]*U3DTextureLayer]*u3dTextureSet
resolveTexture U3DTextureResolver
view *U3DView
lights []u3dRenderLight
lighting *U3DLighting
opacity float64
mode U3DRenderMode
creaseAngle float64
auxiliaryColor, faceColor [3]float64
depthOnly bool
primitives map[u3dPrimitiveKey]struct{}
texturedPrimitives map[u3dTexturedPrimitiveKey]struct{}
states map[string]U3DNodeState
posed map[string]*U3DMesh
animated map[string]u3dAnimationState
sections []u3dSection
sectionScratch [2][]u3dVertex
chunks [][]u3dFragment
fragments int
scratch []int
order uint64
}
// RenderU3D 绘制几何及视图纹理层,返回指定时刻的不透明图像,不执行脚本
// 可通过节点覆盖指定动作轨道与采样时间,逐次生成独立姿态,不修改原模型
// 显式骨骼姿态按权重变换位置和法线,暂不生成骨骼链接或推导缺失权重
// 动画修饰器支持单动作的节点和骨骼求值,尚未实现的动作衔接返回明确错误
// 几何纹理支持模型坐标与程序化映射,参考形状约定见U3DTextureLayer
// 纹理按需解码并复用,不自动访问外部图像
// 采用逐顶点光照及逐像素深度混合,空名称引用标准默认资源
// 缺失几何、材质和着色器采用默认资源,缺失的非空光源引用不产生光照
// 缺失纹理返回错误,空纹理名称使用标准默认图像
// 仅绘制默认根的后代,非空父节点引用缺失时不展开该父路径
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
r, err := NewRenderer(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)
}
// NewRenderer 建立有预算限制的场景实例与资源索引
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组
// 返回: *Renderer 绘制状态, error 参数、场景、预算或取消错误
func NewRenderer(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*Renderer, error) {
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 || !finite(options.Time) {
return nil, fmt.Errorf("invalid U3D render options")
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = DefaultBytes
}
r := &Renderer{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45, resolveTexture: options.ResolveTexture}
if options.Mode > U3DRenderShadedIllustration {
return nil, fmt.Errorf("unsupported U3D render mode")
}
if options.CreaseAngle != nil {
if !finite(*options.CreaseAngle) {
return nil, fmt.Errorf("invalid U3D crease angle")
}
r.creaseAngle = *options.CreaseAngle
}
if options.Opacity != nil {
value := *options.Opacity
if !finite(value) || value < 0 || value > 1 {
return nil, fmt.Errorf("invalid U3D render opacity")
}
r.opacity = value
}
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
}
if err := r.resources(); err != nil {
return nil, err
}
order, index, err := r.sceneGraph()
if err != nil {
return nil, err
}
if err := r.checkAnimations(); err != nil {
return nil, err
}
maxInstances := options.MaxInstances
if maxInstances == 0 {
maxInstances = 65536
}
groups := append([][]U3DNodeState{options.Nodes}, states...)
if err := r.nodeStates(groups...); err != nil {
return nil, err
}
if err := r.prepareAnimations(options.Time); err != nil {
return nil, err
}
if err := r.instances(order, index, maxInstances); err != nil {
return nil, err
}
if err := r.poseNodes(); err != nil {
return nil, err
}
if err := r.Reserve(uint64(len(r.scene)), 1); err != nil {
return nil, err
}
r.members = make([]bool, len(r.scene))
return r, nil
}
// Render 在已选择的相机和轮次下生成图像
// 入参: options 输出与灯光背景, passes 绘制轮次
// 返回: *image.RGBA 图像, error 参数、绘制、预算或取消错误
func (r *Renderer) 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.faceColor = background
for _, item := range []struct {
source *[3]float64
target *[3]float64
}{{options.AuxiliaryColor, &r.auxiliaryColor}, {options.FaceColor, &r.faceColor}} {
if item.source != nil {
for i, value := range *item.source {
if !finite(value) || value < 0 || value > 1 {
return nil, fmt.Errorf("invalid U3D mode color")
}
item.target[i] = value
}
}
}
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
}
if r.view != nil {
if err := r.viewLayers(r.view.Backdrops); err != nil {
return nil, err
}
}
for passIndex, pass := range passes {
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
return nil, err
}
}
if r.view != nil {
if err := r.viewLayers(r.view.Overlays); err != nil {
return nil, err
}
}
for i, pixel := range r.frame {
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 *Renderer) 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 *Renderer) SelectView(options U3DRenderOptions) ([]U3DViewPass, error) {
r.view = nil
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 view.Attributes & ^uint32(7) != 0 {
return nil, fmt.Errorf("invalid U3D view attributes")
}
r.view = view
if i, exists := r.viewIndex[node.Resource]; exists {
passes = r.model.Views[i].Passes
}
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 *Renderer) resources() error {
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) + uint64(len(r.model.Textures)) + uint64(len(r.model.Views)) + uint64(len(r.model.Motions))
if err := r.Reserve(count, 96); err != nil {
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))
r.textureIndex = make(map[string]int, len(r.model.Textures))
r.viewIndex = make(map[string]int, len(r.model.Views))
r.motionIndex = make(map[string]int, len(r.model.Motions))
add := func(index map[string]int, name string, i int) error {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
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
}
}
for i, v := range r.model.Textures {
if err := add(r.textureIndex, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Views {
if err := add(r.viewIndex, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Motions {
if err := add(r.motionIndex, v.Name, i); err != nil {
return err
}
}
return nil
}
// renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度
// 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮
// 返回: error 场景、预算或取消错误
func (r *Renderer) 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 r.mode.shaded() {
if err := r.collectLights(); err != nil {
return err
}
}
if r.mode == U3DRenderHiddenWireframe {
r.depthOnly = true
if err := r.drawScene(pass, passIndex); err != nil {
return err
}
r.depthOnly = false
}
if err := r.drawScene(pass, passIndex); err != nil {
return err
}
if last {
if err := r.drawSectionPlanes(); err != nil {
return err
}
}
return r.resolveFragments()
}
// drawScene 绘制当前轮次中的可见模型,不重置深度及片元缓存
// 入参: pass 绘制轮次, passIndex 轮次索引
// 返回: error 场景、资源或取消错误
func (r *Renderer) drawScene(pass U3DViewPass, passIndex int) error {
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 := r.nodeMesh(node)
if mesh == nil {
continue
}
if err := r.drawMesh(instance, node, mesh, pass, passIndex); err != nil {
return err
}
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"math"
"unicode/utf8"
)
// u3dResourceReader 读取骨骼权重与动作资源中交错的普通字段和压缩字段
type u3dResourceReader struct {
values *u3dValues
decoder *u3dDecoder
bits *u3dBits
hist [8]u3dHistogram
charged uint64
}
// value 读取指定上下文的压缩字段,首次使用时初始化算术区间
// 入参: context 压缩上下文, small 是否为8位字段
// 返回: uint32 字段值
func (r *u3dResourceReader) value(context int, small bool) uint32 {
if r.err() != nil {
return 0
}
if r.decoder.plain {
if small {
return uint32(r.values.u8())
}
return r.values.u32()
}
if r.bits == nil {
r.bits = newU3DBits(r.values.data[r.values.pos:])
r.bits.reserve = func(count, size uint64) error {
if err := r.decoder.reserve(count, size); err != nil {
return err
}
r.charged += count * size
return nil
}
}
return r.bits.dynamicValue(&r.hist[context], small)
}
// u32 读取普通32位字段,压缩开始后沿用当前算术区间
// 返回: uint32 字段值
func (r *u3dResourceReader) u32() uint32 {
if r.bits != nil {
return r.bits.u32()
}
return r.values.u32()
}
// f32 读取有限单精度数值
// 返回: float32 字段值
func (r *u3dResourceReader) f32() float32 {
value := math.Float32frombits(r.u32())
if !finite(float64(value)) && r.err() == nil {
r.values.err = fmt.Errorf("invalid U3D floating-point value")
}
return value
}
// text 读取UTF-8名称,压缩开始后逐字节解码而不重置区间
// 返回: string 名称
func (r *u3dResourceReader) text() string {
if r.err() != nil {
return ""
}
if r.bits == nil {
return r.values.text()
}
length := int(r.bits.byteValue()) | int(r.bits.byteValue())<<8
if r.err() != nil {
return ""
}
if err := r.decoder.reserve(uint64(length), 2); err != nil {
r.values.err = err
return ""
}
data := make([]byte, length)
for index := range data {
if index%1024 == 0 {
if err := r.decoder.ctx.Err(); err != nil {
r.values.err = err
return ""
}
}
data[index] = r.bits.byteValue()
}
if r.err() == nil && !utf8.Valid(data) {
r.values.err = fmt.Errorf("invalid U3D UTF-8 string")
}
return string(data)
}
// err 返回字段或算术解码错误
// 返回: error 读取错误
func (r *u3dResourceReader) err() error {
if r.values.err != nil {
return r.values.err
}
if r.bits != nil {
return r.bits.err
}
return nil
}
// done 校验末尾冲刷字段和块边界
// 返回: error 读取或多余数据错误
func (r *u3dResourceReader) done() error {
if err := r.err(); err != nil {
return err
}
if r.bits != nil {
if err := r.bits.finish(); err != nil {
return err
}
r.values.pos = len(r.values.data)
}
return r.values.done()
}
// release 归还当前块压缩上下文占用的临时预算
func (r *u3dResourceReader) release() {
r.decoder.remaining += r.charged
r.charged = 0
}
+215
View File
@@ -0,0 +1,215 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"context"
"fmt"
"math"
)
// U3DTrackState 指定动作资源、轨道和采样时间,Time与关键帧使用相同单位
// Loop将时间循环到首末帧之间,未启用时停留在端点,不自动解释动画修饰器的时间参数
type U3DTrackState struct {
Motion, Track string
Time float64
Loop bool
}
// Sample 按时间采样轨道,不修改原始帧或按帧数量分配临时数组
// 位移和缩放采用线性插值,旋转归一化后采用最短弧球面插值
// 允许帧乱序,同一时刻取文件中最后一帧;循环区间不包含末端,单帧轨道始终返回该帧
// 入参: ctx 取消上下文, time 与关键帧同单位的时间, loop 是否循环
// 返回: U3DKeyFrame 采样姿态, error 空轨道、数值或取消错误
func (track U3DMotionTrack) Sample(ctx context.Context, time float64, loop bool) (U3DKeyFrame, error) {
if ctx == nil || !finite(time) || len(track.Frames) == 0 {
return U3DKeyFrame{}, fmt.Errorf("invalid U3D motion sample")
}
first, last := 0, 0
for index, frame := range track.Frames {
if index%256 == 0 {
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
}
if !finite(float64(frame.Time)) {
return U3DKeyFrame{}, fmt.Errorf("invalid U3D key frame time")
}
if frame.Time <= track.Frames[first].Time {
first = index
}
if frame.Time >= track.Frames[last].Time {
last = index
}
}
minimum, maximum := float64(track.Frames[first].Time), float64(track.Frames[last].Time)
if loop && maximum > minimum {
time = math.Mod(time-minimum, maximum-minimum)
if time < 0 {
time += maximum - minimum
}
time += minimum
} else {
time = max(minimum, min(time, maximum))
}
lower, upper := first, last
for index, frame := range track.Frames {
if index%256 == 0 {
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
}
value := float64(frame.Time)
if value <= time && frame.Time >= track.Frames[lower].Time {
lower = index
}
if value > time && frame.Time <= track.Frames[upper].Time {
upper = index
}
}
a, b := track.Frames[lower], track.Frames[upper]
if float64(a.Time) == time {
b = a
}
qa, err := a.quaternion()
if err != nil {
return U3DKeyFrame{}, err
}
qb, err := b.quaternion()
if err != nil {
return U3DKeyFrame{}, err
}
fraction := float64(0)
if b.Time != a.Time {
fraction = (time - float64(a.Time)) / (float64(b.Time) - float64(a.Time))
}
result := U3DKeyFrame{Time: float32(time)}
for i := range result.Displacement {
result.Displacement[i] = float32(float64(a.Displacement[i])*(1-fraction) + float64(b.Displacement[i])*fraction)
result.Scale[i] = float32(float64(a.Scale[i])*(1-fraction) + float64(b.Scale[i])*fraction)
}
var dot float64
for i := range qa {
dot += qa[i] * qb[i]
}
if dot < 0 {
dot = -dot
for i := range qb {
qb[i] = -qb[i]
}
}
left, right := 1-fraction, fraction
if dot < .9995 {
angle := math.Acos(min(1, dot))
divisor := math.Sin(angle)
left, right = math.Sin(left*angle)/divisor, math.Sin(right*angle)/divisor
}
var rotation [4]float64
var length float64
for i := range rotation {
rotation[i] = left*qa[i] + right*qb[i]
length += rotation[i] * rotation[i]
}
length = math.Sqrt(length)
for i := range result.Rotation {
result.Rotation[i] = float32(rotation[i] / length)
}
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
return result, nil
}
// quaternion 校验姿态分量并归一化W、X、Y、Z旋转四元数
// 返回: [4]float64 单位四元数, error 非有限或零旋转错误
func (frame U3DKeyFrame) quaternion() ([4]float64, error) {
var rotation [4]float64
for _, values := range [][3]float32{frame.Displacement, frame.Scale} {
for _, value := range values {
if !finite(float64(value)) {
return rotation, fmt.Errorf("invalid U3D key frame transform")
}
}
}
var length float64
for i, value := range frame.Rotation {
rotation[i] = float64(value)
length += rotation[i] * rotation[i]
}
if !finite(length) || length == 0 {
return rotation, fmt.Errorf("invalid U3D key frame rotation")
}
length = math.Sqrt(length)
for i := range rotation {
rotation[i] /= length
}
return rotation, nil
}
// Matrix 将关键帧位移、旋转和缩放组合成相对父项的列式仿射矩阵,不使用Time
// 返回: [16]float64 先缩放、再旋转、后平移的变换, error 姿态数值错误
func (frame U3DKeyFrame) Matrix() ([16]float64, error) {
q, err := frame.quaternion()
if err != nil {
return [16]float64{}, err
}
w, x, y, z := q[0], q[1], q[2], q[3]
matrix := [16]float64{
1 - 2*(y*y+z*z), 2 * (x*y + w*z), 2 * (x*z - w*y), 0,
2 * (x*y - w*z), 1 - 2*(x*x+z*z), 2 * (y*z + w*x), 0,
2 * (x*z + w*y), 2 * (y*z - w*x), 1 - 2*(x*x+y*y), 0,
float64(frame.Displacement[0]), float64(frame.Displacement[1]), float64(frame.Displacement[2]), 1,
}
for axis, scale := range frame.Scale {
for component := range 3 {
matrix[axis*4+component] *= float64(scale)
}
}
return matrix, nil
}
// trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系
// 入参: state 动作轨道状态
// 返回: [16]float64 节点变换, error 引用、数值或取消错误
func (r *Renderer) trackMatrix(state U3DTrackState) ([16]float64, error) {
var track *U3DMotionTrack
if err := r.ctx.Err(); err != nil {
return [16]float64{}, err
}
if index, exists := r.motionIndex[state.Motion]; exists {
motion := &r.model.Motions[index]
for i := range motion.Tracks {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return [16]float64{}, err
}
}
if motion.Tracks[i].Name == state.Track {
if track != nil {
return [16]float64{}, fmt.Errorf("ambiguous U3D motion track %q", state.Track)
}
track = &motion.Tracks[i]
}
}
}
if track == nil {
return [16]float64{}, fmt.Errorf("missing U3D motion track %q in %q", state.Track, state.Motion)
}
frame, err := track.Sample(r.ctx, state.Time, state.Loop)
if err != nil {
return [16]float64{}, err
}
return frame.Matrix()
}
+341
View File
@@ -0,0 +1,341 @@
// 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 threed
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()
}
// sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景
// 返回: []int 拓扑顺序, map[string]int 节点索引, error 结构、预算或取消错误
func (r *Renderer) sceneGraph() ([]int, map[string]int, error) {
nodes := r.model.Nodes
if err := r.Reserve(uint64(len(nodes)), 161); err != nil {
return nil, nil, 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 nil, nil, err
}
}
if _, exists := index[node.Name]; exists || node.Name == "" {
return nil, nil, fmt.Errorf("invalid U3D render node name")
}
switch node.Kind {
case "Group", "Model", "Light", "View":
default:
return nil, nil, 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))
r.connected = make([]bool, len(nodes))
for i, node := range nodes {
if err := r.Reserve(uint64(len(node.Parents)), 16); err != nil {
return nil, nil, err
}
for k, parent := range node.Parents {
if k%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, nil, 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 nil, nil, err
}
}
if n == 0 {
queue = append(queue, i)
}
}
for cursor := 0; cursor < len(queue); cursor++ {
if err := r.ctx.Err(); err != nil {
return nil, nil, err
}
i := queue[cursor]
for j, parent := range nodes[i].Parents {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, nil, err
}
}
if parent.Name == "" {
r.connected[i] = true
break
}
if p, exists := index[parent.Name]; exists && r.connected[p] {
r.connected[i] = true
break
}
}
for childIndex, child := range children[i] {
if childIndex%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, nil, err
}
}
counts[child]--
if counts[child] == 0 {
queue = append(queue, child)
}
}
}
if len(queue) != len(nodes) {
return nil, nil, fmt.Errorf("cyclic U3D node parents")
}
return queue, index, nil
}
// instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根
// 入参: order 拓扑顺序, index 节点索引, limit 最大实例数
// 返回: error 变换、预算、实例上限或取消错误
func (r *Renderer) instances(order []int, index map[string]int, limit int) error {
nodes := r.model.Nodes
for _, i := range order {
if err := r.ctx.Err(); err != nil {
return err
}
if !r.connected[i] {
continue
}
state := r.states[nodes[i].Name]
for parentIndex, parent := range nodes[i].Parents {
if parentIndex%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
parents := []int{-1}
if parent.Name != "" {
j, exists := index[parent.Name]
if !exists || !r.connected[j] {
continue
}
parents = r.byNode[j]
}
var local u3dMatrix
for k, v := range parent.Transform {
local[k] = float64(v)
}
if animation, exists := r.animated[nodes[i].Name]; exists {
local = local.multiply(animation.matrix)
}
if state.Matrix != nil {
local = *state.Matrix
}
if !local.valid() {
return fmt.Errorf("unsupported U3D non-affine node transform")
}
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})
}
}
}
return r.ctx.Err()
}
+283
View File
@@ -0,0 +1,283 @@
// 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 threed
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 *Renderer) PrepareSections(sections []U3DCrossSection) error {
if len(sections) == 0 {
return nil
}
if err := r.Reserve(uint64(len(sections)), 1152); err != nil {
return err
}
if err := r.Reserve(uint64(len(sections))+3, 512); 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 *Renderer) 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]
mesh := r.nodeMesh(&node)
if node.Kind != "Model" || mesh == nil {
continue
}
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(i)
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
for _, corner := range 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 *Renderer) 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
}
}
if r.depthOnly {
return nil
}
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 *Renderer) 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
}
+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 threed
import (
"fmt"
"image"
"math"
)
// U3DTextureLayer 保存单个着色器纹理通道,矩阵按列排列
// Blend依次以0至3表示相乘、相加、替换及线性混合,BlendSource为1时使用BlendConstant
// Mode依次以0至4表示模型坐标、平面、圆柱、球面及反射映射
// Alpha控制是否使用纹理透明度,Repeat低两位分别启用横向及纵向平铺
// 平面映射直接取参考位置的XY;圆柱以Z为纵坐标,圆柱与球面的经线从正X轴朝正Y轴递增
// 球面纬度从正Z极到负Z极映射为0至1;反射映射使用相机空间球面环境图,图像纵坐标向下
type U3DTextureLayer struct {
Texture string
Intensity, BlendConstant float32
Blend, BlendSource uint8
Mode, Repeat uint8
Alpha bool
Transform, WrapTransform [16]float32
}
// u3dTextureSet 保存一次绘制内共用的有序纹理层
type u3dTextureSet struct {
layers [8]*U3DTextureLayer
images [8]*image.NRGBA64
wrap [8]*[16]float64
normal u3dMatrix
mask uint8
reflection, perspective bool
}
// shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码
// 入参: shader 着色器
// 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误
func (r *Renderer) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
if shader.Textures == ([8]*U3DTextureLayer{}) {
return nil, nil
}
if err := r.ctx.Err(); err != nil {
return nil, err
}
if cached := r.textureSets[shader.Textures]; cached != nil {
return cached, nil
}
if err := r.Reserve(1, 512); err != nil {
return nil, err
}
set := &u3dTextureSet{layers: shader.Textures}
for i, layer := range set.layers {
if layer == nil {
continue
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 || !finite(float64(layer.Intensity)) || !finite(float64(layer.BlendConstant)) {
return nil, fmt.Errorf("invalid U3D texture layer")
}
for _, matrix := range [2][16]float32{layer.Transform, layer.WrapTransform} {
for _, value := range matrix {
if !finite(float64(value)) {
return nil, fmt.Errorf("invalid U3D texture transform")
}
}
}
if layer.Mode >= 1 && layer.Mode <= 3 {
if err := r.Reserve(1, 256); err != nil {
return nil, err
}
inverse, err := u3dTextureInverse(layer.WrapTransform)
if err != nil {
return nil, err
}
set.wrap[i] = &inverse
}
if layer.Mode == 4 && !set.reflection {
var ok bool
set.normal, ok = r.toCamera.inverse()
if !ok {
return nil, fmt.Errorf("invalid U3D reflection camera")
}
set.reflection, set.perspective = true, r.camera.Perspective
}
img, err := r.texture(layer.Texture)
if err != nil {
return nil, err
}
set.images[i] = img
set.mask |= 1 << i
}
if r.textureSets == nil {
r.textureSets = make(map[[8]*U3DTextureLayer]*u3dTextureSet)
}
r.textureSets[shader.Textures] = set
return set, nil
}
// coordinates 保存模型纹理坐标或程序化映射的插值输入,避免经线两侧直接插值
// 入参: vertex 目标顶点, mesh 几何资源, shading 属性布局, corner 属性索引, normal 世界法线
// 返回: error 坐标、索引或维度错误
func (s *u3dTextureSet) coordinates(vertex *u3dVertex, mesh *U3DMesh, shading U3DShading, corner U3DCorner, normal u3dVector) error {
if vertex.textureMask != 0 {
clear(vertex.texture[:])
}
vertex.textureMask = 0
if s == nil {
return nil
}
vertex.textureMask = s.mask
for i, layer := range s.layers {
if layer == nil {
continue
}
if layer.Mode != 0 {
if layer.Mode == 4 {
vertex.texture[i] = s.normal.normal(normal)
if vertex.texture[i] == ([3]float64{}) {
return fmt.Errorf("invalid U3D reflection normal")
}
} else {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return fmt.Errorf("invalid U3D texture position index")
}
for c, value := range mesh.Positions[corner.Position] {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D texture position")
}
vertex.texture[i][c] = float64(value)
}
}
continue
}
if i >= len(shading.TextureDimensions) || uint64(corner.Texture[i]) >= uint64(len(mesh.TextureCoordinates)) {
return fmt.Errorf("invalid U3D texture coordinate index")
}
dimension := shading.TextureDimensions[i]
if dimension < 1 || dimension > 4 {
return fmt.Errorf("invalid U3D texture coordinate dimensions")
}
value := [4]float64{0, 0, 0, 1}
for component := range dimension {
v := float64(mesh.TextureCoordinates[corner.Texture[i]][component])
if !finite(v) {
return fmt.Errorf("invalid U3D texture coordinate")
}
value[component] = v
}
for component, row := range [3]int{0, 1, 3} {
total := 0.0
for col, v := range value {
total += float64(layer.Transform[col*4+row]) * v
}
if !finite(total) {
return fmt.Errorf("invalid U3D transformed texture coordinate")
}
vertex.texture[i][component] = total
}
}
return nil
}
// textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度
// 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度
// 返回: error 纹理映射或混合错误
func (r *Renderer) textureColor(textures *u3dTextureSet, color *[4]float64, coordinates *[8][3]float64, position u3dVector, override bool) error {
if textures == nil {
return nil
}
opacity := color[3]
if err := textures.color(color, coordinates, position); err != nil {
return err
}
if override {
color[3] = opacity
}
if r.opacity >= 0 {
color[3] *= r.opacity
}
return nil
}
// color 按通道顺序合成纹理,在透明度覆盖、透明测试和雾效之前执行
// 入参: color 未预乘底色, coordinates 插值后的纹理输入, position 相机空间位置
// 返回: error 无效投影坐标或混合结果
func (s *u3dTextureSet) color(color *[4]float64, coordinates *[8][3]float64, position u3dVector) error {
if s == nil {
return nil
}
for i, layer := range s.layers {
if layer == nil {
continue
}
uv := coordinates[i]
if layer.Mode != 0 {
var err error
uv, err = s.generated(i, uv, position)
if err != nil {
return err
}
}
u, v := uv[0]/uv[2], uv[1]/uv[2]
if !finite(u) || !finite(v) {
return fmt.Errorf("invalid U3D projected texture coordinate")
}
sample := u3dShaderSample(s.images[i], u, v, layer.Repeat, layer.Alpha)
for c := range 3 {
sample[c] *= float64(layer.Intensity)
}
weight := sample[3]
if layer.BlendSource == 1 {
weight = min(1, max(0, float64(layer.BlendConstant)))
}
for c := range color {
switch layer.Blend {
case 0:
color[c] *= sample[c]
case 1:
color[c] += sample[c]
case 2:
color[c] = sample[c]
case 3:
color[c] = sample[c]*weight + color[c]*(1-weight)
}
if !finite(color[c]) {
return fmt.Errorf("invalid U3D texture blend result")
}
color[c] = min(1, max(0, color[c]))
}
}
return nil
}
// u3dShaderSample 按归一化坐标采样纹理,平铺时跨边界插值,未平铺时钳制边缘
// 全透明采样保留原始RGB,供相加、固定权重混合及透明度覆盖使用
// 入参: texture 纹理, u 横坐标, v 纵坐标, repeat 平铺位图, alpha 是否使用透明度
// 返回: [4]float64 未预乘颜色及透明度
func u3dShaderSample(texture *image.NRGBA64, u, v float64, repeat uint8, alpha bool) [4]float64 {
if repeat&1 != 0 {
u -= math.Floor(u)
} else {
u = min(1, max(0, u))
}
if repeat&2 != 0 {
v -= math.Floor(v)
} else {
v = min(1, max(0, v))
}
w, h := texture.Rect.Dx(), texture.Rect.Dy()
x, y := u*float64(w)-.5, v*float64(h)-.5
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
fx, fy := x-float64(x0), y-float64(y0)
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
var result [4]float64
var transparent [3]float64
for row := range 2 {
py := min(h-1, max(0, y0+row))
if repeat&2 != 0 {
py = (y0 + row + h) % h
}
for col := range 2 {
px := min(w-1, max(0, x0+col))
if repeat&1 != 0 {
px = (x0 + col + w) % w
}
c := texture.NRGBA64At(px, py)
a := 1.0
if alpha {
a = float64(c.A) / 65535
}
weight := wx[col] * wy[row]
if a == 0 {
transparent[0] += float64(c.R) / 65535 * weight
transparent[1] += float64(c.G) / 65535 * weight
transparent[2] += float64(c.B) / 65535 * weight
}
result[0] += float64(c.R) / 65535 * a * weight
result[1] += float64(c.G) / 65535 * a * weight
result[2] += float64(c.B) / 65535 * a * weight
result[3] += a * weight
}
}
if result[3] != 0 {
for c := range 3 {
result[c] /= result[3]
}
} else {
copy(result[:3], transparent[:])
}
return result
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"io"
)
// U3DBone 保存相对父骨骼的初始位姿,Orientation按W、X、Y、Z排列
// Attributes标志决定链接、关节字段是否存在,以及各轴旋转约束是否启用和限幅
// RotationConstraints按X、Y、Z轴排列,每项依次为上限、下限;未指定骨骼姿态时不变形
type U3DBone struct {
Name, Parent string
Attributes uint32
Length float32
Displacement [3]float32
Orientation [4]float32
LinkCount uint32
LinkLength float32
StartJoint, EndJoint U3DBoneJoint
RotationConstraints [3][2]float32
}
// U3DBoneJoint 保存骨骼局部坐标系内关节椭圆的中心偏移和两轴尺寸
type U3DBoneJoint struct {
Center, Scale [2]float32
}
// skeleton 按ECMA-363第9.6.1.1.6节读取骨架,三角网格、点集和线集共用
// 入参: r 字段读取器
// 返回: []U3DBone 按父项在前排列的骨骼, error 结构、预算或取消错误
func (d *u3dDecoder) skeleton(r *u3dValues) ([]U3DBone, error) {
count := r.u32()
if r.err != nil {
return nil, r.err
}
if count == 0 {
return nil, nil
}
if uint64(count) > uint64(len(r.data)-r.pos)/64 {
return nil, io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 256); err != nil {
return nil, err
}
bones := make([]U3DBone, int(count))
names := make(map[string]bool, int(count))
for index := range bones {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return nil, err
}
}
bone := &bones[index]
bone.Name, bone.Parent = r.text(), r.text()
bone.Attributes, bone.Length = r.u32(), r.f32()
for component := range bone.Displacement {
bone.Displacement[component] = r.f32()
}
for component := range bone.Orientation {
bone.Orientation[component] = r.f32()
}
if bone.Attributes&1 != 0 {
bone.LinkCount, bone.LinkLength = r.u32(), r.f32()
}
if bone.Attributes&2 != 0 {
for _, joint := range []*U3DBoneJoint{&bone.StartJoint, &bone.EndJoint} {
joint.Center = [2]float32{r.f32(), r.f32()}
joint.Scale = [2]float32{r.f32(), r.f32()}
}
}
for axis := range bone.RotationConstraints {
bone.RotationConstraints[axis] = [2]float32{r.f32(), r.f32()}
}
if r.err != nil {
return nil, r.err
}
if bone.Attributes & ^uint32(255) != 0 {
return nil, fmt.Errorf("invalid U3D bone attributes")
}
if names[bone.Name] {
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
if index == 0 && bone.Parent != "" || index != 0 && !names[bone.Parent] {
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
}
names[bone.Name] = true
}
return bones, nil
}
+505
View File
@@ -0,0 +1,505 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"math"
"slices"
)
// U3DBoneState 按骨骼名称指定相对父骨骼末端的姿态,根骨骼相对模型原点
// Frame可由动作轨道采样得到,Time不参与变形;单位缩放需显式设为[3]float32{1, 1, 1}
// 未覆盖项沿用文件或动画求值后的局部姿态,重复名称使用最后一项,不施加逆运动学约束
type U3DBoneState struct {
Name string
Frame U3DKeyFrame
}
// u3dSkinBone 保存初始姿态到当前姿态的变换及其逆矩阵
type u3dSkinBone struct {
matrix, inverse u3dMatrix
invertible bool
}
// u3dSkinPose 保存同一几何最近求值的姿态和权重,仅用于本次绘制
type u3dSkinPose struct {
weights *U3DBoneWeights
states []U3DBoneState
mesh *U3DMesh
}
// nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源
// 入参: node 模型节点
// 返回: *U3DMesh 几何,资源缺失时为空
func (r *Renderer) nodeMesh(node *U3DNode) *U3DMesh {
if mesh := r.posed[node.Name]; mesh != nil {
return mesh
}
if index, exists := r.meshes[node.Resource]; exists {
return &r.model.Meshes[index]
}
return nil
}
// poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何
// 返回: error 骨架、权重、预算或取消错误
func (r *Renderer) poseNodes() error {
if len(r.states) == 0 && len(r.animated) == 0 {
return r.ctx.Err()
}
var cache map[*U3DMesh]u3dSkinPose
var cacheBytes uint64
defer func() { r.remaining += cacheBytes }()
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if r.connected != nil && !r.connected[i] {
continue
}
node := &r.model.Nodes[i]
states := r.states[node.Name].Bones
animation := r.animated[node.Name]
if len(states) == 0 && len(animation.bones) == 0 {
continue
}
mesh := r.nodeMesh(node)
if node.Kind != "Model" || mesh == nil {
return fmt.Errorf("missing U3D bone state geometry for %q", node.Name)
}
if r.posed == nil {
if err := r.Reserve(1, 1024); err != nil {
return err
}
r.posed = make(map[string]*U3DMesh)
}
if err := r.Reserve(1, 128); err != nil {
return err
}
var temporary uint64
if len(animation.bones) != 0 {
if len(states) == 0 {
states = animation.bones
} else {
count := uint64(len(animation.bones)) + uint64(len(states))
if err := r.Reserve(count, 64); err != nil {
return err
}
temporary = count * 64
combined := make([]U3DBoneState, len(animation.bones), int(count))
copy(combined, animation.bones)
states = append(combined, states...)
}
}
weights := animation.weights
if weights == nil {
var err error
weights, err = r.skinWeightSource(mesh, node.BoneWeights, math.MaxUint64, math.MaxUint64)
if err != nil {
r.remaining += temporary
return err
}
}
previous, exists := cache[mesh]
shared := exists && previous.weights == weights && len(previous.states) == len(states)
if shared {
for j := range states {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
r.remaining += temporary
return err
}
}
if states[j] != previous.states[j] {
shared = false
break
}
}
}
if shared {
r.posed[node.Name] = previous.mesh
r.remaining += temporary
continue
}
source, sets := mesh, node.BoneWeights
if animation.weights != nil {
geometry := *mesh
geometry.BoneWeights = []U3DBoneWeights{*animation.weights}
mesh, sets = &geometry, nil
}
posed, err := r.skinMesh(mesh, sets, states)
if err != nil {
r.remaining += temporary
return err
}
r.posed[node.Name] = posed
if len(r.model.Nodes) > 1 {
if cache == nil {
if err := r.Reserve(1, 1024); err != nil {
return err
}
cacheBytes += 1024
cache = make(map[*U3DMesh]u3dSkinPose)
}
if !exists {
if err := r.Reserve(1, 128); err != nil {
return err
}
cacheBytes += 128
}
if err := r.Reserve(uint64(len(states)), 64); err != nil {
return err
}
cacheBytes += uint64(len(states)) * 64
saved := make([]U3DBoneState, len(states))
copy(saved, states)
cache[source] = u3dSkinPose{weights: weights, states: saved, mesh: posed}
released := uint64(len(previous.states)) * 64
r.remaining += released
cacheBytes -= released
}
r.remaining += temporary
}
return r.ctx.Err()
}
// boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态
// 入参: bones 按父项在前排列的骨架, states 显式姿态
// 返回: []u3dSkinBone 变形矩阵, error 骨架、姿态或取消错误
func (r *Renderer) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3dSkinBone, error) {
index := make(map[string]int, len(bones))
for i, bone := range bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if _, exists := index[bone.Name]; exists {
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
index[bone.Name] = i
}
overrides := make(map[int]U3DKeyFrame, min(len(states), len(bones)))
for i, state := range states {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
bone, exists := index[state.Name]
if !exists {
return nil, fmt.Errorf("missing U3D bone %q", state.Name)
}
overrides[bone] = state.Frame
}
bind, pose := make([]u3dMatrix, len(bones)), make([]u3dMatrix, len(bones))
result := make([]u3dSkinBone, len(bones))
for i, bone := range bones {
if err := r.ctx.Err(); err != nil {
return nil, err
}
if !finite(float64(bone.Length)) || bone.Length < 0 || bone.Attributes & ^uint32(255) != 0 {
return nil, fmt.Errorf("invalid U3D bone parameters")
}
if bone.Attributes&1 != 0 && bone.LinkCount != 0 {
return nil, fmt.Errorf("unsupported U3D bone link deformation")
}
initial := U3DKeyFrame{Displacement: bone.Displacement, Rotation: bone.Orientation, Scale: [3]float32{1, 1, 1}}
base, err := initial.Matrix()
if err != nil {
return nil, err
}
current := base
if frame, exists := overrides[i]; exists {
current, err = frame.Matrix()
if err != nil {
return nil, err
}
}
bind[i], pose[i] = u3dMatrix(base), u3dMatrix(current)
if i == 0 {
if bone.Parent != "" {
return nil, fmt.Errorf("invalid U3D root bone parent")
}
} else {
parent, exists := index[bone.Parent]
if !exists || parent >= i {
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
}
end := u3dIdentity()
end[12] = float64(bones[parent].Length)
bind[i] = bind[parent].multiply(end).multiply(bind[i])
pose[i] = pose[parent].multiply(end).multiply(pose[i])
}
inverse, ok := bind[i].inverse()
if !ok || !pose[i].valid() {
return nil, fmt.Errorf("invalid U3D bone transform")
}
result[i].matrix = pose[i].multiply(inverse)
if !result[i].matrix.valid() {
return nil, fmt.Errorf("invalid U3D bone deformation")
}
result[i].inverse, result[i].invertible = result[i].matrix.inverse()
}
return result, nil
}
// skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链
// 入参: mesh 原始几何, sets 节点权重
// 返回: *U3DBoneWeights 有效权重, error 类型、引用或取消错误
func (r *Renderer) skinWeights(mesh *U3DMesh, sets []U3DBoneWeights) (*U3DBoneWeights, error) {
return r.skinWeightSet(mesh, sets, math.MaxUint64, math.MaxUint64)
}
// skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 权重来源,内容尚未校验, error 类型、引用或取消错误
func (r *Renderer) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
kind := uint32(0)
for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} {
if count != 0 {
if kind != 0 {
return nil, fmt.Errorf("unsupported U3D mixed geometry deformation")
}
kind = 1 << i
}
}
var selected *U3DBoneWeights
for chain, group := range [][]U3DBoneWeights{mesh.BoneWeights, sets} {
end := resourceEnd
if chain != 0 {
end = nodeEnd
}
var last *U3DBoneWeights
for i := range group {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if uint64(group[i].Index) < end && group[i].Attributes == kind && (last == nil || group[i].Index > last.Index) {
last = &group[i]
}
}
if last != nil {
selected = last
}
}
if selected == nil {
return nil, fmt.Errorf("unsupported U3D deformation without bone weights")
}
return selected, r.ctx.Err()
}
// skinWeightSet 选取并校验指定修饰链位置之前的权重
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 有效权重, error 类型、引用、数值或取消错误
func (r *Renderer) skinWeightSet(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
selected, err := r.skinWeightSource(mesh, sets, resourceEnd, nodeEnd)
if err != nil {
return nil, err
}
if selected.Index == 0 || len(selected.Offsets) == 0 || len(selected.Offsets)-1 > len(mesh.Positions) || selected.Offsets[0] != 0 || uint64(selected.Offsets[len(selected.Offsets)-1]) != uint64(len(selected.Weights)) {
return nil, fmt.Errorf("invalid U3D bone weight offsets")
}
for i := 1; i < len(selected.Offsets); i++ {
if err := r.ctx.Err(); err != nil {
return nil, err
}
start, end := selected.Offsets[i-1], selected.Offsets[i]
if end < start || uint64(end) > uint64(len(selected.Weights)) {
return nil, fmt.Errorf("invalid U3D bone weight range")
}
var total float64
for j, weight := range selected.Weights[start:end] {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if uint64(weight.Bone) >= uint64(len(mesh.Bones)) || !finite(float64(weight.Weight)) || weight.Weight < 0 {
return nil, fmt.Errorf("invalid U3D bone weight")
}
total += float64(weight.Weight)
}
if start != end && math.Abs(total-1) > 1e-6 {
return nil, fmt.Errorf("invalid U3D bone weight sum")
}
}
return selected, nil
}
// influence 返回指定位置的骨骼权重,未提供权重的位置保持不变
// 入参: position 作者位置索引
// 返回: []U3DBoneWeight 影响列表
func (weights *U3DBoneWeights) influence(position uint32) []U3DBoneWeight {
if uint64(position)+1 >= uint64(len(weights.Offsets)) {
return nil
}
return weights.Weights[weights.Offsets[position]:weights.Offsets[position+1]]
}
// skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化
// 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线
// 返回: [3]float32 变形结果, error 数值、奇异变换或取消错误
func (r *Renderer) skinVector(value [3]float32, weights []U3DBoneWeight, bones []u3dSkinBone, normal bool) ([3]float32, error) {
input := u3dVector{float64(value[0]), float64(value[1]), float64(value[2])}
for _, component := range input {
if !finite(component) {
return [3]float32{}, fmt.Errorf("invalid U3D skin vector")
}
}
if len(weights) == 0 {
return value, nil
}
var result u3dVector
for i, weight := range weights {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return [3]float32{}, err
}
}
if weight.Weight == 0 {
continue
}
bone := &bones[weight.Bone]
v := bone.matrix.point(input)
if normal {
if !bone.invertible {
return [3]float32{}, fmt.Errorf("unsupported U3D singular bone normal transform")
}
m := bone.inverse
v = u3dVector{m[0]*input[0] + m[1]*input[1] + m[2]*input[2], m[4]*input[0] + m[5]*input[1] + m[6]*input[2], m[8]*input[0] + m[9]*input[1] + m[10]*input[2]}
}
for axis := range result {
result[axis] += v[axis] * float64(weight.Weight)
}
}
for _, component := range result {
if !finite(component) {
return [3]float32{}, fmt.Errorf("invalid U3D skin result")
}
}
if normal {
result = result.unit()
}
for axis := range value {
value[axis] = float32(result[axis])
if !finite(float64(value[axis])) {
return [3]float32{}, fmt.Errorf("U3D skin result exceeds range")
}
}
return value, nil
}
// skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引
// 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态
// 返回: *U3DMesh 独立姿态几何, error 结构、能力、预算或取消错误
func (r *Renderer) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DBoneState) (*U3DMesh, error) {
weights, err := r.skinWeights(mesh, sets)
if err != nil {
return nil, err
}
if err := r.Reserve(uint64(len(mesh.Bones)), 768); err != nil {
return nil, err
}
if err := r.Reserve(uint64(len(states)), 128); err != nil {
r.remaining += uint64(len(mesh.Bones)) * 768
return nil, err
}
temporary := uint64(len(mesh.Bones))*768 + uint64(len(states))*128
defer func() { r.remaining += temporary }()
bones, err := r.boneTransforms(mesh.Bones, states)
if err != nil {
return nil, err
}
if err := r.Reserve(1, 768); err != nil {
return nil, err
}
if err := r.Reserve(uint64(len(mesh.Positions)), 12); err != nil {
return nil, err
}
result := *mesh
result.Positions = make([][3]float32, len(mesh.Positions))
for i, position := range mesh.Positions {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
result.Positions[i], err = r.skinVector(position, weights.influence(uint32(i)), bones, false)
if err != nil {
return nil, err
}
}
if mesh.ExcludeNormals {
return &result, r.ctx.Err()
}
bytes := uint64(len(mesh.Faces))*148 + uint64(len(mesh.Lines))*100 + uint64(len(mesh.Points))*52
if err := r.Reserve(bytes, 1); err != nil {
return nil, err
}
result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points)
result.Normals = nil
if err := r.Reserve(1, 1024); err != nil {
return nil, err
}
index := make(map[[2]uint32]uint32)
defer func() { r.remaining += 1024 + uint64(len(index))*96 }()
for i := range len(result.Faces) + len(result.Lines) + len(result.Points) {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
_, corners := result.primitive(i)
for j := range corners {
corner := &corners[j]
if uint64(corner.Position) >= uint64(len(mesh.Positions)) || uint64(corner.Normal) >= uint64(len(mesh.Normals)) {
return nil, fmt.Errorf("invalid U3D skin corner index")
}
key := [2]uint32{corner.Position, corner.Normal}
normal, exists := index[key]
if !exists {
if err := r.Reserve(1, 96); err != nil {
return nil, err
}
if len(result.Normals) == cap(result.Normals) {
capacity := max(16, uint64(cap(result.Normals))*2)
if capacity > math.MaxUint32 {
return nil, fmt.Errorf("U3D skin normal count exceeds limit")
}
if err := r.Reserve(capacity, 12); err != nil {
return nil, err
}
grown := make([][3]float32, len(result.Normals), int(capacity))
copy(grown, result.Normals)
r.remaining += uint64(cap(result.Normals)) * 12
result.Normals = grown
}
value, err := r.skinVector(mesh.Normals[corner.Normal], weights.influence(corner.Position), bones, true)
if err != nil {
return nil, err
}
normal = uint32(len(result.Normals))
index[key] = normal
result.Normals = append(result.Normals, value)
}
corner.Normal = normal
}
}
return &result, r.ctx.Err()
}
+120
View File
@@ -0,0 +1,120 @@
// 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 threed
import (
"fmt"
)
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
// 显式Opacity覆盖材质和纹理合成的透明度,之后仍应用视图附加透明度
// 未匹配的名称不影响场景,Matrix和Motion均为空时保留每条父路径的原始变换
// Motion以指定轨道替换节点变换,与Matrix互斥,不驱动骨架;显式变换优先于动画修饰器
// Bones覆盖动画求值后的指定骨骼姿态,未指定项沿用当前局部变换,不修改共享几何
type U3DNodeState struct {
Name string
Opacity *float64
Visible *bool
Matrix *[16]float64
Motion *U3DTrackState
Bones []U3DBoneState
}
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
// 入参: groups 按优先级递增排列的节点覆盖分组
// 返回: error 数值、预算或取消错误
func (r *Renderer) nodeStates(groups ...[]U3DNodeState) error {
var count uint64
for _, states := range groups {
if uint64(len(states)) > ^uint64(0)-count {
return fmt.Errorf("U3D node state count exceeds limit")
}
count += uint64(len(states))
}
if count == 0 {
return nil
}
if err := r.Reserve(count, 160); err != nil {
return err
}
r.states = make(map[string]U3DNodeState, int(count))
for _, states := range groups {
for i, state := range states {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
r.states[state.Name] = state
}
}
var samples map[U3DTrackState]*[16]float64
defer func() {
if samples != nil {
r.remaining += 1024 + uint64(len(samples))*192
}
}()
for name, state := range r.states {
if err := r.ctx.Err(); err != nil {
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")
}
if state.Motion != nil {
if state.Matrix != nil || !finite(state.Motion.Time) {
return fmt.Errorf("invalid U3D node motion state")
}
if samples == nil {
if err := r.Reserve(1, 1024); err != nil {
return err
}
samples = make(map[U3DTrackState]*[16]float64)
}
matrix := samples[*state.Motion]
if matrix == nil {
if err := r.Reserve(1, 320); err != nil {
return err
}
value, err := r.trackMatrix(*state.Motion)
if err != nil {
return err
}
matrix = &value
samples[*state.Motion] = matrix
}
state.Matrix = matrix
r.states[name] = state
}
}
return nil
}
// 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
}
+181
View File
@@ -0,0 +1,181 @@
// 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 threed
import (
"fmt"
)
// U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据
type U3DTexture struct {
Name string
Width, Height uint32
ImageType uint8
Images []U3DTextureImage
}
// U3DTextureImage 保存压缩格式、通道位图及完整压缩数据,外部图像仅保留候选URL
// Compression依次以1至4表示JPEG-24、PNG、JPEG-8和TIFF,ByteCount仅用于内置图像
type U3DTextureImage struct {
Compression uint8
Channels uint8
External bool
ByteCount uint32
Data []byte
URLs []string
}
// u3dTextureChannels 将亮度通道展开为RGB覆盖位,保留透明度位
// 入参: channels 标准通道位图
// 返回: uint8 实际覆盖通道
func u3dTextureChannels(channels uint8) uint8 {
if channels&16 != 0 {
channels = channels&^16 | 14
}
return channels
}
// textureDeclaration 按ECMA-363第9.8.5.1节读取纹理及通道图像声明
// 入参: r 字段读取器, name 纹理名称
// 返回: error 格式、预算或取消错误
func (d *u3dDecoder) textureDeclaration(r *u3dValues, name string) error {
if _, exists := d.textures[name]; exists {
return fmt.Errorf("duplicate U3D texture %q", name)
}
texture := U3DTexture{Name: name, Height: r.u32(), Width: r.u32(), ImageType: r.u8()}
if texture.Width == 0 || texture.Height == 0 {
return fmt.Errorf("invalid U3D texture dimensions")
}
switch texture.ImageType {
case 1, 14, 15, 16, 17:
default:
return fmt.Errorf("invalid U3D texture image type")
}
count := r.u32()
if r.err != nil {
return r.err
}
if count == 0 || count > 4 || uint64(count) > uint64(len(r.data)-r.pos)/8 {
return fmt.Errorf("invalid U3D continuation image count")
}
if err := d.reserve(uint64(count), 64); err != nil {
return err
}
texture.Images = make([]U3DTextureImage, int(count))
var covered uint8
for n := range texture.Images {
if err := d.ctx.Err(); err != nil {
return err
}
image := &texture.Images[n]
image.Compression, image.Channels = r.u8(), r.u8()
attributes := uint16(r.u8()) | uint16(r.u8())<<8
if r.err != nil {
return r.err
}
channels := u3dTextureChannels(image.Channels)
if image.Compression < 1 || image.Compression > 4 || image.Channels == 0 || image.Channels&^uint8(31) != 0 || attributes > 1 || covered&channels != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
return fmt.Errorf("invalid U3D continuation image format")
}
covered |= channels
image.External = attributes == 1
if !image.External {
image.ByteCount = r.u32()
continue
}
count := r.u32()
if r.err != nil {
return r.err
}
if uint64(count) > uint64(len(r.data)-r.pos)/2 {
return fmt.Errorf("invalid U3D texture URL count")
}
if err := d.reserve(uint64(count), 16); err != nil {
return err
}
image.URLs = make([]string, int(count))
for i := range image.URLs {
if err := d.ctx.Err(); err != nil {
return err
}
image.URLs[i] = r.text()
if r.err != nil {
return r.err
}
}
}
if err := r.done(); err != nil {
return err
}
if covered != u3dTextureChannels(texture.ImageType) {
return fmt.Errorf("incomplete U3D texture channels")
}
if d.textures == nil {
d.textures = make(map[string]int)
}
d.textures[name] = len(d.model.Textures)
d.model.Textures = append(d.model.Textures, texture)
return nil
}
// textureContinuation 按声明长度合并纹理分片,不与输入共享存储
// 入参: r 字段读取器, name 纹理名称
// 返回: error 格式、预算或取消错误
func (d *u3dDecoder) textureContinuation(r *u3dValues, name string) error {
index := r.u32()
if r.err != nil {
return r.err
}
texture, exists := d.textures[name]
if !exists || uint64(index) >= uint64(len(d.model.Textures[texture].Images)) {
return fmt.Errorf("invalid U3D texture continuation reference")
}
image := &d.model.Textures[texture].Images[index]
data := r.take(len(r.data) - r.pos)
if image.External || uint64(len(image.Data))+uint64(len(data)) > uint64(image.ByteCount) {
return fmt.Errorf("invalid U3D texture continuation length")
}
if image.Data == nil {
if err := d.reserve(uint64(image.ByteCount), 1); err != nil {
return err
}
image.Data = make([]byte, 0, int(image.ByteCount))
}
for len(data) != 0 {
if err := d.ctx.Err(); err != nil {
return err
}
count := min(65536, len(data))
image.Data = append(image.Data, data[:count]...)
data = data[count:]
}
return d.ctx.Err()
}
// checkTextures 确认各通道的内置图像完整,不加载外部引用
// 返回: error 缺失分片或取消错误
func (d *u3dDecoder) checkTextures() error {
for _, texture := range d.model.Textures {
for _, image := range texture.Images {
if err := d.ctx.Err(); err != nil {
return err
}
if !image.External && uint64(len(image.Data)) != uint64(image.ByteCount) {
return fmt.Errorf("incomplete U3D texture %q", texture.Name)
}
}
}
return nil
}
+282
View File
@@ -0,0 +1,282 @@
// 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 threed
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"golang.org/x/image/tiff"
)
// U3DTextureResolver 由调用方按原始URL读取外部纹理,地址解析、访问策略和读取限额由调用方控制
// 返回的数据在本次绘制结束前不得修改;失败时尝试下一候选地址,取消及超时错误立即返回
// 入参: ctx 取消上下文, url 绝对或相对地址
// 返回: []byte 压缩图像数据, error 读取错误
type U3DTextureResolver func(ctx context.Context, url string) ([]byte, error)
// u3dImageReader 在图像解码器读取压缩数据时检查取消状态
type u3dImageReader struct {
ctx context.Context
data *bytes.Reader
}
// Read 读取顺序图像数据
// 入参: p 接收缓冲
// 返回: int 已读字节数, error 读取或取消错误
func (r *u3dImageReader) Read(p []byte) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.data.Read(p)
}
// ReadAt 读取指定偏移的图像数据
// 入参: p 接收缓冲, offset 字节偏移
// 返回: int 已读字节数, error 读取或取消错误
func (r *u3dImageReader) ReadAt(p []byte, offset int64) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.data.ReadAt(p, offset)
}
// texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理
// 入参: name 纹理名称
// 返回: *image.NRGBA64 非预乘纹理, error 资源、解码、预算或取消错误
func (r *Renderer) texture(name string) (*image.NRGBA64, error) {
if err := r.ctx.Err(); err != nil {
return nil, err
}
if cached := r.textures[name]; cached != nil {
return cached, nil
}
if err := r.Reserve(1, 256); err != nil {
return nil, err
}
var pixels *image.NRGBA64
if name == "" {
if err := r.Reserve(64, 8); err != nil {
return nil, err
}
pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8))
for y := range 8 {
for x := range 8 {
c := color.NRGBA64{65535, 65535, 65535, 65535}
if x/4 != y/4 {
c.G, c.B = 26214, 13107
}
pixels.SetNRGBA64(x, y, c)
}
}
} else {
index, exists := r.textureIndex[name]
if !exists {
return nil, fmt.Errorf("missing U3D texture %q", name)
}
var err error
pixels, err = r.decodeTexture(&r.model.Textures[index])
if err != nil {
return nil, fmt.Errorf("U3D texture %q: %w", name, err)
}
}
if r.textures == nil {
r.textures = make(map[string]*image.NRGBA64)
}
r.textures[name] = pixels
return pixels, nil
}
// decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度
// 入参: texture 纹理资源
// 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误
func (r *Renderer) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
switch texture.ImageType {
case 1, 14, 15, 16, 17:
default:
return nil, fmt.Errorf("invalid image type")
}
if texture.Width == 0 || texture.Height == 0 || len(texture.Images) == 0 || len(texture.Images) > 4 {
return nil, fmt.Errorf("invalid image dimensions or channel count")
}
var covered uint8
for _, source := range texture.Images {
if source.Compression < 1 || source.Compression > 4 {
return nil, fmt.Errorf("invalid image compression")
}
channels := u3dTextureChannels(source.Channels)
if source.Channels == 0 || source.Channels&^uint8(31) != 0 || channels&covered != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
return nil, fmt.Errorf("invalid image channels")
}
covered |= channels
if source.External && (r.resolveTexture == nil || len(source.URLs) == 0) {
return nil, fmt.Errorf("external texture image requires resolution")
}
if !source.External && uint64(len(source.Data)) != uint64(source.ByteCount) {
return nil, fmt.Errorf("invalid image data length")
}
}
if covered != u3dTextureChannels(texture.ImageType) {
return nil, fmt.Errorf("incomplete image channels")
}
if err := r.Reserve(uint64(texture.Width)*uint64(texture.Height), 8); err != nil {
return nil, err
}
pixels := image.NewNRGBA64(image.Rect(0, 0, int(texture.Width), int(texture.Height)))
for y := range pixels.Rect.Dy() {
if err := r.ctx.Err(); err != nil {
return nil, err
}
for x := range pixels.Rect.Dx() {
if x%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
pixels.SetNRGBA64(x, y, color.NRGBA64{65535, 65535, 65535, 65535})
}
}
for _, source := range texture.Images {
if err := r.textureSource(pixels, source); err != nil {
return nil, err
}
}
return pixels, r.ctx.Err()
}
// textureSource 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明
// 入参: pixels 目标纹理, source 通道图像
// 返回: error 读取、解码、预算或取消错误
func (r *Renderer) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error {
if !source.External {
return r.textureChannels(pixels, source)
}
if r.resolveTexture == nil || len(source.URLs) == 0 {
return fmt.Errorf("external texture image requires resolution")
}
var last error
for _, address := range source.URLs {
if err := r.ctx.Err(); err != nil {
return err
}
data, err := r.resolveTexture(r.ctx, address)
if r.ctx.Err() != nil {
return r.ctx.Err()
}
if err == nil {
source.Data = data
err = r.textureChannels(pixels, source)
source.Data = nil
}
if canceled := r.ctx.Err(); canceled != nil {
return canceled
}
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
last = fmt.Errorf("external texture image %q: %w", address, err)
}
return last
}
// textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销
// 入参: pixels 目标纹理, source 通道图像
// 返回: error 格式、预算或取消错误
func (r *Renderer) textureChannels(pixels *image.NRGBA64, source U3DTextureImage) error {
var config func(io.Reader) (image.Config, error)
var decode func(io.Reader) (image.Image, error)
switch source.Compression {
case 1, 3:
config, decode = jpeg.DecodeConfig, jpeg.Decode
case 2:
config, decode = png.DecodeConfig, png.Decode
case 4:
config, decode = tiff.DecodeConfig, tiff.Decode
default:
return fmt.Errorf("invalid image compression")
}
budget := r.remaining
defer func() { r.remaining = budget }()
if err := r.Reserve(uint64(len(source.Data)), 4); err != nil {
return err
}
if err := r.Reserve(1, 1<<20); err != nil {
return err
}
reader := &u3dImageReader{ctx: r.ctx, data: bytes.NewReader(source.Data)}
cfg, err := config(reader)
if err != nil {
return err
}
if cfg.Width != pixels.Rect.Dx() || cfg.Height != pixels.Rect.Dy() {
return fmt.Errorf("image dimensions differ from texture declaration")
}
if source.Compression == 3 && cfg.ColorModel != color.GrayModel || source.Compression == 1 && cfg.ColorModel != color.YCbCrModel {
return fmt.Errorf("invalid JPEG texture color model")
}
w, h := uint64(cfg.Width)+16, uint64(cfg.Height)+16
if source.Compression == 4 {
w, h = max(w, 1024), max(h, 1024)
}
if err := r.Reserve(w*h, 32); err != nil {
return err
}
reader.data.Reset(source.Data)
img, err := decode(reader)
if err != nil {
return err
}
if img.Bounds() != pixels.Bounds() {
return fmt.Errorf("invalid decoded image dimensions")
}
channels := u3dTextureChannels(source.Channels)
grayAlpha := source.Channels == 1 && (cfg.ColorModel == color.GrayModel || cfg.ColorModel == color.Gray16Model)
for y := range cfg.Height {
for x := range cfg.Width {
if x%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
value := baseimage.NRGBA64At(img, x, y)
current := pixels.NRGBA64At(x, y)
if channels&8 != 0 {
current.R = value.R
}
if channels&4 != 0 {
current.G = value.G
}
if channels&2 != 0 {
current.B = value.B
}
if channels&1 != 0 {
current.A = value.A
if grayAlpha {
current.A = value.R
}
}
pixels.SetNRGBA64(x, y, current)
}
}
return r.ctx.Err()
}
+545
View File
@@ -0,0 +1,545 @@
// 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 threed
import (
"fmt"
"math"
"slices"
)
// u3dVertex 保存裁剪前后的相机坐标、未预乘颜色及纹理插值输入
type u3dVertex struct {
point u3dVector
color [4]float64
texture [8][3]float64
textureMask uint8
opacityOverride bool
}
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误
func (r *Renderer) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
if r.mode >= U3DRenderBoundingBox && r.mode <= U3DRenderBoundingBoxOutline {
return r.drawBounds(instance, node, mesh, pass, passIndex)
}
if r.opacity == 0 && len(r.sections) == 0 && r.mode == U3DRenderSolid {
return r.ctx.Err()
}
shaded := r.mode.shaded() && !r.depthOnly
var outline *u3dOutline
if r.mode.silhouette() {
if err := r.Reserve(1, 1024); err != nil {
return err
}
outline = &u3dOutline{index: make(map[[2]u3dVector]int)}
defer func() { r.remaining += 1024 + uint64(len(outline.edges))*1024 }()
}
if r.mode == U3DRenderShadedWireframe || r.mode == U3DRenderShadedVertices {
if err := r.Reserve(1, 1024); err != nil {
return err
}
r.primitives = make(map[u3dPrimitiveKey]struct{})
defer func() {
r.remaining += 1024 + uint64(len(r.primitives))*512 + uint64(len(r.texturedPrimitives))*1024
r.primitives = nil
r.texturedPrimitives = nil
}()
}
normalCount := 0
if shaded {
normalCount = len(mesh.Normals)
}
count := uint64(len(mesh.Positions)) + uint64(normalCount)
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, normalCount)
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[:normalCount] {
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])})
}
}
if err := r.drawPrimitives(instance, node, mesh, positions, normals, pass, passIndex); err != nil {
return err
}
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)) || shaded && (!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 shaded && (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)
surfaceVisible := facing != 0 && (front && visibility&1 != 0 || !front && visibility&2 != 0)
if r.mode == U3DRenderHiddenWireframe && !front || (r.mode == U3DRenderSolid || r.mode == U3DRenderSolidWireframe || r.depthOnly) && (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]
}
if outline != nil {
if err := r.outlineFace(outline, world, vertices, front, surfaceVisible, shaderNames, passIndex); err != nil {
return err
}
if !surfaceVisible {
continue
}
}
for shaderIndex, name := range shaderNames {
if shaderIndex%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
var err error
if shaded {
style, err = r.style(name)
if err != nil {
return err
}
}
if style.shader.RenderPass&(1<<passIndex) == 0 {
continue
}
if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606
}
textures, err := r.shaderTextures(style.shader)
if err != nil {
return err
}
for i, corner := range face.Corners {
normal := faceNormal
if shaded && !mesh.ExcludeNormals && invertible {
normal = normals[corner.Normal]
}
if !front {
for j := range normal {
normal[j] = -normal[j]
}
}
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
return err
}
if !shaded {
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if r.mode == U3DRenderIllustration {
alpha := 1.0
if instance.opacity >= 0 {
alpha = instance.opacity
}
if r.opacity >= 0 {
alpha *= r.opacity
}
vertices[i].color = [4]float64{r.faceColor[0], r.faceColor[1], r.faceColor[2], alpha}
}
continue
}
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
vertices[i].opacityOverride = true
}
if r.opacity >= 0 && textures == nil {
vertices[i].color[3] *= r.opacity
}
}
r.order++
if err := r.modeTriangle(vertices, style.shader, pass); err != nil {
return err
}
}
}
if outline != nil {
return r.drawOutline(outline, pass)
}
return nil
}
// clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *Renderer) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if shader.Textures == ([8]*U3DTextureLayer{}) && shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
return r.ctx.Err()
}
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)
intersection := u3dInterpolate(previous, vertex, 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 *Renderer) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
var x, y, reciprocal [3]float64
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
}
if r.depthOnly {
r.frame[pixel].depth = 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])
}
position := u3dVector{}
if pass.Attributes&1 != 0 || textures != nil && textures.reflection {
position = u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth}
if r.camera.Perspective {
position[0] *= depth
position[1] *= depth
}
}
if textures != nil {
var uv [8][3]float64
for layer := range textures.layers {
if textures.mask&(1<<layer) == 0 {
continue
}
for c := range uv[layer] {
uv[layer][c] = triangle[0].texture[layer][c] + w1*(triangle[1].texture[layer][c]-triangle[0].texture[layer][c]) + w2*(triangle[2].texture[layer][c]-triangle[0].texture[layer][c])
}
}
if err := r.textureColor(textures, &color, &uv, position, triangle[0].opacityOverride); err != nil {
return err
}
}
visible, err := u3dFragmentColor(&color, position, &shader, &pass)
if err != nil {
return err
}
if !visible {
continue
}
if err := r.fragment(pixel, depth, color, shader.Blend); err != nil {
return err
}
}
}
return nil
}
// fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度
// 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式
// 返回: error 预算或取消错误
func (r *Renderer) 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 *Renderer) 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 threed
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
}
+39
View File
@@ -0,0 +1,39 @@
// 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 threed
import (
"image"
"math"
)
// viewImageBounds 计算采样器可能写入的像素范围,并在整数转换前裁至目标边界
// 入参: source 原始像素边界, m 像素变换, target 目标边界
// 返回: image.Rectangle 受影响的目标像素区域
func viewImageBounds(source image.Rectangle, m [6]float64, target image.Rectangle) image.Rectangle {
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
for _, p := range [4]image.Point{source.Min, {X: source.Max.X, Y: source.Min.Y}, {X: source.Min.X, Y: source.Max.Y}, source.Max} {
x := math.Floor(float64(m[0]*float64(p.X)) + float64(m[2]*float64(p.Y)) + m[4])
y := math.Floor(float64(m[1]*float64(p.X)) + float64(m[3]*float64(p.Y)) + m[5])
minX, minY = math.Min(minX, x), math.Min(minY, y)
maxX, maxY = math.Max(maxX, x+1), math.Max(maxY, y+1)
}
minX, minY = math.Max(minX, float64(target.Min.X)), math.Max(minY, float64(target.Min.Y))
maxX, maxY = math.Min(maxX, float64(target.Max.X)), math.Min(maxY, float64(target.Max.Y))
if minX >= maxX || minY >= maxY {
return image.Rectangle{}
}
return image.Rect(int(minX), int(minY), int(maxX), int(maxY))
}
+68
View File
@@ -0,0 +1,68 @@
// 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 threed
import (
"errors"
)
// 视图相机路径解析错误,由文档适配层转换为相应格式的错误
var (
ErrCameraNotFound = errors.New("missing U3D camera path")
ErrAmbiguousCamera = errors.New("ambiguous U3D camera instance")
)
// ViewCamera 返回视图节点指定父路径的世界变换,不修改绘制会话
// 入参: selected 视图节点索引, path 从根至节点的路径, exactRoot 是否要求路径始于根
// 返回: [16]float64 世界变换, error 缺失、歧义或取消错误
func (r *Renderer) ViewCamera(selected int, path []string, exactRoot bool) ([16]float64, error) {
var transform [16]float64
if selected < 0 || selected >= len(r.model.Nodes) || r.model.Nodes[selected].Kind != "View" {
return transform, ErrCameraNotFound
}
found := false
for _, index := range r.byNode[selected] {
if err := r.ctx.Err(); err != nil {
return transform, err
}
current, match := index, true
for j := len(path) - 1; j >= 0; j-- {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return transform, err
}
}
if current < 0 || r.model.Nodes[r.scene[current].node].Name != path[j] || r.model.Nodes[r.scene[current].node].Kind != "View" {
match = false
break
}
current = r.scene[current].parent
}
if exactRoot && current >= 0 {
match = false
}
if !match {
continue
}
if found {
return transform, ErrAmbiguousCamera
}
transform, found = r.scene[index].world, true
}
if !found {
return transform, ErrCameraNotFound
}
return transform, nil
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"image"
"math"
)
// viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层
// 入参: layers 按后至前排列的纹理层
// 返回: error 参数、资源、预算或取消错误
func (r *Renderer) viewLayers(layers []U3DViewLayer) error {
for _, layer := range layers {
if err := r.ctx.Err(); err != nil {
return err
}
for _, value := range [...]float32{layer.Blend, layer.Rotation, layer.Location[0], layer.Location[1], layer.Scale[0], layer.Scale[1]} {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D view layer values")
}
}
texture, err := r.texture(layer.Texture)
if err != nil {
return err
}
blend := min(1, max(0, float64(layer.Blend)))
if blend == 0 || layer.Scale[0] == 0 || layer.Scale[1] == 0 {
continue
}
sin, cos := math.Sincos(float64(layer.Rotation))
x, y := float64(layer.Location[0]), float64(layer.Location[1])
if r.view.Attributes&1 != 0 {
x *= float64(r.output.Rect.Dx())
y *= float64(r.output.Rect.Dy())
}
sx, sy := float64(layer.Scale[0]), float64(layer.Scale[1])
rx, ry := float64(layer.Registration[0]), float64(layer.Registration[1])
m := [6]float64{cos * sx, -sin * sx, sin * sy, cos * sy, x - cos*sx*rx - sin*sy*ry, y + sin*sx*rx - cos*sy*ry}
bounds := viewImageBounds(texture.Bounds(), m, r.output.Bounds())
for py := bounds.Min.Y; py < bounds.Max.Y; py++ {
for px := bounds.Min.X; px < bounds.Max.X; px++ {
if px%4096 == 0 || px == bounds.Min.X {
if err := r.ctx.Err(); err != nil {
return err
}
}
cx, cy := float64(px)+.5, float64(py)+.5
if cx < r.viewport[0] || cy < r.viewport[1] || cx >= r.viewport[0]+r.viewport[2] || cy >= r.viewport[1]+r.viewport[3] {
continue
}
dx, dy := cx-x, cy-y
u, v := (cos*dx-sin*dy)/sx+rx, (sin*dx+cos*dy)/sy+ry
if u < 0 || v < 0 || u >= float64(texture.Rect.Dx()) || v >= float64(texture.Rect.Dy()) {
continue
}
value := u3dTextureSample(texture, u, v)
pixel := &r.frame[py*r.output.Rect.Dx()+px]
alpha := value[3] * blend
for i := range pixel.color {
pixel.color[i] = value[i]*blend + pixel.color[i]*(1-alpha)
}
}
}
}
return r.ctx.Err()
}
// u3dTextureSample 在像素中心进行双线性采样,使用预乘颜色避免透明边缘色晕
// 入参: texture 纹理, x 横向像素坐标, y 纵向像素坐标
// 返回: [4]float64 预乘颜色及透明度
func u3dTextureSample(texture *image.NRGBA64, x, y float64) [4]float64 {
x, y = x-.5, y-.5
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
fx, fy := x-float64(x0), y-float64(y0)
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
var result [4]float64
for row := range 2 {
for col := range 2 {
weight := wx[col] * wy[row]
c := texture.NRGBA64At(min(max(0, x0+col), texture.Rect.Max.X-1), min(max(0, y0+row), texture.Rect.Max.Y-1))
alpha := float64(c.A) / 65535
result[0] += float64(c.R) / 65535 * alpha * weight
result[1] += float64(c.G) / 65535 * alpha * weight
result[2] += float64(c.B) / 65535 * alpha * weight
result[3] += alpha * weight
}
}
return result
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package threed
import (
"fmt"
"math"
)
// U3DBoneWeight 保存某骨骼对一个位置的影响系数
type U3DBoneWeight struct {
Bone uint32
Weight float32
}
// U3DBoneWeights 保存修饰链中的权重,Index为链内位置,Attributes的1、2、4分别表示网格、线集、点集
// 第i个位置的权重为Weights[Offsets[i]:Offsets[i+1]],空区间表示没有骨骼影响
type U3DBoneWeights struct {
Index, Attributes uint32
InverseQuantization float32
Offsets []uint32
Weights []U3DBoneWeight
}
// boneWeights 读取ECMA-363第9.7.4节权重修饰器,末项权重由其余各项之和确定
// 入参: values 字段读取器, name 所属链名, chain 链类型, index 链内位置
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) boneWeights(values *u3dValues, name string, chain int, index uint32) error {
r := u3dResourceReader{values: values, decoder: d}
defer r.release()
weights := U3DBoneWeights{Index: index, Attributes: r.u32(), InverseQuantization: r.f32()}
positions := r.u32()
if err := r.err(); err != nil {
return err
}
if weights.Attributes != 1 && weights.Attributes != 2 && weights.Attributes != 4 || weights.InverseQuantization < 0 {
return fmt.Errorf("invalid U3D bone weight parameters")
}
if err := d.reserve(uint64(positions)+1, 4); err != nil {
return err
}
weights.Offsets = make([]uint32, int(positions)+1)
for position := range positions {
if err := d.ctx.Err(); err != nil {
return err
}
count := r.value(0, false)
if err := r.err(); err != nil {
return err
}
start := len(weights.Weights)
needed := uint64(start) + uint64(count)
if needed > math.MaxUint32 {
return fmt.Errorf("U3D bone weight count exceeds limit")
}
if needed > uint64(cap(weights.Weights)) {
capacity := max(needed, uint64(cap(weights.Weights))*2)
if err := d.reserve(capacity, 8); err != nil {
return err
}
grown := make([]U3DBoneWeight, int(needed), int(capacity))
copy(grown, weights.Weights)
d.remaining += uint64(cap(weights.Weights)) * 8
weights.Weights = grown
} else {
weights.Weights = weights.Weights[:int(needed)]
}
list := weights.Weights[start:]
for i := range list {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
list[i].Bone = r.value(1, false)
if err := r.err(); err != nil {
return err
}
}
var total float32
for i := 0; i+1 < len(list); i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
list[i].Weight = float32(float64(r.value(2, false)) * float64(weights.InverseQuantization))
total += list[i].Weight
if err := r.err(); err != nil {
return err
}
if !finite(float64(total)) || total > 1 {
return fmt.Errorf("invalid U3D bone weight sum")
}
}
if len(list) != 0 {
list[len(list)-1].Weight = 1 - total
}
weights.Offsets[position+1] = uint32(needed)
}
if err := r.done(); err != nil {
return err
}
if chain == 1 {
d.meshes[name].mesh.BoneWeights = append(d.meshes[name].mesh.BoneWeights, weights)
} else {
node := &d.model.Nodes[d.nodes[name]]
node.BoneWeights = append(node.BoneWeights, weights)
}
return nil
}
// checkBoneWeights 在几何和节点声明齐备后校验权重引用,允许节点引用后续资源
// 返回: error 位置、骨骼引用或取消错误
func (d *u3dDecoder) checkBoneWeights() error {
for _, name := range d.meshOrder {
mesh := d.meshes[name]
if err := d.checkMeshBoneWeights(mesh, mesh.mesh.BoneWeights); err != nil {
return err
}
}
for _, node := range d.model.Nodes {
if err := d.checkMeshBoneWeights(d.meshes[node.Resource], node.BoneWeights); err != nil {
return err
}
}
return nil
}
// checkMeshBoneWeights 校验适用于指定几何类型的权重,不把未匹配的类型强加到几何上
// 入参: mesh 几何声明,可为空, sets 权重修饰器
// 返回: error 位置、骨骼引用或取消错误
func (d *u3dDecoder) checkMeshBoneWeights(mesh *u3dMeshDeclaration, sets []U3DBoneWeights) error {
for _, weights := range sets {
if err := d.ctx.Err(); err != nil {
return err
}
if mesh == nil {
if len(weights.Weights) != 0 {
return fmt.Errorf("missing U3D bone weight geometry")
}
continue
}
kind := uint32(1)
if mesh.points {
kind = 4
} else if mesh.line != nil {
kind = 2
}
if weights.Attributes != kind {
continue
}
if uint64(len(weights.Offsets)-1) > uint64(len(mesh.mesh.Positions)) {
return fmt.Errorf("invalid U3D bone weight position count")
}
for index, weight := range weights.Weights {
if index%1024 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
if uint64(weight.Bone) >= uint64(len(mesh.mesh.Bones)) {
return fmt.Errorf("invalid U3D bone weight index")
}
}
}
return nil
}