topfans/backend/services/laserCompositor/compositor/grating.go
2026-06-03 22:19:22 +08:00

225 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package compositor
import (
"image"
"image/color"
"math"
)
// SPECTRUM_STOPS 可见光波长到 RGB 的映射点Alan Zucconi CD-ROM Shader 模型)
var spectrumStops = []struct{ Wavelength, R, G, B float64 }{
{400, 0.18, 0.0, 0.47},
{440, 0.0, 0.0, 1.0},
{490, 0.0, 0.8, 0.55},
{510, 0.0, 1.0, 0.0},
{580, 1.0, 1.0, 0.0},
{600, 1.0, 0.5, 0.0},
{650, 1.0, 0.0, 0.0},
{700, 0.4, 0.0, 0.0},
}
// spectralZucconi6 将可见光波长400-700nm映射到 RGB
// 复刻 laserBatchExport.js 中 spectral_zucconi6 的物理模型
func spectralZucconi6(w float64) (r, g, b float64) {
x := clamp01((w - 400) / 300)
c1 := []float64{3.54585104, 2.93225262, 2.41593945}
x1 := []float64{0.69549072, 0.49228336, 0.2769988}
y1 := []float64{0.02312639, 0.15225084, 0.52607955}
c2 := []float64{3.9030714, 3.21182957, 3.96587128}
x2 := []float64{0.11748627, 0.86755042, 0.6607786}
y2 := []float64{0.8489713, 0.88445281, 0.73949448}
b1 := bump3y(c1, x1, y1, x)
b2 := bump3y(c2, x2, y2, x)
return clamp01(b1[0] + b2[0]),
clamp01(b1[1] + b2[1]),
clamp01(b1[2] + b2[2])
}
func bump3y(c, x0, y0 []float64, x float64) [3]float64 {
var result [3]float64
for i := 0; i < 3; i++ {
y := 1 - c[i]*(x-x0[i])*c[i]*(x-x0[i])
result[i] = clamp01(y - y0[i])
}
return result
}
// GratingConfig 光栅渲染参数
type GratingConfig struct {
SheenBandAngle float64 `json:"sheen_band_angle"` // 色带方向(度),对角 135°/120° 等
SheenIntensity float64 `json:"sheen_intensity"` // 彩虹强度 0-1
SheenSpeed float64 `json:"sheen_speed"` // 色带扫过速度
FoilCoverage float64 `json:"foil_coverage"` // foil 覆盖比例 0-1
BackdropTone string `json:"backdrop_tone"` // 底色 hex降级用
}
// DrawDiffractionStripes 绘制彩虹衍射光栅色带(整卡 overlay
// 复刻 laserBatchExport.js 中 drawDiffractionStripes 的物理模型
func DrawDiffractionStripes(dst *image.NRGBA, cfg GratingConfig, variantIndex int) {
intensity := clamp(cfg.SheenIntensity, 0.15, 0.6)
if intensity < 0.05 {
return
}
cw := dst.Bounds().Dx()
ch := dst.Bounds().Dy()
deg := cfg.SheenBandAngle
if deg == 0 {
deg = 135
}
rad := deg * math.Pi / 180
normalX := -math.Sin(rad)
normalY := math.Cos(rad)
tangentX := math.Cos(rad)
tangentY := math.Sin(rad)
// 色带中心相位(不同 variant 偏移不同)
phase := float64(variantIndex) * 0.37 * 0.18
bandCenter := math.Sin(phase * math.Pi * 2) * 0.18
bandHalfWidth := 0.30
diag := math.Sqrt(float64(cw*cw + ch*ch))
halfDiag := diag / 2
rangeVal := diag * 0.55
stepSize := 2.0
for n := -rangeVal; n <= rangeVal; n += stepSize {
phaseAlong := (n/diag + 0.5)
dist := math.Abs(phaseAlong - 0.5 - bandCenter)
if dist > bandHalfWidth+0.04 {
continue
}
bandShape := math.Exp(-(dist * dist) / (bandHalfWidth * bandHalfWidth * 0.18))
// 波长 400-700nm 沿投影位置渐变
wavelength := 400 + ((n + rangeVal) / (rangeVal * 2)) * 300
wClamped := math.Max(400, math.Min(700, wavelength))
r, g, b := spectralZucconi6(wClamped)
a := bandShape * intensity
if a < 0.01 {
continue
}
// 沿切线方向绘制色带
x0 := float64(cw)/2 + tangentX*(-halfDiag) + normalX*n
y0 := float64(ch)/2 + tangentY*(-halfDiag) + normalY*n
x1 := float64(cw)/2 + tangentX*(halfDiag) + normalX*n
y1 := float64(ch)/2 + tangentY*(halfDiag) + normalY*n
drawScreenLine(dst, int(x0), int(y0), int(x1), int(y1), r, g, b, a)
}
}
// drawScreenLine 在屏幕上绘制一条带 screen 混合的线段(带渐变羽化)
func drawScreenLine(dst *image.NRGBA, x0, y0, x1, y1 int, r, g, b, a float64) {
dx := x1 - x0
dy := y1 - y0
steps := max(abs(dx), abs(dy))
if steps == 0 {
steps = 1
}
for i := 0; i <= steps; i++ {
t := float64(i) / float64(steps)
cx := int(float64(x0) + float64(dx)*t)
cy := int(float64(y0) + float64(dy)*t)
// 对垂直于色带方向做 2px 渐变羽化
for offset := -2; offset <= 2; offset++ {
px := cx + offset
if px < 0 || px >= dst.Bounds().Dx() || cy < 0 || cy >= dst.Bounds().Dy() {
continue
}
fade := 1.0 - math.Abs(float64(offset))/3.0
if fade < 0.01 {
continue
}
sc := a * fade
applyScreenPixel(dst, px, cy, r, g, b, sc)
}
}
}
// applyScreenPixel screen 混合: dst = 1 - (1-dst) * (1-src)
func applyScreenPixel(dst *image.NRGBA, x, y int, sr, sg, sb, alpha float64) {
idx := dst.PixOffset(x, y)
dr := float64(dst.Pix[idx]) / 255.0
dg := float64(dst.Pix[idx+1]) / 255.0
db := float64(dst.Pix[idx+2]) / 255.0
// screen 混合
nr := 1 - (1-dr)*(1-sr*alpha)
ng := 1 - (1-dg)*(1-sg*alpha)
nb := 1 - (1-db)*(1-sb*alpha)
dst.Pix[idx] = uint8(clamp01(nr) * 255)
dst.Pix[idx+1] = uint8(clamp01(ng) * 255)
dst.Pix[idx+2] = uint8(clamp01(nb) * 255)
dst.Pix[idx+3] = 255
}
func clamp(v, lo, hi float64) float64 {
return math.Max(lo, math.Min(hi, v))
}
func clamp01(v float64) float64 {
return clamp(v, 0, 1)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// HexToColor 将 hex 颜色字符串 (#RRGGBB) 转换为 color.NRGBA
func HexToColor(hex string) color.NRGBA {
hex = trimPrefix(hex, "#")
if len(hex) != 6 {
return color.NRGBA{R: 168, G: 172, B: 178, A: 255} // 默认冷银
}
r := hexByte(hex[0:2])
g := hexByte(hex[2:4])
b := hexByte(hex[4:6])
return color.NRGBA{R: r, G: g, B: b, A: 255}
}
func hexByte(s string) uint8 {
var v uint8
for _, c := range s {
v *= 16
switch {
case c >= '0' && c <= '9':
v += uint8(c - '0')
case c >= 'A' && c <= 'F':
v += uint8(c-'A') + 10
case c >= 'a' && c <= 'f':
v += uint8(c-'a') + 10
}
}
return v
}
func trimPrefix(s, prefix string) string {
if len(s) >= len(prefix) && s[:len(prefix)] == prefix {
return s[len(prefix):]
}
return s
}