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 }