809 lines
24 KiB
Go
809 lines
24 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"carrotskin/internal/model"
|
||
"carrotskin/internal/repository"
|
||
"carrotskin/internal/service/skin_renderer"
|
||
"carrotskin/pkg/database"
|
||
"carrotskin/pkg/storage"
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/png"
|
||
"io"
|
||
"net/http"
|
||
"time"
|
||
|
||
"github.com/chai2010/webp"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// textureRenderService TextureRenderService的实现
|
||
type textureRenderService struct {
|
||
textureRepo repository.TextureRepository
|
||
storage *storage.StorageClient
|
||
cache *database.CacheManager
|
||
cacheKeys *database.CacheKeyBuilder
|
||
logger *zap.Logger
|
||
minecraft *skin_renderer.Minecraft // 3D 渲染器
|
||
}
|
||
|
||
// NewTextureRenderService 创建TextureRenderService实例
|
||
func NewTextureRenderService(
|
||
textureRepo repository.TextureRepository,
|
||
storageClient *storage.StorageClient,
|
||
cacheManager *database.CacheManager,
|
||
logger *zap.Logger,
|
||
) TextureRenderService {
|
||
return &textureRenderService{
|
||
textureRepo: textureRepo,
|
||
storage: storageClient,
|
||
cache: cacheManager,
|
||
cacheKeys: database.NewCacheKeyBuilder(""),
|
||
logger: logger,
|
||
minecraft: skin_renderer.NewMinecraft(),
|
||
}
|
||
}
|
||
|
||
// RenderTexture 渲染纹理为预览图
|
||
func (s *textureRenderService) RenderTexture(ctx context.Context, textureID int64, renderType RenderType, size int, format ImageFormat) (*RenderResult, error) {
|
||
// 参数验证
|
||
if size <= 0 || size > 2048 {
|
||
return nil, errors.New("渲染尺寸必须在1到2048之间")
|
||
}
|
||
contentType, err := normalizeFormat(format)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 检查缓存(包含格式)
|
||
cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderType, format), size)
|
||
var cached RenderResult
|
||
if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" {
|
||
return &cached, nil
|
||
}
|
||
|
||
// 获取纹理信息
|
||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取纹理失败: %w", err)
|
||
}
|
||
if texture == nil {
|
||
return nil, errors.New("纹理不存在")
|
||
}
|
||
|
||
// 从对象存储获取纹理文件
|
||
textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("下载纹理失败: %w", err)
|
||
}
|
||
|
||
// 渲染纹理
|
||
renderedImage, _, err := s.RenderTextureFromData(ctx, textureData, renderType, size, format, texture.IsSlim)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("渲染纹理失败: %w", err)
|
||
}
|
||
|
||
// 保存渲染结果到对象存储
|
||
result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, renderType, size, format, renderedImage, contentType)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("保存渲染结果失败: %w", err)
|
||
}
|
||
|
||
// 若源对象有元信息,透传 LastModified/ETag 作为参考
|
||
if srcInfo != nil {
|
||
if result.LastModified.IsZero() {
|
||
result.LastModified = srcInfo.LastModified
|
||
}
|
||
if result.ETag == "" {
|
||
result.ETag = srcInfo.ETag
|
||
}
|
||
}
|
||
|
||
// 缓存结果(1小时)
|
||
if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil {
|
||
s.logger.Warn("缓存渲染结果失败", zap.Error(err))
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// RenderAvatar 渲染头像(支持2D/3D模式)
|
||
func (s *textureRenderService) RenderAvatar(ctx context.Context, textureID int64, size int, mode AvatarMode, format ImageFormat) (*RenderResult, error) {
|
||
if size <= 0 || size > 1024 {
|
||
return nil, errors.New("头像渲染尺寸必须在1到1024之间")
|
||
}
|
||
contentType, err := normalizeFormat(format)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
renderKey := fmt.Sprintf("avatar-%s", mode)
|
||
cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderKey, format), size)
|
||
var cached RenderResult
|
||
if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" {
|
||
return &cached, nil
|
||
}
|
||
|
||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取纹理失败: %w", err)
|
||
}
|
||
if texture == nil {
|
||
return nil, errors.New("纹理不存在")
|
||
}
|
||
if texture.Type != model.TextureTypeSkin {
|
||
return nil, errors.New("仅皮肤纹理支持头像渲染")
|
||
}
|
||
|
||
textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("下载纹理失败: %w", err)
|
||
}
|
||
|
||
// 使用新的 3D 渲染器
|
||
var rendered image.Image
|
||
switch mode {
|
||
case AvatarMode3D:
|
||
// 使用 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 {
|
||
return nil, fmt.Errorf("编码渲染头像失败: %w", err)
|
||
}
|
||
|
||
result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, RenderType(renderKey), size, format, encoded, contentType)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("保存头像渲染失败: %w", err)
|
||
}
|
||
if srcInfo != nil && result.LastModified.IsZero() {
|
||
result.LastModified = srcInfo.LastModified
|
||
}
|
||
if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil {
|
||
s.logger.Warn("缓存头像渲染失败", zap.Error(err))
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// RenderCape 渲染披风
|
||
func (s *textureRenderService) RenderCape(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error) {
|
||
if size <= 0 || size > 2048 {
|
||
return nil, errors.New("披风渲染尺寸必须在1到2048之间")
|
||
}
|
||
contentType, err := normalizeFormat(format)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("cape:%s", format), size)
|
||
var cached RenderResult
|
||
if err := s.cache.Get(ctx, cacheKey, &cached); err == nil && cached.URL != "" {
|
||
return &cached, nil
|
||
}
|
||
|
||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取纹理失败: %w", err)
|
||
}
|
||
if texture == nil {
|
||
return nil, errors.New("纹理不存在")
|
||
}
|
||
if texture.Type != model.TextureTypeCape {
|
||
return nil, errors.New("仅披风纹理支持披风渲染")
|
||
}
|
||
|
||
textureData, srcInfo, err := s.downloadTexture(ctx, texture.URL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("下载纹理失败: %w", err)
|
||
}
|
||
|
||
img, err := png.Decode(bytes.NewReader(textureData))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解码PNG失败: %w", err)
|
||
}
|
||
|
||
rendered := s.renderCapeView(img, size)
|
||
|
||
encoded, err := encodeImage(rendered, format)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("编码披风渲染失败: %w", err)
|
||
}
|
||
|
||
result, err := s.saveRenderToStorage(ctx, textureID, texture.Hash, RenderType("cape"), size, format, encoded, contentType)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("保存披风渲染失败: %w", err)
|
||
}
|
||
if srcInfo != nil && result.LastModified.IsZero() {
|
||
result.LastModified = srcInfo.LastModified
|
||
}
|
||
|
||
if err := s.cache.Set(ctx, cacheKey, result, time.Hour); err != nil {
|
||
s.logger.Warn("缓存披风渲染失败", zap.Error(err))
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// RenderPreview 渲染预览图(类似 Blessing Skin preview)
|
||
func (s *textureRenderService) RenderPreview(ctx context.Context, textureID int64, size int, format ImageFormat) (*RenderResult, error) {
|
||
if size <= 0 || size > 2048 {
|
||
return nil, errors.New("预览渲染尺寸必须在1到2048之间")
|
||
}
|
||
|
||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取纹理失败: %w", err)
|
||
}
|
||
if texture == nil {
|
||
return nil, errors.New("纹理不存在")
|
||
}
|
||
|
||
switch texture.Type {
|
||
case model.TextureTypeCape:
|
||
return s.RenderCape(ctx, textureID, size, format)
|
||
default:
|
||
// 使用改进的等距视图作为默认预览
|
||
return s.RenderTexture(ctx, textureID, RenderTypeIsometric, size, format)
|
||
}
|
||
}
|
||
|
||
// RenderTextureFromData 从原始数据渲染纹理
|
||
func (s *textureRenderService) RenderTextureFromData(ctx context.Context, textureData []byte, renderType RenderType, size int, format ImageFormat, isSlim bool) ([]byte, string, error) {
|
||
// 解码PNG图像
|
||
img, err := png.Decode(bytes.NewReader(textureData))
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("解码PNG失败: %w", err)
|
||
}
|
||
|
||
contentType, err := normalizeFormat(format)
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
|
||
// 根据渲染类型处理图像
|
||
var renderedImage image.Image
|
||
switch renderType {
|
||
case RenderTypeFront:
|
||
renderedImage = s.renderFrontView(img, isSlim, size)
|
||
case RenderTypeBack:
|
||
renderedImage = s.renderBackView(img, isSlim, size)
|
||
case RenderTypeFull:
|
||
renderedImage = s.renderFullView(img, isSlim, size)
|
||
case RenderTypeHead:
|
||
renderedImage = s.renderHeadView(img, size)
|
||
case RenderTypeIsometric:
|
||
renderedImage = s.renderIsometricView(img, isSlim, size)
|
||
default:
|
||
return nil, "", errors.New("不支持的渲染类型")
|
||
}
|
||
|
||
encoded, err := encodeImage(renderedImage, format)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("编码纹理失败: %w", err)
|
||
}
|
||
|
||
return encoded, contentType, nil
|
||
}
|
||
|
||
// GetRenderURL 获取渲染图的URL
|
||
func (s *textureRenderService) GetRenderURL(textureID int64, renderType RenderType, size int, format ImageFormat) string {
|
||
// 构建渲染图的存储路径
|
||
// 格式: renders/{textureID}/{renderType}/{size}.{ext}
|
||
ext := string(format)
|
||
if ext == "" {
|
||
ext = string(ImageFormatPNG)
|
||
}
|
||
return fmt.Sprintf("renders/%d/%s/%d.%s", textureID, renderType, size, ext)
|
||
}
|
||
|
||
// DeleteRenderCache 删除渲染缓存
|
||
func (s *textureRenderService) DeleteRenderCache(ctx context.Context, textureID int64) error {
|
||
// 删除所有渲染类型与格式的缓存
|
||
renderTypes := []RenderType{
|
||
RenderTypeFront, RenderTypeBack, RenderTypeFull, RenderTypeHead,
|
||
RenderTypeIsometric, RenderType("avatar-2d"), RenderType("avatar-3d"), RenderType("cape"),
|
||
}
|
||
formats := []ImageFormat{ImageFormatPNG, ImageFormatWEBP}
|
||
sizes := []int{64, 128, 256, 512}
|
||
|
||
for _, renderType := range renderTypes {
|
||
for _, size := range sizes {
|
||
for _, format := range formats {
|
||
cacheKey := s.cacheKeys.TextureRender(textureID, fmt.Sprintf("%s:%s", renderType, format), size)
|
||
if err := s.cache.Delete(ctx, cacheKey); err != nil {
|
||
s.logger.Warn("删除渲染缓存失败", zap.Error(err))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// downloadTexture 从对象存储下载纹理
|
||
func (s *textureRenderService) downloadTexture(ctx context.Context, textureURL string) ([]byte, *storage.ObjectInfo, error) {
|
||
// 先直接通过 HTTP GET 下载(对公有/匿名可读对象最兼容)
|
||
if resp, httpErr := http.Get(textureURL); httpErr == nil && resp != nil && resp.StatusCode == http.StatusOK {
|
||
defer resp.Body.Close()
|
||
body, readErr := io.ReadAll(resp.Body)
|
||
if readErr == nil {
|
||
var lm time.Time
|
||
if t, parseErr := http.ParseTime(resp.Header.Get("Last-Modified")); parseErr == nil {
|
||
lm = t
|
||
}
|
||
return body, &storage.ObjectInfo{
|
||
Size: resp.ContentLength,
|
||
LastModified: lm,
|
||
ContentType: resp.Header.Get("Content-Type"),
|
||
ETag: resp.Header.Get("ETag"),
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 若 HTTP 失败,再尝试通过对象存储 SDK 访问
|
||
bucket, objectName, err := s.storage.ParseFileURL(textureURL)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("解析纹理URL失败: %w", err)
|
||
}
|
||
|
||
reader, info, err := s.storage.GetObject(ctx, bucket, objectName)
|
||
if err != nil {
|
||
s.logger.Error("获取纹理对象失败",
|
||
zap.String("texture_url", textureURL),
|
||
zap.String("bucket", bucket),
|
||
zap.String("object", objectName),
|
||
zap.Error(err),
|
||
)
|
||
return nil, nil, fmt.Errorf("获取纹理对象失败: bucket=%s object=%s err=%v", bucket, objectName, err)
|
||
}
|
||
defer reader.Close()
|
||
|
||
data, readErr := io.ReadAll(reader)
|
||
if readErr != nil {
|
||
return nil, nil, readErr
|
||
}
|
||
return data, info, nil
|
||
}
|
||
|
||
// saveRenderToStorage 保存渲染结果到对象存储
|
||
func (s *textureRenderService) saveRenderToStorage(ctx context.Context, textureID int64, textureHash string, renderType RenderType, size int, format ImageFormat, imageData []byte, contentType string) (*RenderResult, error) {
|
||
// 获取存储桶
|
||
bucketName, err := s.storage.GetBucket("renders")
|
||
if err != nil {
|
||
// 如果renders桶不存在,使用textures桶
|
||
bucketName, err = s.storage.GetBucket("textures")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取存储桶失败: %w", err)
|
||
}
|
||
}
|
||
|
||
if len(textureHash) < 4 {
|
||
return nil, errors.New("纹理哈希长度不足,无法生成路径")
|
||
}
|
||
|
||
ext := string(format)
|
||
objectName := fmt.Sprintf("renders/%s/%s/%s_%s_%d.%s",
|
||
textureHash[:2], textureHash[2:4], textureHash, renderType, size, ext)
|
||
|
||
// 上传到对象存储
|
||
reader := bytes.NewReader(imageData)
|
||
if err := s.storage.UploadObject(ctx, bucketName, objectName, reader, int64(len(imageData)), contentType); err != nil {
|
||
return nil, fmt.Errorf("上传渲染结果失败: %w", err)
|
||
}
|
||
|
||
etag := sha256.Sum256(imageData)
|
||
result := &RenderResult{
|
||
URL: s.storage.BuildFileURL(bucketName, objectName),
|
||
ContentType: contentType,
|
||
ETag: hex.EncodeToString(etag[:]),
|
||
LastModified: time.Now().UTC(),
|
||
Size: int64(len(imageData)),
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// renderFrontView 渲染正面视图(分块+第二层,含 Alex/Steve)
|
||
func (s *textureRenderService) renderFrontView(img image.Image, isSlim bool, size int) image.Image {
|
||
base := composeFrontModel(img, isSlim)
|
||
return scaleNearest(base, size, size)
|
||
}
|
||
|
||
// renderBackView 渲染背面视图(分块+第二层)
|
||
func (s *textureRenderService) renderBackView(img image.Image, isSlim bool, size int) image.Image {
|
||
base := composeBackModel(img, isSlim)
|
||
return scaleNearest(base, size, size)
|
||
}
|
||
|
||
// renderFullView 渲染全身视图(正面+背面)
|
||
func (s *textureRenderService) renderFullView(img image.Image, isSlim bool, size int) image.Image {
|
||
front := composeFrontModel(img, isSlim)
|
||
back := composeBackModel(img, isSlim)
|
||
|
||
full := image.NewRGBA(image.Rect(0, 0, front.Bounds().Dx()+back.Bounds().Dx(), front.Bounds().Dy()))
|
||
draw.Draw(full, image.Rect(0, 0, front.Bounds().Dx(), front.Bounds().Dy()), front, image.Point{}, draw.Src)
|
||
draw.Draw(full, image.Rect(front.Bounds().Dx(), 0, full.Bounds().Dx(), full.Bounds().Dy()), back, image.Point{}, draw.Src)
|
||
|
||
return scaleNearest(full, size*2, size)
|
||
}
|
||
|
||
// renderHeadView 渲染头像视图(包含第二层帽子)
|
||
func (s *textureRenderService) renderHeadView(img image.Image, size int) image.Image {
|
||
headBase := safeCrop(img, image.Rect(8, 8, 16, 16))
|
||
headOverlay := safeCrop(img, image.Rect(40, 8, 48, 16))
|
||
|
||
if headBase == nil {
|
||
// 返回空白头像
|
||
return scaleNearest(image.NewRGBA(image.Rect(0, 0, 8, 8)), size, size)
|
||
}
|
||
|
||
canvas := image.NewRGBA(image.Rect(0, 0, headBase.Bounds().Dx(), headBase.Bounds().Dy()))
|
||
draw.Draw(canvas, canvas.Bounds(), headBase, headBase.Bounds().Min, draw.Src)
|
||
if headOverlay != nil {
|
||
draw.Draw(canvas, canvas.Bounds(), headOverlay, headOverlay.Bounds().Min, draw.Over)
|
||
}
|
||
|
||
return scaleNearest(canvas, size, size)
|
||
}
|
||
|
||
// 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}
|
||
draw.Draw(result, result.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
|
||
|
||
front := scaleNearest(composeFrontModel(img, isSlim), size/2, size/2)
|
||
|
||
for y := 0; y < front.Bounds().Dy(); y++ {
|
||
for x := 0; x < front.Bounds().Dx(); x++ {
|
||
destX := x + size/4
|
||
destY := y + size/4
|
||
depth := float64(x) / float64(front.Bounds().Dx())
|
||
brightness := 1.0 - depth*0.25
|
||
|
||
c := front.At(x, y)
|
||
r, g, b, a := c.RGBA()
|
||
newR := uint32(float64(r) * brightness)
|
||
newG := uint32(float64(g) * brightness)
|
||
newB := uint32(float64(b) * brightness)
|
||
|
||
if a > 0 {
|
||
result.Set(destX, destY, color.RGBA64{
|
||
R: uint16(newR),
|
||
G: uint16(newG),
|
||
B: uint16(newB),
|
||
A: uint16(a),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
borderColor := color.RGBA{200, 200, 200, 255}
|
||
for i := 0; i < 2; i++ {
|
||
drawLine(result, size/4, size/4, size*3/4, size/4, borderColor)
|
||
drawLine(result, size/4, size*3/4, size*3/4, size*3/4, borderColor)
|
||
drawLine(result, size/4, size/4, size/4, size*3/4, borderColor)
|
||
drawLine(result, size*3/4, size/4, size*3/4, size*3/4, borderColor)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// drawLine 绘制直线
|
||
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {
|
||
dx := abs(x2 - x1)
|
||
dy := abs(y2 - y1)
|
||
sx := -1
|
||
if x1 < x2 {
|
||
sx = 1
|
||
}
|
||
sy := -1
|
||
if y1 < y2 {
|
||
sy = 1
|
||
}
|
||
err := dx - dy
|
||
|
||
for {
|
||
img.Set(x1, y1, c)
|
||
if x1 == x2 && y1 == y2 {
|
||
break
|
||
}
|
||
e2 := 2 * err
|
||
if e2 > -dy {
|
||
err -= dy
|
||
x1 += sx
|
||
}
|
||
if e2 < dx {
|
||
err += dx
|
||
y1 += sy
|
||
}
|
||
}
|
||
}
|
||
|
||
// abs 绝对值
|
||
func abs(x int) int {
|
||
if x < 0 {
|
||
return -x
|
||
}
|
||
return x
|
||
}
|
||
|
||
// 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
|
||
}
|
||
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 组合正面分块(含第二层)
|
||
func composeFrontModel(img image.Image, isSlim bool) *image.RGBA {
|
||
canvas := image.NewRGBA(image.Rect(0, 0, 16, 32))
|
||
armW := 4
|
||
if isSlim {
|
||
armW = 3
|
||
}
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 0, 12, 8),
|
||
safeCrop(img, image.Rect(8, 8, 16, 16)),
|
||
safeCrop(img, image.Rect(40, 8, 48, 16)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 8, 12, 20),
|
||
safeCrop(img, image.Rect(20, 20, 28, 32)),
|
||
safeCrop(img, image.Rect(20, 36, 28, 48)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(0, 8, armW, 20),
|
||
safeCrop(img, image.Rect(44, 20, 48, 32)),
|
||
safeCrop(img, image.Rect(44, 36, 48, 48)))
|
||
drawLayeredPart(canvas, image.Rect(16-armW, 8, 16, 20),
|
||
safeCrop(img, image.Rect(36, 52, 40, 64)),
|
||
safeCrop(img, image.Rect(52, 52, 56, 64)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 20, 8, 32),
|
||
safeCrop(img, image.Rect(4, 20, 8, 32)),
|
||
safeCrop(img, image.Rect(4, 36, 8, 48)))
|
||
drawLayeredPart(canvas, image.Rect(8, 20, 12, 32),
|
||
safeCrop(img, image.Rect(20, 52, 24, 64)),
|
||
safeCrop(img, image.Rect(4, 52, 8, 64)))
|
||
|
||
return canvas
|
||
}
|
||
|
||
// composeBackModel 组合背面分块(含第二层)
|
||
func composeBackModel(img image.Image, isSlim bool) *image.RGBA {
|
||
canvas := image.NewRGBA(image.Rect(0, 0, 16, 32))
|
||
armW := 4
|
||
if isSlim {
|
||
armW = 3
|
||
}
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 0, 12, 8),
|
||
safeCrop(img, image.Rect(24, 8, 32, 16)),
|
||
safeCrop(img, image.Rect(56, 8, 64, 16)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 8, 12, 20),
|
||
safeCrop(img, image.Rect(32, 20, 40, 32)),
|
||
safeCrop(img, image.Rect(32, 36, 40, 48)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(0, 8, armW, 20),
|
||
safeCrop(img, image.Rect(52, 20, 56, 32)),
|
||
safeCrop(img, image.Rect(52, 36, 56, 48)))
|
||
drawLayeredPart(canvas, image.Rect(16-armW, 8, 16, 20),
|
||
safeCrop(img, image.Rect(44, 52, 48, 64)),
|
||
safeCrop(img, image.Rect(60, 52, 64, 64)))
|
||
|
||
drawLayeredPart(canvas, image.Rect(4, 20, 8, 32),
|
||
safeCrop(img, image.Rect(12, 20, 16, 32)),
|
||
safeCrop(img, image.Rect(12, 36, 16, 48)))
|
||
drawLayeredPart(canvas, image.Rect(8, 20, 12, 32),
|
||
safeCrop(img, image.Rect(28, 52, 32, 64)),
|
||
safeCrop(img, image.Rect(12, 52, 16, 64)))
|
||
|
||
return canvas
|
||
}
|
||
|
||
// drawLayeredPart 绘制单个分块(基础层+第二层,正确的 Alpha 混合)
|
||
func drawLayeredPart(dst draw.Image, dstRect image.Rectangle, base image.Image, overlay image.Image) {
|
||
if base == nil {
|
||
return
|
||
}
|
||
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
|
||
srcY := base.Bounds().Min.Y + y*base.Bounds().Dy()/dstH
|
||
dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, base.At(srcX, srcY))
|
||
}
|
||
}
|
||
|
||
// 绘制第二层(使用 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
|
||
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()
|
||
if rect.Min.X < 0 || rect.Min.Y < 0 || rect.Max.X > b.Max.X || rect.Max.Y > b.Max.Y {
|
||
return nil
|
||
}
|
||
subImg := image.NewRGBA(rect)
|
||
draw.Draw(subImg, rect, img, rect.Min, draw.Src)
|
||
return subImg
|
||
}
|
||
|
||
// scaleNearest 最近邻缩放
|
||
func scaleNearest(src image.Image, targetW, targetH int) *image.RGBA {
|
||
dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
|
||
srcBounds := src.Bounds()
|
||
for y := 0; y < targetH; y++ {
|
||
for x := 0; x < targetW; x++ {
|
||
srcX := srcBounds.Min.X + x*srcBounds.Dx()/targetW
|
||
srcY := srcBounds.Min.Y + y*srcBounds.Dy()/targetH
|
||
dst.Set(x, y, src.At(srcX, srcY))
|
||
}
|
||
}
|
||
return dst
|
||
}
|
||
|
||
// normalizeFormat 校验输出格式
|
||
func normalizeFormat(format ImageFormat) (string, error) {
|
||
if format == "" {
|
||
format = ImageFormatPNG
|
||
}
|
||
switch format {
|
||
case ImageFormatPNG:
|
||
return "image/png", nil
|
||
case ImageFormatWEBP:
|
||
return "image/webp", nil
|
||
default:
|
||
return "", fmt.Errorf("不支持的输出格式: %s", format)
|
||
}
|
||
}
|
||
|
||
// encodeImage 将图像编码为指定格式
|
||
func encodeImage(img image.Image, format ImageFormat) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
switch format {
|
||
case ImageFormatWEBP:
|
||
if err := webp.Encode(&buf, img, &webp.Options{Lossless: true}); err != nil {
|
||
return nil, err
|
||
}
|
||
default:
|
||
if err := png.Encode(&buf, img); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return buf.Bytes(), nil
|
||
}
|