diff --git a/.gitignore b/.gitignore index ee4c55d..197577d 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ dev/ service_coverage .gitignore docs/ +blessing skin材质渲染示例/ diff --git a/internal/service/skin_renderer/cape_renderer.go b/internal/service/skin_renderer/cape_renderer.go new file mode 100644 index 0000000..bf81143 --- /dev/null +++ b/internal/service/skin_renderer/cape_renderer.go @@ -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 +} diff --git a/internal/service/skin_renderer/minecraft.go b/internal/service/skin_renderer/minecraft.go new file mode 100644 index 0000000..899c15c --- /dev/null +++ b/internal/service/skin_renderer/minecraft.go @@ -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 +} diff --git a/internal/service/skin_renderer/point.go b/internal/service/skin_renderer/point.go new file mode 100644 index 0000000..794aa1c --- /dev/null +++ b/internal/service/skin_renderer/point.go @@ -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 +} diff --git a/internal/service/skin_renderer/polygon.go b/internal/service/skin_renderer/polygon.go new file mode 100644 index 0000000..85cfda7 --- /dev/null +++ b/internal/service/skin_renderer/polygon.go @@ -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), + } +} diff --git a/internal/service/skin_renderer/polygons.go b/internal/service/skin_renderer/polygons.go new file mode 100644 index 0000000..9485717 --- /dev/null +++ b/internal/service/skin_renderer/polygons.go @@ -0,0 +1,1080 @@ +package skin_renderer + +// generatePolygons 生成所有部件的多边形 +func (r *SkinRenderer) generatePolygons() { + r.polygons = make(map[string]map[string][]*Polygon) + + // 初始化各部件的面 + parts := []string{"helmet", "head", "torso", "rightArm", "leftArm", "rightLeg", "leftLeg"} + faces := []string{"front", "back", "top", "bottom", "right", "left"} + + for _, part := range parts { + r.polygons[part] = make(map[string][]*Polygon) + for _, face := range faces { + r.polygons[part][face] = []*Polygon{} + } + } + + hd := r.hdRatio + + // 生成头部多边形 + r.generateHeadPolygons(hd) + + // 生成头盔多边形 + r.generateHelmetPolygons(hd) + + if !r.headOnly { + // 生成躯干多边形 + r.generateTorsoPolygons(hd) + + // 生成右臂多边形 + r.generateRightArmPolygons(hd) + + // 生成左臂多边形 + r.generateLeftArmPolygons(hd) + + // 生成右腿多边形 + r.generateRightLegPolygons(hd) + + // 生成左腿多边形 + r.generateLeftLegPolygons(hd) + } +} + +// generateHeadPolygons 生成头部多边形 +func (r *SkinRenderer) generateHeadPolygons(hd int) { + // 创建头部体积点 + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= 8*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 8*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + // 前后面的点 + volumePoints[i][j][-2*hd] = NewPoint(float64(i), float64(j), float64(-2*hd)) + volumePoints[i][j][6*hd] = NewPoint(float64(i), float64(j), float64(6*hd)) + } + } + + // 左右面的点 + for j := 0; j <= 8*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[8*hd][j] == nil { + volumePoints[8*hd][j] = make(map[int]*Point) + } + for k := -2 * hd; k <= 6*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(0, float64(j), float64(k)) + } + if volumePoints[8*hd][j][k] == nil { + volumePoints[8*hd][j][k] = NewPoint(float64(8*hd), float64(j), float64(k)) + } + } + } + + // 上下面的点 + for i := 0; i <= 8*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][8*hd] == nil { + volumePoints[i][8*hd] = make(map[int]*Point) + } + for k := -2 * hd; k <= 6*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i), 0, float64(k)) + } + if volumePoints[i][8*hd][k] == nil { + volumePoints[i][8*hd][k] = NewPoint(float64(i), float64(8*hd), float64(k)) + } + } + } + + // 生成前后面多边形 + for i := 0; i < 8*hd; i++ { + for j := 0; j < 8*hd; j++ { + // 背面 + r.polygons["head"]["back"] = append(r.polygons["head"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][-2*hd], + volumePoints[i+1][j][-2*hd], + volumePoints[i+1][j+1][-2*hd], + volumePoints[i][j+1][-2*hd], + }, + r.getPixelColor((32*hd-1)-i, 8*hd+j), + )) + // 正面 + r.polygons["head"]["front"] = append(r.polygons["head"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][6*hd], + volumePoints[i+1][j][6*hd], + volumePoints[i+1][j+1][6*hd], + volumePoints[i][j+1][6*hd], + }, + r.getPixelColor(8*hd+i, 8*hd+j), + )) + } + } + + // 生成左右面多边形 + for j := 0; j < 8*hd; j++ { + for k := -2 * hd; k < 6*hd; k++ { + // 右面 + r.polygons["head"]["right"] = append(r.polygons["head"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(k+2*hd, 8*hd+j), + )) + // 左面 + r.polygons["head"]["left"] = append(r.polygons["head"]["left"], NewPolygon( + [4]*Point{ + volumePoints[8*hd][j][k], + volumePoints[8*hd][j][k+1], + volumePoints[8*hd][j+1][k+1], + volumePoints[8*hd][j+1][k], + }, + r.getPixelColor((24*hd-1)-k-2*hd, 8*hd+j), + )) + } + } + + // 生成上下面多边形 + for i := 0; i < 8*hd; i++ { + for k := -2 * hd; k < 6*hd; k++ { + // 顶面 + r.polygons["head"]["top"] = append(r.polygons["head"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(8*hd+i, k+2*hd), + )) + // 底面 + r.polygons["head"]["bottom"] = append(r.polygons["head"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][8*hd][k], + volumePoints[i+1][8*hd][k], + volumePoints[i+1][8*hd][k+1], + volumePoints[i][8*hd][k+1], + }, + r.getPixelColor(16*hd+i, 2*hd+k), + )) + } + } +} + +// generateHelmetPolygons 生成头盔/第二层多边形 +func (r *SkinRenderer) generateHelmetPolygons(hd int) { + // 头盔比头部稍大一点 + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= 8*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 8*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + // 稍微放大 + volumePoints[i][j][-2*hd] = NewPoint(float64(i)*9/8-0.5*float64(hd), float64(j)*9/8-0.5*float64(hd), -2.5*float64(hd)) + volumePoints[i][j][6*hd] = NewPoint(float64(i)*9/8-0.5*float64(hd), float64(j)*9/8-0.5*float64(hd), 6.5*float64(hd)) + } + } + + for j := 0; j <= 8*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[8*hd][j] == nil { + volumePoints[8*hd][j] = make(map[int]*Point) + } + for k := -2 * hd; k <= 6*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(-0.5*float64(hd), float64(j)*9/8-0.5*float64(hd), float64(k)*9/8-0.5*float64(hd)) + } + if volumePoints[8*hd][j][k] == nil { + volumePoints[8*hd][j][k] = NewPoint(8.5*float64(hd), float64(j)*9/8-0.5*float64(hd), float64(k)*9/8-0.5*float64(hd)) + } + } + } + + for i := 0; i <= 8*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][8*hd] == nil { + volumePoints[i][8*hd] = make(map[int]*Point) + } + for k := -2 * hd; k <= 6*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i)*9/8-0.5*float64(hd), -0.5*float64(hd), float64(k)*9/8-0.5*float64(hd)) + } + if volumePoints[i][8*hd][k] == nil { + volumePoints[i][8*hd][k] = NewPoint(float64(i)*9/8-0.5*float64(hd), 8.5*float64(hd), float64(k)*9/8-0.5*float64(hd)) + } + } + } + + // 生成前后面多边形(头盔纹理偏移 32*hd) + for i := 0; i < 8*hd; i++ { + for j := 0; j < 8*hd; j++ { + r.polygons["helmet"]["back"] = append(r.polygons["helmet"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][-2*hd], + volumePoints[i+1][j][-2*hd], + volumePoints[i+1][j+1][-2*hd], + volumePoints[i][j+1][-2*hd], + }, + r.getPixelColor(32*hd+(32*hd-1)-i, 8*hd+j), + )) + r.polygons["helmet"]["front"] = append(r.polygons["helmet"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][6*hd], + volumePoints[i+1][j][6*hd], + volumePoints[i+1][j+1][6*hd], + volumePoints[i][j+1][6*hd], + }, + r.getPixelColor(32*hd+8*hd+i, 8*hd+j), + )) + } + } + + for j := 0; j < 8*hd; j++ { + for k := -2 * hd; k < 6*hd; k++ { + r.polygons["helmet"]["right"] = append(r.polygons["helmet"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(32*hd+k+2*hd, 8*hd+j), + )) + r.polygons["helmet"]["left"] = append(r.polygons["helmet"]["left"], NewPolygon( + [4]*Point{ + volumePoints[8*hd][j][k], + volumePoints[8*hd][j][k+1], + volumePoints[8*hd][j+1][k+1], + volumePoints[8*hd][j+1][k], + }, + r.getPixelColor(32*hd+(24*hd-1)-k-2*hd, 8*hd+j), + )) + } + } + + for i := 0; i < 8*hd; i++ { + for k := -2 * hd; k < 6*hd; k++ { + r.polygons["helmet"]["top"] = append(r.polygons["helmet"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(32*hd+8*hd+i, k+2*hd), + )) + r.polygons["helmet"]["bottom"] = append(r.polygons["helmet"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][8*hd][k], + volumePoints[i+1][8*hd][k], + volumePoints[i+1][8*hd][k+1], + volumePoints[i][8*hd][k+1], + }, + r.getPixelColor(32*hd+16*hd+i, 2*hd+k), + )) + } + } +} + +// generateTorsoPolygons 生成躯干多边形 +func (r *SkinRenderer) generateTorsoPolygons(hd int) { + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= 8*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 12*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + volumePoints[i][j][0] = NewPoint(float64(i), float64(j+8*hd), 0) + volumePoints[i][j][4*hd] = NewPoint(float64(i), float64(j+8*hd), float64(4*hd)) + } + } + + for j := 0; j <= 12*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[8*hd][j] == nil { + volumePoints[8*hd][j] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(0, float64(j+8*hd), float64(k)) + } + if volumePoints[8*hd][j][k] == nil { + volumePoints[8*hd][j][k] = NewPoint(float64(8*hd), float64(j+8*hd), float64(k)) + } + } + } + + for i := 0; i <= 8*hd; i++ { + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][12*hd] == nil { + volumePoints[i][12*hd] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i), float64(8*hd), float64(k)) + } + if volumePoints[i][12*hd][k] == nil { + volumePoints[i][12*hd][k] = NewPoint(float64(i), float64(12*hd+8*hd), float64(k)) + } + } + } + + for i := 0; i < 8*hd; i++ { + for j := 0; j < 12*hd; j++ { + r.polygons["torso"]["back"] = append(r.polygons["torso"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor((40*hd-1)-i, 20*hd+j), + )) + r.polygons["torso"]["front"] = append(r.polygons["torso"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(20*hd+i, 20*hd+j), + )) + } + } + + for j := 0; j < 12*hd; j++ { + for k := 0; k < 4*hd; k++ { + r.polygons["torso"]["right"] = append(r.polygons["torso"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(16*hd+k, 20*hd+j), + )) + r.polygons["torso"]["left"] = append(r.polygons["torso"]["left"], NewPolygon( + [4]*Point{ + volumePoints[8*hd][j][k], + volumePoints[8*hd][j][k+1], + volumePoints[8*hd][j+1][k+1], + volumePoints[8*hd][j+1][k], + }, + r.getPixelColor((32*hd-1)-k, 20*hd+j), + )) + } + } + + for i := 0; i < 8*hd; i++ { + for k := 0; k < 4*hd; k++ { + r.polygons["torso"]["top"] = append(r.polygons["torso"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(20*hd+i, 16*hd+k), + )) + r.polygons["torso"]["bottom"] = append(r.polygons["torso"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][12*hd][k], + volumePoints[i+1][12*hd][k], + volumePoints[i+1][12*hd][k+1], + volumePoints[i][12*hd][k+1], + }, + r.getPixelColor(28*hd+i, (20*hd-1)-k), + )) + } + } +} + +// generateRightArmPolygons 生成右臂多边形 +func (r *SkinRenderer) generateRightArmPolygons(hd int) { + armWidth := 4 * hd + if r.isAlex { + armWidth = 3 * hd + } + + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= armWidth; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 12*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + volumePoints[i][j][0] = NewPoint(float64(i-4*hd), float64(j+8*hd), 0) + volumePoints[i][j][4*hd] = NewPoint(float64(i-4*hd), float64(j+8*hd), float64(4*hd)) + } + } + + for j := 0; j <= 12*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[armWidth][j] == nil { + volumePoints[armWidth][j] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(float64(-4*hd), float64(j+8*hd), float64(k)) + } + if volumePoints[armWidth][j][k] == nil { + volumePoints[armWidth][j][k] = NewPoint(float64(armWidth-4*hd), float64(j+8*hd), float64(k)) + } + } + } + + for i := 0; i <= armWidth; i++ { + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][12*hd] == nil { + volumePoints[i][12*hd] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i-4*hd), float64(8*hd), float64(k)) + } + if volumePoints[i][12*hd][k] == nil { + volumePoints[i][12*hd][k] = NewPoint(float64(i-4*hd), float64(12*hd+8*hd), float64(k)) + } + } + } + + // 前后面 + for i := 0; i < armWidth; i++ { + for j := 0; j < 12*hd; j++ { + var backX, frontX int + if r.isAlex { + backX = (51*hd - 1) - i + frontX = 44*hd + i + } else { + backX = (56*hd - 1) - i + frontX = 44*hd + i + } + + r.polygons["rightArm"]["back"] = append(r.polygons["rightArm"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor(backX, 20*hd+j), + )) + r.polygons["rightArm"]["front"] = append(r.polygons["rightArm"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(frontX, 20*hd+j), + )) + } + } + + // 左右面 + for j := 0; j < 12*hd; j++ { + for k := 0; k < 4*hd; k++ { + var rightOffsetX, leftOffsetX int + if r.isAlex { + rightOffsetX = 47 * hd + leftOffsetX = 40 * hd + } else { + rightOffsetX = 40 * hd + leftOffsetX = 52 * hd + } + + r.polygons["rightArm"]["right"] = append(r.polygons["rightArm"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(rightOffsetX+k, 20*hd+j), + )) + r.polygons["rightArm"]["left"] = append(r.polygons["rightArm"]["left"], NewPolygon( + [4]*Point{ + volumePoints[armWidth][j][k], + volumePoints[armWidth][j][k+1], + volumePoints[armWidth][j+1][k+1], + volumePoints[armWidth][j+1][k], + }, + r.getPixelColor((leftOffsetX-1)-k, 20*hd+j), + )) + } + } + + // 上下面 + for i := 0; i < armWidth; i++ { + for k := 0; k < 4*hd; k++ { + var topX, bottomX int + if r.isAlex { + topX = 44*hd + i + bottomX = 47*hd + i + } else { + topX = 44*hd + i + bottomX = 48*hd + i + } + + r.polygons["rightArm"]["top"] = append(r.polygons["rightArm"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(topX, 16*hd+k), + )) + r.polygons["rightArm"]["bottom"] = append(r.polygons["rightArm"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][12*hd][k], + volumePoints[i+1][12*hd][k], + volumePoints[i+1][12*hd][k+1], + volumePoints[i][12*hd][k+1], + }, + r.getPixelColor(bottomX, 16*hd+k), + )) + } + } +} + +// generateLeftArmPolygons 生成左臂多边形 +func (r *SkinRenderer) generateLeftArmPolygons(hd int) { + armWidth := 4 * hd + if r.isAlex { + armWidth = 3 * hd + } + + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= armWidth; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 12*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + volumePoints[i][j][0] = NewPoint(float64(i+8*hd), float64(j+8*hd), 0) + volumePoints[i][j][4*hd] = NewPoint(float64(i+8*hd), float64(j+8*hd), float64(4*hd)) + } + } + + for j := 0; j <= 12*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[armWidth][j] == nil { + volumePoints[armWidth][j] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(float64(8*hd), float64(j+8*hd), float64(k)) + } + if volumePoints[armWidth][j][k] == nil { + volumePoints[armWidth][j][k] = NewPoint(float64(armWidth+8*hd), float64(j+8*hd), float64(k)) + } + } + } + + for i := 0; i <= armWidth; i++ { + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][12*hd] == nil { + volumePoints[i][12*hd] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i+8*hd), float64(8*hd), float64(k)) + } + if volumePoints[i][12*hd][k] == nil { + volumePoints[i][12*hd][k] = NewPoint(float64(i+8*hd), float64(12*hd+8*hd), float64(k)) + } + } + } + + // 前后面 + for i := 0; i < armWidth; i++ { + for j := 0; j < 12*hd; j++ { + var color1, color2 int + + if r.isAlex { + color1 = 43*hd + i + color2 = 36*hd + i + r.polygons["leftArm"]["back"] = append(r.polygons["leftArm"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor(color1, 52*hd+j), + )) + r.polygons["leftArm"]["front"] = append(r.polygons["leftArm"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(color2, 52*hd+j), + )) + } else { + if r.isNewSkinType { + color1 = 47*hd - i // from right to left + color2 = 36*hd + i // from left to right + r.polygons["leftArm"]["back"] = append(r.polygons["leftArm"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor(color1, 52*hd+j), + )) + r.polygons["leftArm"]["front"] = append(r.polygons["leftArm"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(color2, 52*hd+j), + )) + } else { + // 旧版皮肤镜像右臂 + color1 = (56*hd - 1) - ((4*hd - 1) - i) + color2 = 44*hd + ((4*hd - 1) - i) + r.polygons["leftArm"]["back"] = append(r.polygons["leftArm"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor(color1, 20*hd+j), + )) + r.polygons["leftArm"]["front"] = append(r.polygons["leftArm"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(color2, 20*hd+j), + )) + } + } + } + } + + // 左右面 + for j := 0; j < 12*hd; j++ { + for k := 0; k < 4*hd; k++ { + var color1X, color2X, color1Y, color2Y int + + if r.isNewSkinType { + color1X = 32*hd + k + color2X = 43*hd - k + color1Y = 52*hd + j + color2Y = 52*hd + j + } else { + color1X = 40*hd + ((4*hd - 1) - k) + color2X = (52*hd - 1) - ((4*hd - 1) - k) + color1Y = 20*hd + j + color2Y = 20*hd + j + } + + r.polygons["leftArm"]["right"] = append(r.polygons["leftArm"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(color1X, color1Y), + )) + r.polygons["leftArm"]["left"] = append(r.polygons["leftArm"]["left"], NewPolygon( + [4]*Point{ + volumePoints[armWidth][j][k], + volumePoints[armWidth][j][k+1], + volumePoints[armWidth][j+1][k+1], + volumePoints[armWidth][j+1][k], + }, + r.getPixelColor(color2X, color2Y), + )) + } + } + + // 上下面 + for i := 0; i < armWidth; i++ { + for k := 0; k < 4*hd; k++ { + var topX, topY, bottomX, bottomY int + + if r.isAlex { + topX = 36*hd + i + topY = 48*hd + k + bottomX = 39*hd + i + bottomY = 48*hd + k + } else if r.isNewSkinType { + topX = 36*hd + i + topY = 48*hd + k + bottomX = 40*hd + i + bottomY = 48*hd + k + } else { + topX = 44*hd + ((4*hd - 1) - i) + topY = 16*hd + k + bottomX = 48*hd + ((4*hd - 1) - i) + bottomY = (20*hd - 1) - k + } + + r.polygons["leftArm"]["top"] = append(r.polygons["leftArm"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(topX, topY), + )) + r.polygons["leftArm"]["bottom"] = append(r.polygons["leftArm"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][12*hd][k], + volumePoints[i+1][12*hd][k], + volumePoints[i+1][12*hd][k+1], + volumePoints[i][12*hd][k+1], + }, + r.getPixelColor(bottomX, bottomY), + )) + } + } +} + +// generateRightLegPolygons 生成右腿多边形 +func (r *SkinRenderer) generateRightLegPolygons(hd int) { + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= 4*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 12*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + volumePoints[i][j][0] = NewPoint(float64(i), float64(j+20*hd), 0) + volumePoints[i][j][4*hd] = NewPoint(float64(i), float64(j+20*hd), float64(4*hd)) + } + } + + for j := 0; j <= 12*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[4*hd][j] == nil { + volumePoints[4*hd][j] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(0, float64(j+20*hd), float64(k)) + } + if volumePoints[4*hd][j][k] == nil { + volumePoints[4*hd][j][k] = NewPoint(float64(4*hd), float64(j+20*hd), float64(k)) + } + } + } + + for i := 0; i <= 4*hd; i++ { + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][12*hd] == nil { + volumePoints[i][12*hd] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i), float64(20*hd), float64(k)) + } + if volumePoints[i][12*hd][k] == nil { + volumePoints[i][12*hd][k] = NewPoint(float64(i), float64(12*hd+20*hd), float64(k)) + } + } + } + + for i := 0; i < 4*hd; i++ { + for j := 0; j < 12*hd; j++ { + r.polygons["rightLeg"]["back"] = append(r.polygons["rightLeg"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor((16*hd-1)-i, 20*hd+j), + )) + r.polygons["rightLeg"]["front"] = append(r.polygons["rightLeg"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(4*hd+i, 20*hd+j), + )) + } + } + + for j := 0; j < 12*hd; j++ { + for k := 0; k < 4*hd; k++ { + r.polygons["rightLeg"]["right"] = append(r.polygons["rightLeg"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(k, 20*hd+j), + )) + r.polygons["rightLeg"]["left"] = append(r.polygons["rightLeg"]["left"], NewPolygon( + [4]*Point{ + volumePoints[4*hd][j][k], + volumePoints[4*hd][j][k+1], + volumePoints[4*hd][j+1][k+1], + volumePoints[4*hd][j+1][k], + }, + r.getPixelColor((12*hd-1)-k, 20*hd+j), + )) + } + } + + for i := 0; i < 4*hd; i++ { + for k := 0; k < 4*hd; k++ { + r.polygons["rightLeg"]["top"] = append(r.polygons["rightLeg"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(4*hd+i, 16*hd+k), + )) + r.polygons["rightLeg"]["bottom"] = append(r.polygons["rightLeg"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][12*hd][k], + volumePoints[i+1][12*hd][k], + volumePoints[i+1][12*hd][k+1], + volumePoints[i][12*hd][k+1], + }, + r.getPixelColor(8*hd+i, 16*hd+k), + )) + } + } +} + +// generateLeftLegPolygons 生成左腿多边形 +func (r *SkinRenderer) generateLeftLegPolygons(hd int) { + volumePoints := make(map[int]map[int]map[int]*Point) + + for i := 0; i <= 4*hd; i++ { + if volumePoints[i] == nil { + volumePoints[i] = make(map[int]map[int]*Point) + } + for j := 0; j <= 12*hd; j++ { + if volumePoints[i][j] == nil { + volumePoints[i][j] = make(map[int]*Point) + } + volumePoints[i][j][0] = NewPoint(float64(i+4*hd), float64(j+20*hd), 0) + volumePoints[i][j][4*hd] = NewPoint(float64(i+4*hd), float64(j+20*hd), float64(4*hd)) + } + } + + for j := 0; j <= 12*hd; j++ { + if volumePoints[0][j] == nil { + volumePoints[0][j] = make(map[int]*Point) + } + if volumePoints[4*hd][j] == nil { + volumePoints[4*hd][j] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[0][j][k] == nil { + volumePoints[0][j][k] = NewPoint(float64(4*hd), float64(j+20*hd), float64(k)) + } + if volumePoints[4*hd][j][k] == nil { + volumePoints[4*hd][j][k] = NewPoint(float64(4*hd+4*hd), float64(j+20*hd), float64(k)) + } + } + } + + for i := 0; i <= 4*hd; i++ { + if volumePoints[i][0] == nil { + volumePoints[i][0] = make(map[int]*Point) + } + if volumePoints[i][12*hd] == nil { + volumePoints[i][12*hd] = make(map[int]*Point) + } + for k := 0; k <= 4*hd; k++ { + if volumePoints[i][0][k] == nil { + volumePoints[i][0][k] = NewPoint(float64(i+4*hd), float64(20*hd), float64(k)) + } + if volumePoints[i][12*hd][k] == nil { + volumePoints[i][12*hd][k] = NewPoint(float64(i+4*hd), float64(12*hd+20*hd), float64(k)) + } + } + } + + for i := 0; i < 4*hd; i++ { + for j := 0; j < 12*hd; j++ { + var color1X, color2X, color1Y, color2Y int + + if r.isNewSkinType { + color1X = 31*hd - i // from right to left + color2X = 20*hd + i // from left to right + color1Y = 52*hd + j + color2Y = 52*hd + j + } else { + color1X = (16*hd - 1) - ((4*hd - 1) - i) + color2X = 4*hd + ((4*hd - 1) - i) + color1Y = 20*hd + j + color2Y = 20*hd + j + } + + r.polygons["leftLeg"]["back"] = append(r.polygons["leftLeg"]["back"], NewPolygon( + [4]*Point{ + volumePoints[i][j][0], + volumePoints[i+1][j][0], + volumePoints[i+1][j+1][0], + volumePoints[i][j+1][0], + }, + r.getPixelColor(color1X, color1Y), + )) + r.polygons["leftLeg"]["front"] = append(r.polygons["leftLeg"]["front"], NewPolygon( + [4]*Point{ + volumePoints[i][j][4*hd], + volumePoints[i+1][j][4*hd], + volumePoints[i+1][j+1][4*hd], + volumePoints[i][j+1][4*hd], + }, + r.getPixelColor(color2X, color2Y), + )) + } + } + + for j := 0; j < 12*hd; j++ { + for k := 0; k < 4*hd; k++ { + var color1X, color2X, color1Y, color2Y int + + if r.isNewSkinType { + color1X = 16*hd + k // from left to right + color2X = 27*hd - k // from right to left + color1Y = 52*hd + j + color2Y = 52*hd + j + } else { + color1X = ((4*hd - 1) - k) + color2X = (12*hd - 1) - ((4*hd - 1) - k) + color1Y = 20*hd + j + color2Y = 20*hd + j + } + + r.polygons["leftLeg"]["right"] = append(r.polygons["leftLeg"]["right"], NewPolygon( + [4]*Point{ + volumePoints[0][j][k], + volumePoints[0][j][k+1], + volumePoints[0][j+1][k+1], + volumePoints[0][j+1][k], + }, + r.getPixelColor(color1X, color1Y), + )) + r.polygons["leftLeg"]["left"] = append(r.polygons["leftLeg"]["left"], NewPolygon( + [4]*Point{ + volumePoints[4*hd][j][k], + volumePoints[4*hd][j][k+1], + volumePoints[4*hd][j+1][k+1], + volumePoints[4*hd][j+1][k], + }, + r.getPixelColor(color2X, color2Y), + )) + } + } + + for i := 0; i < 4*hd; i++ { + for k := 0; k < 4*hd; k++ { + var topX, topY, bottomX, bottomY int + + if r.isNewSkinType { + topX = 20*hd + i + topY = 48*hd + k + bottomX = 24*hd + i + bottomY = 48*hd + k + } else { + topX = 4*hd + ((4*hd - 1) - i) + topY = 16*hd + k + bottomX = 8*hd + ((4*hd - 1) - i) + bottomY = (20*hd - 1) - k + } + + r.polygons["leftLeg"]["top"] = append(r.polygons["leftLeg"]["top"], NewPolygon( + [4]*Point{ + volumePoints[i][0][k], + volumePoints[i+1][0][k], + volumePoints[i+1][0][k+1], + volumePoints[i][0][k+1], + }, + r.getPixelColor(topX, topY), + )) + r.polygons["leftLeg"]["bottom"] = append(r.polygons["leftLeg"]["bottom"], NewPolygon( + [4]*Point{ + volumePoints[i][12*hd][k], + volumePoints[i+1][12*hd][k], + volumePoints[i+1][12*hd][k+1], + volumePoints[i][12*hd][k+1], + }, + r.getPixelColor(bottomX, bottomY), + )) + } + } +} diff --git a/internal/service/skin_renderer/renderer.go b/internal/service/skin_renderer/renderer.go new file mode 100644 index 0000000..4e70260 --- /dev/null +++ b/internal/service/skin_renderer/renderer.go @@ -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), + } +} diff --git a/internal/service/skin_renderer/renderer_test.go b/internal/service/skin_renderer/renderer_test.go new file mode 100644 index 0000000..a915460 --- /dev/null +++ b/internal/service/skin_renderer/renderer_test.go @@ -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("多边形应该在图像上绘制了像素") + } +} diff --git a/internal/service/texture_render_service.go b/internal/service/texture_render_service.go index aefcb60..8f4a9f4 100644 --- a/internal/service/texture_render_service.go +++ b/internal/service/texture_render_service.go @@ -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,17 +147,33 @@ 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) + // 使用 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: - rendered = s.renderHeadView(img, size) + // 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) @@ -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 { - srcBounds := img.Bounds() - if srcBounds.Dx() == 0 || srcBounds.Dy() == 0 { - return img + // 将图像编码为 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 + } + return scaleNearest(img, size*2, size) } - targetWidth := size * 2 - targetHeight := size - return scaleNearest(img, targetWidth, targetHeight) + + // 使用新的披风渲染器 + 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,17 +689,68 @@ 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 { b := img.Bounds()