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

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

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,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()