mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
330 lines
9.9 KiB
Go
330 lines
9.9 KiB
Go
// 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
|
|
}
|