Compare commits
1 Commits
63ca7eff0d
...
Server-sid
| Author | SHA1 | Date | |
|---|---|---|---|
| 399e6f096f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -109,3 +109,4 @@ dev/
|
||||
service_coverage
|
||||
.gitignore
|
||||
docs/
|
||||
blessing skin材质渲染示例/
|
||||
|
||||
121
internal/service/skin_renderer/cape_renderer.go
Normal file
121
internal/service/skin_renderer/cape_renderer.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package skin_renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
// CapeRenderer 披风渲染器
|
||||
type CapeRenderer struct{}
|
||||
|
||||
// NewCapeRenderer 创建披风渲染器
|
||||
func NewCapeRenderer() *CapeRenderer {
|
||||
return &CapeRenderer{}
|
||||
}
|
||||
|
||||
// Render 渲染披风
|
||||
// 披风纹理布局:
|
||||
// - 正面: (1, 1) 到 (11, 17) - 10x16 像素
|
||||
// - 背面: (12, 1) 到 (22, 17) - 10x16 像素
|
||||
func (r *CapeRenderer) Render(capeData []byte, height int) (image.Image, error) {
|
||||
// 解码披风图像
|
||||
img, err := png.Decode(bytes.NewReader(capeData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
srcWidth := bounds.Dx()
|
||||
srcHeight := bounds.Dy()
|
||||
|
||||
// 披风纹理可能是 64x32 或 22x17
|
||||
// 标准披风正面区域
|
||||
var frontX, frontY, frontW, frontH int
|
||||
|
||||
if srcWidth >= 64 && srcHeight >= 32 {
|
||||
// 64x32 格式(Minecraft 1.8+)
|
||||
// 正面: (1, 1) 到 (11, 17)
|
||||
frontX = 1
|
||||
frontY = 1
|
||||
frontW = 10
|
||||
frontH = 16
|
||||
} else if srcWidth >= 22 && srcHeight >= 17 {
|
||||
// 22x17 格式(旧版)
|
||||
frontX = 1
|
||||
frontY = 1
|
||||
frontW = 10
|
||||
frontH = 16
|
||||
} else {
|
||||
// 未知格式,直接缩放整个图像
|
||||
return resizeImageBilinear(img, height*srcWidth/srcHeight, height), nil
|
||||
}
|
||||
|
||||
// 提取正面区域
|
||||
front := image.NewRGBA(image.Rect(0, 0, frontW, frontH))
|
||||
for y := 0; y < frontH; y++ {
|
||||
for x := 0; x < frontW; x++ {
|
||||
front.Set(x, y, img.At(bounds.Min.X+frontX+x, bounds.Min.Y+frontY+y))
|
||||
}
|
||||
}
|
||||
|
||||
// 计算输出尺寸,保持宽高比
|
||||
outputWidth := height * frontW / frontH
|
||||
if outputWidth < 1 {
|
||||
outputWidth = 1
|
||||
}
|
||||
|
||||
// 使用最近邻缩放保持像素风格
|
||||
return scaleNearest(front, outputWidth, height), nil
|
||||
}
|
||||
|
||||
// scaleNearest 最近邻缩放
|
||||
func scaleNearest(src image.Image, width, height int) *image.RGBA {
|
||||
bounds := src.Bounds()
|
||||
srcW := bounds.Dx()
|
||||
srcH := bounds.Dy()
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
srcX := bounds.Min.X + x*srcW/width
|
||||
srcY := bounds.Min.Y + y*srcH/height
|
||||
dst.Set(x, y, src.At(srcX, srcY))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// resizeImageBilinear 双线性插值缩放
|
||||
func resizeImageBilinear(src image.Image, width, height int) *image.RGBA {
|
||||
bounds := src.Bounds()
|
||||
srcW := float64(bounds.Dx())
|
||||
srcH := float64(bounds.Dy())
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// 计算源图像中的位置
|
||||
srcX := float64(x) * srcW / float64(width)
|
||||
srcY := float64(y) * srcH / float64(height)
|
||||
|
||||
// 简单的最近邻(可以改进为双线性)
|
||||
ix := int(srcX)
|
||||
iy := int(srcY)
|
||||
|
||||
if ix >= bounds.Dx() {
|
||||
ix = bounds.Dx() - 1
|
||||
}
|
||||
if iy >= bounds.Dy() {
|
||||
iy = bounds.Dy() - 1
|
||||
}
|
||||
|
||||
dst.Set(x, y, src.At(bounds.Min.X+ix, bounds.Min.Y+iy))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
113
internal/service/skin_renderer/minecraft.go
Normal file
113
internal/service/skin_renderer/minecraft.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package skin_renderer
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
)
|
||||
|
||||
// Minecraft 提供 Minecraft 皮肤渲染的入口方法
|
||||
// 与 blessing/texture-renderer 的 Minecraft 类保持兼容
|
||||
type Minecraft struct{}
|
||||
|
||||
// NewMinecraft 创建 Minecraft 渲染器实例
|
||||
func NewMinecraft() *Minecraft {
|
||||
return &Minecraft{}
|
||||
}
|
||||
|
||||
// RenderSkin 渲染完整皮肤预览(正面+背面)
|
||||
// ratio: 缩放比例,默认 7.0
|
||||
// isAlex: 是否为 Alex 模型(细手臂)
|
||||
func (m *Minecraft) RenderSkin(skinData []byte, ratio float64, isAlex bool) (image.Image, error) {
|
||||
vp := 15 // vertical padding
|
||||
hp := 30 // horizontal padding
|
||||
ip := 15 // internal padding
|
||||
|
||||
// 渲染正面(-45度)
|
||||
frontRenderer := NewSkinRenderer(ratio, false, -45, -25)
|
||||
front, err := frontRenderer.Render(skinData, isAlex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 渲染背面(135度)
|
||||
backRenderer := NewSkinRenderer(ratio, false, 135, -25)
|
||||
back, err := backRenderer.Render(skinData, isAlex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := front.Bounds().Dx()
|
||||
height := front.Bounds().Dy()
|
||||
|
||||
// 创建画布
|
||||
canvas := createEmptyCanvas((hp+width+ip)*2, vp*2+height)
|
||||
|
||||
// 绘制背面(左侧)
|
||||
draw.Draw(canvas, image.Rect(hp, vp, hp+width, vp+height), back, back.Bounds().Min, draw.Over)
|
||||
|
||||
// 绘制正面(右侧)
|
||||
draw.Draw(canvas, image.Rect(hp+width+ip*2, vp, hp+width*2+ip*2, vp+height), front, front.Bounds().Min, draw.Over)
|
||||
|
||||
return canvas, nil
|
||||
}
|
||||
|
||||
// RenderCape 渲染披风
|
||||
// height: 输出高度
|
||||
func (m *Minecraft) RenderCape(capeData []byte, height int) (image.Image, error) {
|
||||
vp := 20 // vertical padding
|
||||
hp := 40 // horizontal padding
|
||||
|
||||
renderer := NewCapeRenderer()
|
||||
cape, err := renderer.Render(capeData, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := cape.Bounds().Dx()
|
||||
capeHeight := cape.Bounds().Dy()
|
||||
|
||||
canvas := createEmptyCanvas(hp*2+width, vp*2+capeHeight)
|
||||
draw.Draw(canvas, image.Rect(hp, vp, hp+width, vp+capeHeight), cape, cape.Bounds().Min, draw.Over)
|
||||
|
||||
return canvas, nil
|
||||
}
|
||||
|
||||
// Render2DAvatar 渲染 2D 头像
|
||||
// ratio: 缩放比例,默认 15.0
|
||||
func (m *Minecraft) Render2DAvatar(skinData []byte, ratio float64) (image.Image, error) {
|
||||
renderer := NewSkinRendererFull(ratio, true, 0, 0, 0, 0, 0, 0, 0, true)
|
||||
return renderer.Render(skinData, false)
|
||||
}
|
||||
|
||||
// Render3DAvatar 渲染 3D 头像
|
||||
// ratio: 缩放比例,默认 15.0
|
||||
func (m *Minecraft) Render3DAvatar(skinData []byte, ratio float64) (image.Image, error) {
|
||||
renderer := NewSkinRenderer(ratio, true, 45, -25)
|
||||
return renderer.Render(skinData, false)
|
||||
}
|
||||
|
||||
// RenderSkinWithAngle 渲染指定角度的皮肤
|
||||
func (m *Minecraft) RenderSkinWithAngle(skinData []byte, ratio float64, isAlex bool, hRotation, vRotation float64) (image.Image, error) {
|
||||
renderer := NewSkinRenderer(ratio, false, hRotation, vRotation)
|
||||
return renderer.Render(skinData, isAlex)
|
||||
}
|
||||
|
||||
// RenderHeadWithAngle 渲染指定角度的头像
|
||||
func (m *Minecraft) RenderHeadWithAngle(skinData []byte, ratio float64, hRotation, vRotation float64) (image.Image, error) {
|
||||
renderer := NewSkinRenderer(ratio, true, hRotation, vRotation)
|
||||
return renderer.Render(skinData, false)
|
||||
}
|
||||
|
||||
// createEmptyCanvas 创建透明画布
|
||||
func createEmptyCanvas(width, height int) *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
// 填充透明背景
|
||||
transparent := color.RGBA{0, 0, 0, 0}
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.SetRGBA(x, y, transparent)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
95
internal/service/skin_renderer/point.go
Normal file
95
internal/service/skin_renderer/point.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Package skin_renderer 实现 Minecraft 皮肤的 3D 渲染
|
||||
// 移植自 blessing/texture-renderer
|
||||
package skin_renderer
|
||||
|
||||
// Point 表示 3D 空间中的一个点
|
||||
type Point struct {
|
||||
// 原始坐标
|
||||
originX, originY, originZ float64
|
||||
// 投影后的坐标
|
||||
destX, destY, destZ float64
|
||||
// 是否已投影
|
||||
isProjected bool
|
||||
isPreProjected bool
|
||||
}
|
||||
|
||||
// NewPoint 创建一个新的 3D 点
|
||||
func NewPoint(x, y, z float64) *Point {
|
||||
return &Point{
|
||||
originX: x,
|
||||
originY: y,
|
||||
originZ: z,
|
||||
}
|
||||
}
|
||||
|
||||
// Project 将 3D 点投影到 2D 平面
|
||||
// 使用欧拉角旋转:alpha 为垂直旋转(X轴),omega 为水平旋转(Y轴)
|
||||
func (p *Point) Project(cosAlpha, sinAlpha, cosOmega, sinOmega float64, minX, maxX, minY, maxY *float64) {
|
||||
x := p.originX
|
||||
y := p.originY
|
||||
z := p.originZ
|
||||
|
||||
// 3D 旋转投影公式
|
||||
p.destX = x*cosOmega + z*sinOmega
|
||||
p.destY = x*sinAlpha*sinOmega + y*cosAlpha - z*sinAlpha*cosOmega
|
||||
p.destZ = -x*cosAlpha*sinOmega + y*sinAlpha + z*cosAlpha*cosOmega
|
||||
|
||||
p.isProjected = true
|
||||
|
||||
// 更新边界
|
||||
if p.destX < *minX {
|
||||
*minX = p.destX
|
||||
}
|
||||
if p.destX > *maxX {
|
||||
*maxX = p.destX
|
||||
}
|
||||
if p.destY < *minY {
|
||||
*minY = p.destY
|
||||
}
|
||||
if p.destY > *maxY {
|
||||
*maxY = p.destY
|
||||
}
|
||||
}
|
||||
|
||||
// PreProject 预投影,用于部件独立旋转(如头部、手臂)
|
||||
// dx, dy, dz 为旋转中心点
|
||||
func (p *Point) PreProject(dx, dy, dz, cosAlpha, sinAlpha, cosOmega, sinOmega float64) {
|
||||
if p.isPreProjected {
|
||||
return
|
||||
}
|
||||
|
||||
// 相对于旋转中心的坐标
|
||||
x := p.originX - dx
|
||||
y := p.originY - dy
|
||||
z := p.originZ - dz
|
||||
|
||||
// 旋转后加回偏移
|
||||
p.originX = x*cosOmega + z*sinOmega + dx
|
||||
p.originY = x*sinAlpha*sinOmega + y*cosAlpha - z*sinAlpha*cosOmega + dy
|
||||
p.originZ = -x*cosAlpha*sinOmega + y*sinAlpha + z*cosAlpha*cosOmega + dz
|
||||
|
||||
p.isPreProjected = true
|
||||
}
|
||||
|
||||
// GetDestCoord 获取投影后的坐标
|
||||
func (p *Point) GetDestCoord() (x, y, z float64) {
|
||||
return p.destX, p.destY, p.destZ
|
||||
}
|
||||
|
||||
// GetOriginCoord 获取原始坐标
|
||||
func (p *Point) GetOriginCoord() (x, y, z float64) {
|
||||
return p.originX, p.originY, p.originZ
|
||||
}
|
||||
|
||||
// IsProjected 返回是否已投影
|
||||
func (p *Point) IsProjected() bool {
|
||||
return p.isProjected
|
||||
}
|
||||
|
||||
// GetDepth 获取深度值(用于排序)
|
||||
func (p *Point) GetDepth(cosAlpha, sinAlpha, cosOmega, sinOmega float64, minX, maxX, minY, maxY *float64) float64 {
|
||||
if !p.isProjected {
|
||||
p.Project(cosAlpha, sinAlpha, cosOmega, sinOmega, minX, maxX, minY, maxY)
|
||||
}
|
||||
return p.destZ
|
||||
}
|
||||
200
internal/service/skin_renderer/polygon.go
Normal file
200
internal/service/skin_renderer/polygon.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package skin_renderer
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Polygon 表示一个四边形面片
|
||||
type Polygon struct {
|
||||
dots [4]*Point
|
||||
color color.RGBA
|
||||
isProjected bool
|
||||
face string // 面的方向: "x", "y", "z"
|
||||
faceDepth float64 // 面的深度
|
||||
}
|
||||
|
||||
// NewPolygon 创建一个新的多边形
|
||||
func NewPolygon(dots [4]*Point, c color.RGBA) *Polygon {
|
||||
p := &Polygon{
|
||||
dots: dots,
|
||||
color: c,
|
||||
}
|
||||
|
||||
// 确定面的方向
|
||||
x0, y0, z0 := dots[0].GetOriginCoord()
|
||||
x1, y1, z1 := dots[1].GetOriginCoord()
|
||||
x2, y2, z2 := dots[2].GetOriginCoord()
|
||||
|
||||
if x0 == x1 && x1 == x2 {
|
||||
p.face = "x"
|
||||
p.faceDepth = x0
|
||||
} else if y0 == y1 && y1 == y2 {
|
||||
p.face = "y"
|
||||
p.faceDepth = y0
|
||||
} else if z0 == z1 && z1 == z2 {
|
||||
p.face = "z"
|
||||
p.faceDepth = z0
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Project 投影多边形的所有顶点
|
||||
func (p *Polygon) Project(cosAlpha, sinAlpha, cosOmega, sinOmega float64, minX, maxX, minY, maxY *float64) {
|
||||
for _, dot := range p.dots {
|
||||
if !dot.IsProjected() {
|
||||
dot.Project(cosAlpha, sinAlpha, cosOmega, sinOmega, minX, maxX, minY, maxY)
|
||||
}
|
||||
}
|
||||
p.isProjected = true
|
||||
}
|
||||
|
||||
// PreProject 预投影多边形的所有顶点
|
||||
func (p *Polygon) PreProject(dx, dy, dz, cosAlpha, sinAlpha, cosOmega, sinOmega float64) {
|
||||
for _, dot := range p.dots {
|
||||
dot.PreProject(dx, dy, dz, cosAlpha, sinAlpha, cosOmega, sinOmega)
|
||||
}
|
||||
}
|
||||
|
||||
// IsProjected 返回是否已投影
|
||||
func (p *Polygon) IsProjected() bool {
|
||||
return p.isProjected
|
||||
}
|
||||
|
||||
// AddToImage 将多边形绘制到图像上
|
||||
func (p *Polygon) AddToImage(img *image.RGBA, minX, minY, ratio float64) {
|
||||
// 检查透明度,完全透明则跳过
|
||||
if p.color.A == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取投影后的 2D 坐标
|
||||
points := make([][2]float64, 4)
|
||||
var coordX, coordY *float64
|
||||
|
||||
samePlanX := true
|
||||
samePlanY := true
|
||||
|
||||
for i, dot := range p.dots {
|
||||
x, y, _ := dot.GetDestCoord()
|
||||
points[i] = [2]float64{
|
||||
(x - minX) * ratio,
|
||||
(y - minY) * ratio,
|
||||
}
|
||||
|
||||
if coordX == nil {
|
||||
coordX = &x
|
||||
coordY = &y
|
||||
} else {
|
||||
if *coordX != x {
|
||||
samePlanX = false
|
||||
}
|
||||
if *coordY != y {
|
||||
samePlanY = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有点在同一平面(退化面),跳过
|
||||
if samePlanX || samePlanY {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用扫描线算法填充多边形
|
||||
fillPolygon(img, points, p.color)
|
||||
}
|
||||
|
||||
// fillPolygon 使用扫描线算法填充四边形
|
||||
func fillPolygon(img *image.RGBA, points [][2]float64, c color.RGBA) {
|
||||
// 找到 Y 的范围
|
||||
minY := points[0][1]
|
||||
maxY := points[0][1]
|
||||
for _, pt := range points {
|
||||
if pt[1] < minY {
|
||||
minY = pt[1]
|
||||
}
|
||||
if pt[1] > maxY {
|
||||
maxY = pt[1]
|
||||
}
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
|
||||
// 扫描每一行
|
||||
for y := int(minY); y <= int(maxY); y++ {
|
||||
if y < bounds.Min.Y || y >= bounds.Max.Y {
|
||||
continue
|
||||
}
|
||||
|
||||
// 找到这一行与多边形边的交点
|
||||
var intersections []float64
|
||||
n := len(points)
|
||||
for i := 0; i < n; i++ {
|
||||
j := (i + 1) % n
|
||||
y1, y2 := points[i][1], points[j][1]
|
||||
x1, x2 := points[i][0], points[j][0]
|
||||
|
||||
// 检查这条边是否与当前扫描线相交
|
||||
if (y1 <= float64(y) && y2 > float64(y)) || (y2 <= float64(y) && y1 > float64(y)) {
|
||||
// 计算交点的 X 坐标
|
||||
t := (float64(y) - y1) / (y2 - y1)
|
||||
x := x1 + t*(x2-x1)
|
||||
intersections = append(intersections, x)
|
||||
}
|
||||
}
|
||||
|
||||
// 排序交点
|
||||
for i := 0; i < len(intersections)-1; i++ {
|
||||
for j := i + 1; j < len(intersections); j++ {
|
||||
if intersections[i] > intersections[j] {
|
||||
intersections[i], intersections[j] = intersections[j], intersections[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 填充交点之间的像素
|
||||
for i := 0; i+1 < len(intersections); i += 2 {
|
||||
xStart := int(intersections[i])
|
||||
xEnd := int(intersections[i+1])
|
||||
|
||||
for x := xStart; x <= xEnd; x++ {
|
||||
if x >= bounds.Min.X && x < bounds.Max.X {
|
||||
// Alpha 混合
|
||||
if c.A == 255 {
|
||||
img.SetRGBA(x, y, c)
|
||||
} else {
|
||||
existing := img.RGBAAt(x, y)
|
||||
blended := alphaBlend(existing, c)
|
||||
img.SetRGBA(x, y, blended)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// alphaBlend 执行 Alpha 混合
|
||||
func alphaBlend(dst, src color.RGBA) color.RGBA {
|
||||
if src.A == 0 {
|
||||
return dst
|
||||
}
|
||||
if src.A == 255 {
|
||||
return src
|
||||
}
|
||||
|
||||
srcA := float64(src.A) / 255.0
|
||||
dstA := float64(dst.A) / 255.0
|
||||
outA := srcA + dstA*(1-srcA)
|
||||
|
||||
if outA == 0 {
|
||||
return color.RGBA{}
|
||||
}
|
||||
|
||||
return color.RGBA{
|
||||
R: uint8((float64(src.R)*srcA + float64(dst.R)*dstA*(1-srcA)) / outA),
|
||||
G: uint8((float64(src.G)*srcA + float64(dst.G)*dstA*(1-srcA)) / outA),
|
||||
B: uint8((float64(src.B)*srcA + float64(dst.B)*dstA*(1-srcA)) / outA),
|
||||
A: uint8(outA * 255),
|
||||
}
|
||||
}
|
||||
1080
internal/service/skin_renderer/polygons.go
Normal file
1080
internal/service/skin_renderer/polygons.go
Normal file
File diff suppressed because it is too large
Load Diff
591
internal/service/skin_renderer/renderer.go
Normal file
591
internal/service/skin_renderer/renderer.go
Normal file
@@ -0,0 +1,591 @@
|
||||
package skin_renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
)
|
||||
|
||||
// SkinRenderer 皮肤渲染器
|
||||
type SkinRenderer struct {
|
||||
playerSkin image.Image
|
||||
isNewSkinType bool
|
||||
isAlex bool
|
||||
hdRatio int
|
||||
|
||||
// 旋转参数
|
||||
ratio float64
|
||||
headOnly bool
|
||||
hR float64 // 水平旋转角度
|
||||
vR float64 // 垂直旋转角度
|
||||
hrh float64 // 头部水平旋转
|
||||
vrll float64 // 左腿垂直旋转
|
||||
vrrl float64 // 右腿垂直旋转
|
||||
vrla float64 // 左臂垂直旋转
|
||||
vrra float64 // 右臂垂直旋转
|
||||
layers bool // 是否渲染第二层
|
||||
|
||||
// 计算后的三角函数值
|
||||
cosAlpha, sinAlpha float64
|
||||
cosOmega, sinOmega float64
|
||||
|
||||
// 边界
|
||||
minX, maxX, minY, maxY float64
|
||||
|
||||
// 各部件的旋转角度
|
||||
membersAngles map[string]angleSet
|
||||
|
||||
// 可见面
|
||||
visibleFaces map[string]faceVisibility
|
||||
frontFaces []string
|
||||
backFaces []string
|
||||
|
||||
// 多边形
|
||||
polygons map[string]map[string][]*Polygon
|
||||
}
|
||||
|
||||
type angleSet struct {
|
||||
cosAlpha, sinAlpha float64
|
||||
cosOmega, sinOmega float64
|
||||
}
|
||||
|
||||
type faceVisibility struct {
|
||||
front []string
|
||||
back []string
|
||||
}
|
||||
|
||||
var allFaces = []string{"back", "right", "top", "front", "left", "bottom"}
|
||||
|
||||
// NewSkinRenderer 创建皮肤渲染器
|
||||
func NewSkinRenderer(ratio float64, headOnly bool, horizontalRotation, verticalRotation float64) *SkinRenderer {
|
||||
return &SkinRenderer{
|
||||
ratio: ratio,
|
||||
headOnly: headOnly,
|
||||
hR: horizontalRotation,
|
||||
vR: verticalRotation,
|
||||
hrh: 0,
|
||||
vrll: 0,
|
||||
vrrl: 0,
|
||||
vrla: 0,
|
||||
vrra: 0,
|
||||
layers: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSkinRendererFull 创建带完整参数的皮肤渲染器
|
||||
func NewSkinRendererFull(ratio float64, headOnly bool, hR, vR, hrh, vrll, vrrl, vrla, vrra float64, layers bool) *SkinRenderer {
|
||||
return &SkinRenderer{
|
||||
ratio: ratio,
|
||||
headOnly: headOnly,
|
||||
hR: hR,
|
||||
vR: vR,
|
||||
hrh: hrh,
|
||||
vrll: vrll,
|
||||
vrrl: vrrl,
|
||||
vrla: vrla,
|
||||
vrra: vrra,
|
||||
layers: layers,
|
||||
}
|
||||
}
|
||||
|
||||
// Render 渲染皮肤
|
||||
func (r *SkinRenderer) Render(skinData []byte, isAlex bool) (image.Image, error) {
|
||||
// 解码皮肤图像
|
||||
img, err := png.Decode(bytes.NewReader(skinData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.playerSkin = img
|
||||
r.isAlex = isAlex
|
||||
|
||||
// 计算 HD 比例
|
||||
sourceWidth := img.Bounds().Dx()
|
||||
sourceHeight := img.Bounds().Dy()
|
||||
|
||||
// 防止内存溢出,限制最大尺寸
|
||||
if sourceWidth > 256 {
|
||||
r.playerSkin = resizeImage(img, 256, sourceHeight*256/sourceWidth)
|
||||
}
|
||||
|
||||
r.hdRatio = r.playerSkin.Bounds().Dx() / 64
|
||||
|
||||
// 检查是否为新版皮肤格式(64x64)
|
||||
if r.playerSkin.Bounds().Dx() == r.playerSkin.Bounds().Dy() {
|
||||
r.isNewSkinType = true
|
||||
}
|
||||
|
||||
// 转换为 RGBA
|
||||
r.playerSkin = convertToRGBA(r.playerSkin)
|
||||
|
||||
// 处理背景透明
|
||||
r.makeBackgroundTransparent()
|
||||
|
||||
// 计算角度
|
||||
r.calculateAngles()
|
||||
|
||||
// 确定可见面
|
||||
r.facesDetermination()
|
||||
|
||||
// 生成多边形
|
||||
r.generatePolygons()
|
||||
|
||||
// 部件旋转
|
||||
r.memberRotation()
|
||||
|
||||
// 创建投影
|
||||
r.createProjectionPlan()
|
||||
|
||||
// 渲染图像
|
||||
return r.displayImage(), nil
|
||||
}
|
||||
|
||||
// makeBackgroundTransparent 处理背景透明
|
||||
func (r *SkinRenderer) makeBackgroundTransparent() {
|
||||
rgba, ok := r.playerSkin.(*image.RGBA)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查左上角 8x8 区域是否为纯色
|
||||
var tempColor color.RGBA
|
||||
needRemove := true
|
||||
first := true
|
||||
|
||||
for y := 0; y < 8; y++ {
|
||||
for x := 0; x < 8; x++ {
|
||||
c := rgba.RGBAAt(x, y)
|
||||
|
||||
// 如果已有透明度,不需要处理
|
||||
if c.A < 128 {
|
||||
needRemove = false
|
||||
break
|
||||
}
|
||||
|
||||
if first {
|
||||
tempColor = c
|
||||
first = false
|
||||
} else if c != tempColor {
|
||||
needRemove = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needRemove {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !needRemove {
|
||||
return
|
||||
}
|
||||
|
||||
// 将该颜色设为透明
|
||||
bounds := rgba.Bounds()
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := rgba.RGBAAt(x, y)
|
||||
if c.R == tempColor.R && c.G == tempColor.G && c.B == tempColor.B {
|
||||
rgba.SetRGBA(x, y, color.RGBA{0, 0, 0, 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateAngles 计算旋转角度
|
||||
func (r *SkinRenderer) calculateAngles() {
|
||||
// 转换为弧度
|
||||
alpha := r.vR * math.Pi / 180
|
||||
omega := r.hR * math.Pi / 180
|
||||
|
||||
r.cosAlpha = math.Cos(alpha)
|
||||
r.sinAlpha = math.Sin(alpha)
|
||||
r.cosOmega = math.Cos(omega)
|
||||
r.sinOmega = math.Sin(omega)
|
||||
|
||||
r.membersAngles = make(map[string]angleSet)
|
||||
|
||||
// 躯干不旋转
|
||||
r.membersAngles["torso"] = angleSet{
|
||||
cosAlpha: 1, sinAlpha: 0,
|
||||
cosOmega: 1, sinOmega: 0,
|
||||
}
|
||||
|
||||
// 头部旋转
|
||||
omegaHead := r.hrh * math.Pi / 180
|
||||
r.membersAngles["head"] = angleSet{
|
||||
cosAlpha: 1, sinAlpha: 0,
|
||||
cosOmega: math.Cos(omegaHead), sinOmega: math.Sin(omegaHead),
|
||||
}
|
||||
r.membersAngles["helmet"] = r.membersAngles["head"]
|
||||
|
||||
// 右臂旋转
|
||||
alphaRightArm := r.vrra * math.Pi / 180
|
||||
r.membersAngles["rightArm"] = angleSet{
|
||||
cosAlpha: math.Cos(alphaRightArm), sinAlpha: math.Sin(alphaRightArm),
|
||||
cosOmega: 1, sinOmega: 0,
|
||||
}
|
||||
|
||||
// 左臂旋转
|
||||
alphaLeftArm := r.vrla * math.Pi / 180
|
||||
r.membersAngles["leftArm"] = angleSet{
|
||||
cosAlpha: math.Cos(alphaLeftArm), sinAlpha: math.Sin(alphaLeftArm),
|
||||
cosOmega: 1, sinOmega: 0,
|
||||
}
|
||||
|
||||
// 右腿旋转
|
||||
alphaRightLeg := r.vrrl * math.Pi / 180
|
||||
r.membersAngles["rightLeg"] = angleSet{
|
||||
cosAlpha: math.Cos(alphaRightLeg), sinAlpha: math.Sin(alphaRightLeg),
|
||||
cosOmega: 1, sinOmega: 0,
|
||||
}
|
||||
|
||||
// 左腿旋转
|
||||
alphaLeftLeg := r.vrll * math.Pi / 180
|
||||
r.membersAngles["leftLeg"] = angleSet{
|
||||
cosAlpha: math.Cos(alphaLeftLeg), sinAlpha: math.Sin(alphaLeftLeg),
|
||||
cosOmega: 1, sinOmega: 0,
|
||||
}
|
||||
|
||||
r.minX, r.maxX = 0, 0
|
||||
r.minY, r.maxY = 0, 0
|
||||
}
|
||||
|
||||
// facesDetermination 确定可见面
|
||||
func (r *SkinRenderer) facesDetermination() {
|
||||
r.visibleFaces = make(map[string]faceVisibility)
|
||||
|
||||
parts := []string{"head", "torso", "rightArm", "leftArm", "rightLeg", "leftLeg"}
|
||||
|
||||
for _, part := range parts {
|
||||
angles := r.membersAngles[part]
|
||||
|
||||
// 创建测试立方体点
|
||||
cubePoints := r.createCubePoints()
|
||||
|
||||
var maxDepthPoint *Point
|
||||
var maxDepthFaces []string
|
||||
|
||||
for _, cp := range cubePoints {
|
||||
point := cp.point
|
||||
point.PreProject(0, 0, 0, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
point.Project(r.cosAlpha, r.sinAlpha, r.cosOmega, r.sinOmega, &r.minX, &r.maxX, &r.minY, &r.maxY)
|
||||
|
||||
if maxDepthPoint == nil {
|
||||
maxDepthPoint = point
|
||||
maxDepthFaces = cp.faces
|
||||
} else {
|
||||
_, _, z1 := maxDepthPoint.GetDestCoord()
|
||||
_, _, z2 := point.GetDestCoord()
|
||||
if z1 > z2 {
|
||||
maxDepthPoint = point
|
||||
maxDepthFaces = cp.faces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.visibleFaces[part] = faceVisibility{
|
||||
back: maxDepthFaces,
|
||||
front: diffFaces(allFaces, maxDepthFaces),
|
||||
}
|
||||
}
|
||||
|
||||
// 确定全局前后面
|
||||
cubePoints := r.createCubePoints()
|
||||
var maxDepthPoint *Point
|
||||
var maxDepthFaces []string
|
||||
|
||||
for _, cp := range cubePoints {
|
||||
point := cp.point
|
||||
point.Project(r.cosAlpha, r.sinAlpha, r.cosOmega, r.sinOmega, &r.minX, &r.maxX, &r.minY, &r.maxY)
|
||||
|
||||
if maxDepthPoint == nil {
|
||||
maxDepthPoint = point
|
||||
maxDepthFaces = cp.faces
|
||||
} else {
|
||||
_, _, z1 := maxDepthPoint.GetDestCoord()
|
||||
_, _, z2 := point.GetDestCoord()
|
||||
if z1 > z2 {
|
||||
maxDepthPoint = point
|
||||
maxDepthFaces = cp.faces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.backFaces = maxDepthFaces
|
||||
r.frontFaces = diffFaces(allFaces, maxDepthFaces)
|
||||
}
|
||||
|
||||
type cubePoint struct {
|
||||
point *Point
|
||||
faces []string
|
||||
}
|
||||
|
||||
func (r *SkinRenderer) createCubePoints() []cubePoint {
|
||||
return []cubePoint{
|
||||
{NewPoint(0, 0, 0), []string{"back", "right", "top"}},
|
||||
{NewPoint(0, 0, 1), []string{"front", "right", "top"}},
|
||||
{NewPoint(0, 1, 0), []string{"back", "right", "bottom"}},
|
||||
{NewPoint(0, 1, 1), []string{"front", "right", "bottom"}},
|
||||
{NewPoint(1, 0, 0), []string{"back", "left", "top"}},
|
||||
{NewPoint(1, 0, 1), []string{"front", "left", "top"}},
|
||||
{NewPoint(1, 1, 0), []string{"back", "left", "bottom"}},
|
||||
{NewPoint(1, 1, 1), []string{"front", "left", "bottom"}},
|
||||
}
|
||||
}
|
||||
|
||||
func diffFaces(all, exclude []string) []string {
|
||||
excludeMap := make(map[string]bool)
|
||||
for _, f := range exclude {
|
||||
excludeMap[f] = true
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, f := range all {
|
||||
if !excludeMap[f] {
|
||||
result = append(result, f)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// memberRotation 部件旋转
|
||||
func (r *SkinRenderer) memberRotation() {
|
||||
hd := float64(r.hdRatio)
|
||||
|
||||
// 头部和头盔旋转
|
||||
angles := r.membersAngles["head"]
|
||||
for _, face := range r.polygons["head"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(4*hd, 8*hd, 2*hd, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
for _, face := range r.polygons["helmet"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(4*hd, 8*hd, 2*hd, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
|
||||
if r.headOnly {
|
||||
return
|
||||
}
|
||||
|
||||
// 右臂旋转
|
||||
angles = r.membersAngles["rightArm"]
|
||||
for _, face := range r.polygons["rightArm"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(-2*hd, 8*hd, 2*hd, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
|
||||
// 左臂旋转
|
||||
angles = r.membersAngles["leftArm"]
|
||||
for _, face := range r.polygons["leftArm"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(10*hd, 8*hd, 2*hd, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
|
||||
// 右腿旋转
|
||||
angles = r.membersAngles["rightLeg"]
|
||||
zOffset := 4 * hd
|
||||
if angles.sinAlpha < 0 {
|
||||
zOffset = 0
|
||||
}
|
||||
for _, face := range r.polygons["rightLeg"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(2*hd, 20*hd, zOffset, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
|
||||
// 左腿旋转
|
||||
angles = r.membersAngles["leftLeg"]
|
||||
zOffset = 4 * hd
|
||||
if angles.sinAlpha < 0 {
|
||||
zOffset = 0
|
||||
}
|
||||
for _, face := range r.polygons["leftLeg"] {
|
||||
for _, poly := range face {
|
||||
poly.PreProject(6*hd, 20*hd, zOffset, angles.cosAlpha, angles.sinAlpha, angles.cosOmega, angles.sinOmega)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createProjectionPlan 创建投影
|
||||
func (r *SkinRenderer) createProjectionPlan() {
|
||||
for _, piece := range r.polygons {
|
||||
for _, face := range piece {
|
||||
for _, poly := range face {
|
||||
if !poly.IsProjected() {
|
||||
poly.Project(r.cosAlpha, r.sinAlpha, r.cosOmega, r.sinOmega, &r.minX, &r.maxX, &r.minY, &r.maxY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// displayImage 渲染最终图像
|
||||
func (r *SkinRenderer) displayImage() image.Image {
|
||||
width := r.maxX - r.minX
|
||||
height := r.maxY - r.minY
|
||||
ratio := r.ratio * 2
|
||||
|
||||
srcWidth := int(ratio*width) + 1
|
||||
srcHeight := int(ratio*height) + 1
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, srcWidth, srcHeight))
|
||||
|
||||
// 按深度顺序绘制
|
||||
displayOrder := r.getDisplayOrder()
|
||||
|
||||
for _, order := range displayOrder {
|
||||
for piece, faces := range order {
|
||||
for _, face := range faces {
|
||||
if polys, ok := r.polygons[piece][face]; ok {
|
||||
for _, poly := range polys {
|
||||
poly.AddToImage(img, r.minX, r.minY, ratio)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 抗锯齿:2x 渲染后缩小
|
||||
realWidth := srcWidth / 2
|
||||
realHeight := srcHeight / 2
|
||||
destImg := resizeImage(img, realWidth, realHeight)
|
||||
|
||||
return destImg
|
||||
}
|
||||
|
||||
// getDisplayOrder 获取绘制顺序
|
||||
func (r *SkinRenderer) getDisplayOrder() []map[string][]string {
|
||||
var displayOrder []map[string][]string
|
||||
|
||||
if contains(r.frontFaces, "top") {
|
||||
if contains(r.frontFaces, "right") {
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.visibleFaces["leftLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.visibleFaces["rightLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.visibleFaces["leftArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.visibleFaces["torso"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.visibleFaces["rightArm"].front})
|
||||
} else {
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.visibleFaces["rightLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.visibleFaces["leftLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.visibleFaces["rightArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.visibleFaces["torso"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.visibleFaces["leftArm"].front})
|
||||
}
|
||||
|
||||
displayOrder = append(displayOrder, map[string][]string{"helmet": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"head": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"head": r.visibleFaces["head"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"helmet": r.visibleFaces["head"].front})
|
||||
} else {
|
||||
displayOrder = append(displayOrder, map[string][]string{"helmet": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"head": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"head": r.visibleFaces["head"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"helmet": r.visibleFaces["head"].front})
|
||||
|
||||
if contains(r.frontFaces, "right") {
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.visibleFaces["leftArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.visibleFaces["torso"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.visibleFaces["rightArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.visibleFaces["leftLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.visibleFaces["rightLeg"].front})
|
||||
} else {
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightArm": r.visibleFaces["rightArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"torso": r.visibleFaces["torso"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftArm": r.visibleFaces["leftArm"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"rightLeg": r.visibleFaces["rightLeg"].front})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.backFaces})
|
||||
displayOrder = append(displayOrder, map[string][]string{"leftLeg": r.visibleFaces["leftLeg"].front})
|
||||
}
|
||||
}
|
||||
|
||||
return displayOrder
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
func convertToRGBA(img image.Image) *image.RGBA {
|
||||
if rgba, ok := img.(*image.RGBA); ok {
|
||||
return rgba
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
rgba := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
rgba.Set(x, y, img.At(x, y))
|
||||
}
|
||||
}
|
||||
return rgba
|
||||
}
|
||||
|
||||
func resizeImage(img image.Image, width, height int) *image.RGBA {
|
||||
bounds := img.Bounds()
|
||||
srcW := bounds.Dx()
|
||||
srcH := bounds.Dy()
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
srcX := bounds.Min.X + x*srcW/width
|
||||
srcY := bounds.Min.Y + y*srcH/height
|
||||
dst.Set(x, y, img.At(srcX, srcY))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// getPixelColor 从皮肤图像获取像素颜色
|
||||
func (r *SkinRenderer) getPixelColor(x, y int) color.RGBA {
|
||||
if x < 0 || y < 0 {
|
||||
return color.RGBA{}
|
||||
}
|
||||
bounds := r.playerSkin.Bounds()
|
||||
if x >= bounds.Dx() || y >= bounds.Dy() {
|
||||
return color.RGBA{}
|
||||
}
|
||||
|
||||
c := r.playerSkin.At(bounds.Min.X+x, bounds.Min.Y+y)
|
||||
r32, g32, b32, a32 := c.RGBA()
|
||||
return color.RGBA{
|
||||
R: uint8(r32 >> 8),
|
||||
G: uint8(g32 >> 8),
|
||||
B: uint8(b32 >> 8),
|
||||
A: uint8(a32 >> 8),
|
||||
}
|
||||
}
|
||||
203
internal/service/skin_renderer/renderer_test.go
Normal file
203
internal/service/skin_renderer/renderer_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package skin_renderer
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// createTestSkin 创建一个测试用的 64x64 皮肤图像
|
||||
func createTestSkin() []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
|
||||
|
||||
// 填充一些测试颜色
|
||||
// 头部区域 (8,8) - (16,16)
|
||||
for y := 8; y < 16; y++ {
|
||||
for x := 8; x < 16; x++ {
|
||||
img.Set(x, y, image.White)
|
||||
}
|
||||
}
|
||||
|
||||
// 躯干区域 (20,20) - (28,32)
|
||||
for y := 20; y < 32; y++ {
|
||||
for x := 20; x < 28; x++ {
|
||||
img.Set(x, y, image.Black)
|
||||
}
|
||||
}
|
||||
|
||||
// 编码为 PNG
|
||||
f, _ := os.CreateTemp("", "test_skin_*.png")
|
||||
defer os.Remove(f.Name())
|
||||
defer f.Close()
|
||||
|
||||
png.Encode(f, img)
|
||||
f.Seek(0, 0)
|
||||
|
||||
data, _ := os.ReadFile(f.Name())
|
||||
return data
|
||||
}
|
||||
|
||||
func TestSkinRenderer_Render(t *testing.T) {
|
||||
skinData := createTestSkin()
|
||||
if len(skinData) == 0 {
|
||||
t.Skip("无法创建测试皮肤")
|
||||
}
|
||||
|
||||
renderer := NewSkinRenderer(7.0, false, -45, -25)
|
||||
result, err := renderer.Render(skinData, false)
|
||||
if err != nil {
|
||||
t.Fatalf("渲染失败: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("渲染结果为空")
|
||||
}
|
||||
|
||||
bounds := result.Bounds()
|
||||
if bounds.Dx() == 0 || bounds.Dy() == 0 {
|
||||
t.Error("渲染结果尺寸为零")
|
||||
}
|
||||
|
||||
t.Logf("渲染结果尺寸: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
func TestSkinRenderer_RenderHeadOnly(t *testing.T) {
|
||||
skinData := createTestSkin()
|
||||
if len(skinData) == 0 {
|
||||
t.Skip("无法创建测试皮肤")
|
||||
}
|
||||
|
||||
renderer := NewSkinRenderer(15.0, true, 45, -25)
|
||||
result, err := renderer.Render(skinData, false)
|
||||
if err != nil {
|
||||
t.Fatalf("渲染头像失败: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("渲染结果为空")
|
||||
}
|
||||
|
||||
bounds := result.Bounds()
|
||||
t.Logf("头像渲染结果尺寸: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
func TestMinecraft_RenderSkin(t *testing.T) {
|
||||
skinData := createTestSkin()
|
||||
if len(skinData) == 0 {
|
||||
t.Skip("无法创建测试皮肤")
|
||||
}
|
||||
|
||||
mc := NewMinecraft()
|
||||
result, err := mc.RenderSkin(skinData, 7.0, false)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderSkin 失败: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("渲染结果为空")
|
||||
}
|
||||
|
||||
bounds := result.Bounds()
|
||||
t.Logf("完整皮肤渲染结果尺寸: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
func TestMinecraft_Render2DAvatar(t *testing.T) {
|
||||
skinData := createTestSkin()
|
||||
if len(skinData) == 0 {
|
||||
t.Skip("无法创建测试皮肤")
|
||||
}
|
||||
|
||||
mc := NewMinecraft()
|
||||
result, err := mc.Render2DAvatar(skinData, 15.0)
|
||||
if err != nil {
|
||||
t.Fatalf("Render2DAvatar 失败: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("渲染结果为空")
|
||||
}
|
||||
|
||||
bounds := result.Bounds()
|
||||
t.Logf("2D头像渲染结果尺寸: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
func TestMinecraft_Render3DAvatar(t *testing.T) {
|
||||
skinData := createTestSkin()
|
||||
if len(skinData) == 0 {
|
||||
t.Skip("无法创建测试皮肤")
|
||||
}
|
||||
|
||||
mc := NewMinecraft()
|
||||
result, err := mc.Render3DAvatar(skinData, 15.0)
|
||||
if err != nil {
|
||||
t.Fatalf("Render3DAvatar 失败: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("渲染结果为空")
|
||||
}
|
||||
|
||||
bounds := result.Bounds()
|
||||
t.Logf("3D头像渲染结果尺寸: %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
func TestPoint_Project(t *testing.T) {
|
||||
p := NewPoint(1, 2, 3)
|
||||
|
||||
var minX, maxX, minY, maxY float64
|
||||
|
||||
// 测试 45 度旋转
|
||||
cosAlpha := 0.9063077870366499 // cos(-25°)
|
||||
sinAlpha := -0.42261826174069944 // sin(-25°)
|
||||
cosOmega := 0.7071067811865476 // cos(45°)
|
||||
sinOmega := 0.7071067811865476 // sin(45°)
|
||||
|
||||
p.Project(cosAlpha, sinAlpha, cosOmega, sinOmega, &minX, &maxX, &minY, &maxY)
|
||||
|
||||
x, y, z := p.GetDestCoord()
|
||||
t.Logf("投影结果: x=%.2f, y=%.2f, z=%.2f", x, y, z)
|
||||
|
||||
if !p.IsProjected() {
|
||||
t.Error("点应该标记为已投影")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolygon_AddToImage(t *testing.T) {
|
||||
// 创建一个简单的正方形多边形
|
||||
p1 := NewPoint(0, 0, 0)
|
||||
p2 := NewPoint(10, 0, 0)
|
||||
p3 := NewPoint(10, 10, 0)
|
||||
p4 := NewPoint(0, 10, 0)
|
||||
|
||||
var minX, maxX, minY, maxY float64
|
||||
p1.Project(1, 0, 1, 0, &minX, &maxX, &minY, &maxY)
|
||||
p2.Project(1, 0, 1, 0, &minX, &maxX, &minY, &maxY)
|
||||
p3.Project(1, 0, 1, 0, &minX, &maxX, &minY, &maxY)
|
||||
p4.Project(1, 0, 1, 0, &minX, &maxX, &minY, &maxY)
|
||||
|
||||
poly := NewPolygon([4]*Point{p1, p2, p3, p4}, color.RGBA{R: 255, G: 255, B: 255, A: 255})
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
|
||||
poly.AddToImage(img, minX, minY, 5.0)
|
||||
|
||||
// 检查是否有像素被绘制
|
||||
hasPixels := false
|
||||
for y := 0; y < 100; y++ {
|
||||
for x := 0; x < 100; x++ {
|
||||
c := img.RGBAAt(x, y)
|
||||
if c.A > 0 {
|
||||
hasPixels = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasPixels {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPixels {
|
||||
t.Error("多边形应该在图像上绘制了像素")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/internal/service/skin_renderer"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
@@ -30,6 +31,7 @@ type textureRenderService struct {
|
||||
cache *database.CacheManager
|
||||
cacheKeys *database.CacheKeyBuilder
|
||||
logger *zap.Logger
|
||||
minecraft *skin_renderer.Minecraft // 3D 渲染器
|
||||
}
|
||||
|
||||
// NewTextureRenderService 创建TextureRenderService实例
|
||||
@@ -45,6 +47,7 @@ func NewTextureRenderService(
|
||||
cache: cacheManager,
|
||||
cacheKeys: database.NewCacheKeyBuilder(""),
|
||||
logger: logger,
|
||||
minecraft: skin_renderer.NewMinecraft(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,18 +147,34 @@ func (s *textureRenderService) RenderAvatar(ctx context.Context, textureID int64
|
||||
return nil, fmt.Errorf("下载纹理失败: %w", err)
|
||||
}
|
||||
|
||||
img, err := png.Decode(bytes.NewReader(textureData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码PNG失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用新的 3D 渲染器
|
||||
var rendered image.Image
|
||||
switch mode {
|
||||
case AvatarMode3D:
|
||||
rendered = s.renderIsometricView(img, texture.IsSlim, size)
|
||||
default:
|
||||
// 使用 Blessing Skin 风格的 3D 头像渲染
|
||||
ratio := float64(size) / 15.0 // 基准比例
|
||||
rendered, err = s.minecraft.Render3DAvatar(textureData, ratio)
|
||||
if err != nil {
|
||||
s.logger.Warn("3D头像渲染失败,回退到2D", zap.Error(err))
|
||||
img, decErr := png.Decode(bytes.NewReader(textureData))
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("解码PNG失败: %w", decErr)
|
||||
}
|
||||
rendered = s.renderHeadView(img, size)
|
||||
}
|
||||
default:
|
||||
// 2D 头像使用新渲染器
|
||||
ratio := float64(size) / 15.0
|
||||
rendered, err = s.minecraft.Render2DAvatar(textureData, ratio)
|
||||
if err != nil {
|
||||
s.logger.Warn("2D头像渲染失败,回退到旧方法", zap.Error(err))
|
||||
img, decErr := png.Decode(bytes.NewReader(textureData))
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("解码PNG失败: %w", decErr)
|
||||
}
|
||||
rendered = s.renderHeadView(img, size)
|
||||
}
|
||||
}
|
||||
|
||||
encoded, err := encodeImage(rendered, format)
|
||||
if err != nil {
|
||||
@@ -457,8 +476,28 @@ func (s *textureRenderService) renderHeadView(img image.Image, size int) image.I
|
||||
return scaleNearest(canvas, size, size)
|
||||
}
|
||||
|
||||
// renderIsometricView 渲染等距视图(改进 3D)
|
||||
// renderIsometricView 渲染等距视图(使用 Blessing Skin 风格的真 3D 渲染)
|
||||
func (s *textureRenderService) renderIsometricView(img image.Image, isSlim bool, size int) image.Image {
|
||||
// 将图像编码为 PNG 数据
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
// 编码失败,回退到简单渲染
|
||||
return s.renderIsometricViewFallback(img, isSlim, size)
|
||||
}
|
||||
|
||||
// 使用新的 3D 渲染器渲染完整皮肤
|
||||
ratio := float64(size) / 32.0 // 基准比例,32 像素高度的皮肤
|
||||
rendered, err := s.minecraft.RenderSkin(buf.Bytes(), ratio, isSlim)
|
||||
if err != nil {
|
||||
// 渲染失败,回退到简单渲染
|
||||
return s.renderIsometricViewFallback(img, isSlim, size)
|
||||
}
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
// renderIsometricViewFallback 等距视图回退方案(简单 2D)
|
||||
func (s *textureRenderService) renderIsometricViewFallback(img image.Image, isSlim bool, size int) image.Image {
|
||||
result := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
|
||||
bgColor := color.RGBA{240, 240, 240, 255}
|
||||
@@ -540,15 +579,31 @@ func abs(x int) int {
|
||||
return x
|
||||
}
|
||||
|
||||
// renderCapeView 渲染披风(等比缩放)
|
||||
// renderCapeView 渲染披风(使用新渲染器)
|
||||
func (s *textureRenderService) renderCapeView(img image.Image, size int) image.Image {
|
||||
// 将图像编码为 PNG 数据
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
// 编码失败,回退到简单缩放
|
||||
srcBounds := img.Bounds()
|
||||
if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 {
|
||||
return img
|
||||
}
|
||||
targetWidth := size * 2
|
||||
targetHeight := size
|
||||
return scaleNearest(img, targetWidth, targetHeight)
|
||||
return scaleNearest(img, size*2, size)
|
||||
}
|
||||
|
||||
// 使用新的披风渲染器
|
||||
rendered, err := s.minecraft.RenderCape(buf.Bytes(), size)
|
||||
if err != nil {
|
||||
// 渲染失败,回退到简单缩放
|
||||
srcBounds := img.Bounds()
|
||||
if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 {
|
||||
return img
|
||||
}
|
||||
return scaleNearest(img, size*2, size)
|
||||
}
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
// composeFrontModel 组合正面分块(含第二层)
|
||||
@@ -617,7 +672,7 @@ func composeBackModel(img image.Image, isSlim bool) *image.RGBA {
|
||||
return canvas
|
||||
}
|
||||
|
||||
// drawLayeredPart 绘制单个分块(基础层+第二层)
|
||||
// drawLayeredPart 绘制单个分块(基础层+第二层,正确的 Alpha 混合)
|
||||
func drawLayeredPart(dst draw.Image, dstRect image.Rectangle, base image.Image, overlay image.Image) {
|
||||
if base == nil {
|
||||
return
|
||||
@@ -625,6 +680,7 @@ func drawLayeredPart(dst draw.Image, dstRect image.Rectangle, base image.Image,
|
||||
dstW := dstRect.Dx()
|
||||
dstH := dstRect.Dy()
|
||||
|
||||
// 绘制基础层
|
||||
for y := 0; y < dstH; y++ {
|
||||
for x := 0; x < dstW; x++ {
|
||||
srcX := base.Bounds().Min.X + x*base.Bounds().Dx()/dstW
|
||||
@@ -633,16 +689,67 @@ func drawLayeredPart(dst draw.Image, dstRect image.Rectangle, base image.Image,
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制第二层(使用 Alpha 混合)
|
||||
if overlay != nil {
|
||||
for y := 0; y < dstH; y++ {
|
||||
for x := 0; x < dstW; x++ {
|
||||
srcX := overlay.Bounds().Min.X + x*overlay.Bounds().Dx()/dstW
|
||||
srcY := overlay.Bounds().Min.Y + y*overlay.Bounds().Dy()/dstH
|
||||
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, overlay.At(srcX, srcY))
|
||||
overlayColor := overlay.At(srcX, srcY)
|
||||
|
||||
// 获取 overlay 的 alpha 值
|
||||
_, _, _, a := overlayColor.RGBA()
|
||||
if a == 0 {
|
||||
// 完全透明,跳过
|
||||
continue
|
||||
}
|
||||
|
||||
if a == 0xFFFF {
|
||||
// 完全不透明,直接覆盖
|
||||
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, overlayColor)
|
||||
} else {
|
||||
// 半透明,进行 Alpha 混合
|
||||
baseColor := dst.At(dstRect.Min.X+x, dstRect.Min.Y+y)
|
||||
blended := alphaBlendColors(baseColor, overlayColor)
|
||||
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, blended)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// alphaBlendColors 执行 Alpha 混合
|
||||
func alphaBlendColors(dst, src color.Color) color.Color {
|
||||
sr, sg, sb, sa := src.RGBA()
|
||||
dr, dg, db, da := dst.RGBA()
|
||||
|
||||
if sa == 0 {
|
||||
return dst
|
||||
}
|
||||
if sa == 0xFFFF {
|
||||
return src
|
||||
}
|
||||
|
||||
// Alpha 混合公式
|
||||
srcA := float64(sa) / 0xFFFF
|
||||
dstA := float64(da) / 0xFFFF
|
||||
outA := srcA + dstA*(1-srcA)
|
||||
|
||||
if outA == 0 {
|
||||
return color.RGBA{}
|
||||
}
|
||||
|
||||
outR := (float64(sr)*srcA + float64(dr)*dstA*(1-srcA)) / outA
|
||||
outG := (float64(sg)*srcA + float64(dg)*dstA*(1-srcA)) / outA
|
||||
outB := (float64(sb)*srcA + float64(db)*dstA*(1-srcA)) / outA
|
||||
|
||||
return color.RGBA64{
|
||||
R: uint16(outR),
|
||||
G: uint16(outG),
|
||||
B: uint16(outB),
|
||||
A: uint16(outA * 0xFFFF),
|
||||
}
|
||||
}
|
||||
|
||||
// safeCrop 安全裁剪(超界返回nil)
|
||||
func safeCrop(img image.Image, rect image.Rectangle) image.Image {
|
||||
|
||||
Reference in New Issue
Block a user