暂存服务端渲染功能,材质渲染计划迁移至前端
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user