暂存服务端渲染功能,材质渲染计划迁移至前端

This commit is contained in:
2025-12-08 17:40:28 +08:00
parent 63ca7eff0d
commit 399e6f096f
9 changed files with 2528 additions and 17 deletions

1
.gitignore vendored
View File

@@ -109,3 +109,4 @@ dev/
service_coverage
.gitignore
docs/
blessing skin材质渲染示例/

View 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
}

View 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
}

View 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
}

View 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),
}
}

File diff suppressed because it is too large Load Diff

View 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),
}
}

View 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("多边形应该在图像上绘制了像素")
}
}

View File

@@ -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,15 +689,66 @@ 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