225 lines
5.7 KiB
Go
225 lines
5.7 KiB
Go
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
|
||
}
|