518 lines
18 KiB
JavaScript
518 lines
18 KiB
JavaScript
/**
|
||
* 镭射卡预览专用 WebGL1 渲染器(全息光栅风格单 Pass)。
|
||
* 预览与导出分离:导出仍走 2D Canvas;本模块仅用于页面内实时预览。
|
||
*
|
||
* 依赖:WebGL1 + OES_texture_float_linear 非必须;仅用 RGBA8 纹理。
|
||
*/
|
||
|
||
import { hexToRgb } from './laserGrating.js'
|
||
|
||
const VERT_SRC = `
|
||
attribute vec2 a_uv;
|
||
varying vec2 v_uv;
|
||
void main() {
|
||
v_uv = a_uv;
|
||
gl_Position = vec4(a_uv * 2.0 - 1.0, 0.0, 1.0);
|
||
}
|
||
`
|
||
|
||
const FRAG_SRC = `
|
||
precision highp float;
|
||
varying vec2 v_uv;
|
||
|
||
uniform vec2 u_resolution;
|
||
uniform float u_time;
|
||
uniform vec4 u_photoRect;
|
||
uniform float u_corner;
|
||
uniform vec3 u_view;
|
||
uniform vec3 u_lightDir;
|
||
uniform float u_styleHue;
|
||
uniform float u_frostIntensity;
|
||
uniform float u_flowSpeed;
|
||
uniform float u_stripeAngle;
|
||
uniform float u_stripeIntensity;
|
||
uniform float u_backdropToneR;
|
||
uniform float u_backdropToneG;
|
||
uniform float u_backdropToneB;
|
||
uniform float u_foilCoverage;
|
||
uniform vec4 u_personEdge;
|
||
|
||
uniform sampler2D u_photo;
|
||
|
||
// ============================================================
|
||
// 物理波长→RGB (spectral_zucconi6)
|
||
// ============================================================
|
||
vec3 bump3y(vec3 x, vec3 yoffset) {
|
||
vec3 y = 1.0 - x * x;
|
||
y = clamp(y - yoffset, 0.0, 1.0);
|
||
return y;
|
||
}
|
||
|
||
vec3 spectral_zucconi6(float w) {
|
||
float x = clamp((w - 400.0) / 300.0, 0.0, 1.0);
|
||
const vec3 c1 = vec3(3.54585104, 2.93225262, 2.41593945);
|
||
const vec3 x1 = vec3(0.69549072, 0.49228336, 0.27699880);
|
||
const vec3 y1 = vec3(0.02312639, 0.15225084, 0.52607955);
|
||
const vec3 c2 = vec3(3.90307140, 3.21182957, 3.96587128);
|
||
const vec3 x2 = vec3(0.11748627, 0.86755042, 0.66077860);
|
||
const vec3 y2 = vec3(0.84897130, 0.88445281, 0.73949448);
|
||
return bump3y(c1 * (x - x1), y1) + bump3y(c2 * (x - x2), y2);
|
||
}
|
||
|
||
float hash(vec2 p) {
|
||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||
}
|
||
|
||
float roundedBox(vec2 p, vec2 b, float r) {
|
||
vec2 q = abs(p) - b + r;
|
||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
||
}
|
||
|
||
vec4 samplePhoto(vec2 uv) {
|
||
vec2 pRaw = (uv - u_photoRect.xy) / u_photoRect.zw;
|
||
vec2 p = clamp(pRaw, 0.0, 1.0);
|
||
return texture2D(u_photo, p);
|
||
}
|
||
|
||
float photoInside(vec2 uv) {
|
||
vec2 pRaw = (uv - u_photoRect.xy) / u_photoRect.zw;
|
||
return step(0.0, pRaw.x) * step(pRaw.x, 1.0) * step(0.0, pRaw.y) * step(pRaw.y, 1.0);
|
||
}
|
||
|
||
// ============================================================
|
||
// 常驻双彩虹色带(实体镭射卡在静态时刻也可见)
|
||
// 沿光栅法线方向分布 2 个高斯峰 + 时间微动
|
||
// ============================================================
|
||
vec3 computeSheenBand(vec2 uv, vec3 viewDir, vec3 lightDir,
|
||
float bandAngle, float intensity, float timeMs) {
|
||
float ang = bandAngle * 0.01745329252;
|
||
vec2 normal = vec2(-sin(ang), cos(ang));
|
||
float proj = dot(uv - 0.5, normal);
|
||
|
||
// 时间流动:色带在法线方向上缓慢漂移
|
||
float flow = timeMs * 0.00018 * max(u_flowSpeed, 0.15);
|
||
|
||
// 双高斯:2 条固定分布的色带(center1=-0.18, center2=+0.18)
|
||
// 每条带宽 ~0.18(占整张卡对角线比例)
|
||
float c1 = -0.18;
|
||
float c2 = +0.18;
|
||
float dist1 = abs(proj - c1);
|
||
float dist2 = abs(proj - c2);
|
||
float band1 = exp(-(dist1 * dist1) / 0.022); // 0.022 ~ 1/45
|
||
float band2 = exp(-(dist2 * dist2) / 0.022);
|
||
|
||
// 沿切线方向,每条色带内部波长 400-700nm 渐变
|
||
vec2 tangent = vec2(cos(ang), sin(ang));
|
||
float along = dot(uv - 0.5, tangent);
|
||
|
||
// 每条色带颜色:wave 1 = 红→紫(沿 +tangent),wave 2 = 紫→红(沿 -tangent)
|
||
// 加上 flow 让色带略动
|
||
float hue1 = fract(along * 0.6 + flow * 0.5);
|
||
float hue2 = fract(-along * 0.6 + flow * 0.5 + 0.5);
|
||
|
||
// wavelength hue1=0 → 700nm(紫), hue1=1 → 400nm(红)
|
||
float w1 = mix(700.0, 400.0, hue1);
|
||
float w2 = mix(400.0, 700.0, hue2);
|
||
|
||
vec3 c1col = spectral_zucconi6(w1);
|
||
vec3 c2col = spectral_zucconi6(w2);
|
||
|
||
return (c1col * band1 + c2col * band2) * intensity;
|
||
}
|
||
|
||
// ============================================================
|
||
// 人像边缘彩虹圈:沿抠图 alpha 边界绘制彩虹光晕
|
||
// 这是「实体镭射卡最显眼的视觉信号」
|
||
// 思路:pc.a 边缘的 gradient magnitude(sobel-like)就是边界强度
|
||
// ============================================================
|
||
vec3 computePersonEdgeHalo(vec2 uv, vec3 lightDir) {
|
||
// 3x3 邻域采样 alpha
|
||
vec2 texel = vec2(1.0) / u_resolution;
|
||
float aC = samplePhoto(uv).a;
|
||
float aL = samplePhoto(uv - vec2(texel.x, 0.0)).a;
|
||
float aR = samplePhoto(uv + vec2(texel.x, 0.0)).a;
|
||
float aT = samplePhoto(uv - vec2(0.0, texel.y)).a;
|
||
float aB = samplePhoto(uv + vec2(0.0, texel.y)).a;
|
||
|
||
// alpha 梯度(人像边缘 = 大梯度)
|
||
float gx = (aR - aL) * 0.5;
|
||
float gy = (aB - aT) * 0.5;
|
||
float edge = sqrt(gx * gx + gy * gy);
|
||
|
||
// 抑制图外的假边缘:图内 alpha > 0.5 区域 OR 图外
|
||
float inside = photoInside(uv);
|
||
// 在 alpha 边界外侧 ~1-2 px 处也应有 halo,所以向外延伸
|
||
float pcC = samplePhoto(uv).a * inside;
|
||
// 让 halo 出现在 alpha ~0.1~0.6 范围(边缘 + 外溢 1-2px)
|
||
float haloBand = smoothstep(0.05, 0.4, aC) * (1.0 - smoothstep(0.55, 0.95, aC));
|
||
float haloEdge = edge * 8.0 + haloBand * 0.6;
|
||
|
||
// 沿边界法线方向映射彩虹色相
|
||
// 边界法线 ≈ (gx, gy)(指向 alpha 增大方向,即从外指向人像)
|
||
float ang = atan(gy, gx);
|
||
float hue = fract(ang / 6.28318 + dot(lightDir.xy, vec2(0.3, 0.5)) * 0.2 + 0.5);
|
||
float wavelength = mix(400.0, 700.0, hue);
|
||
vec3 haloColor = spectral_zucconi6(wavelength);
|
||
|
||
return haloColor * haloEdge * 1.4;
|
||
}
|
||
|
||
// ============================================================
|
||
// 银粉星点散点层:高频亮点模拟金属膜的细反光
|
||
// ============================================================
|
||
vec3 computeSilverSparkle(vec2 uv, float density) {
|
||
// 三层不同频率的 hash 网格:避免规律性
|
||
float total = 0.0;
|
||
vec3 totalColor = vec3(0.0);
|
||
|
||
// Layer A:密集细点(每 8x8 像素 1 个)
|
||
vec2 gA = floor(uv * u_resolution / 8.0);
|
||
float hA = hash(gA + 13.0);
|
||
if (hA > 0.985) {
|
||
vec2 cellUV = fract(uv * u_resolution / 8.0) - 0.5;
|
||
float dA = length(cellUV);
|
||
float starA = smoothstep(0.45, 0.0, dA) * 0.8;
|
||
total = max(total, starA);
|
||
// 亮点颜色 = 接近白 + 微紫/微蓝偏移
|
||
totalColor += vec3(0.95, 0.96, 1.0) * starA;
|
||
}
|
||
|
||
// Layer B:中等亮点(每 16x16 像素 1 个)
|
||
vec2 gB = floor(uv * u_resolution / 16.0);
|
||
float hB = hash(gB + 271.0);
|
||
if (hB > 0.992) {
|
||
vec2 cellUV = fract(uv * u_resolution / 16.0) - 0.5;
|
||
float dB = length(cellUV);
|
||
float starB = smoothstep(0.42, 0.0, dB) * 0.95;
|
||
// 偶尔有彩色星点(彩虹光)
|
||
float colorRoll = hash(gB + 777.0);
|
||
vec3 starCol = colorRoll > 0.7
|
||
? spectral_zucconi6(mix(420.0, 680.0, hash(gB + 333.0)))
|
||
: vec3(1.0, 1.0, 1.0);
|
||
if (starB > total) {
|
||
total = starB;
|
||
totalColor = starCol * starB;
|
||
} else {
|
||
totalColor += starCol * starB * 0.6;
|
||
}
|
||
}
|
||
|
||
return totalColor * density;
|
||
}
|
||
|
||
// ============================================================
|
||
// 金属反光梯度:对角方向的高光带,让卡面有"银的反射"质感
|
||
// ============================================================
|
||
float computeMetallicSheen(vec2 uv, float bandAngle) {
|
||
float ang = bandAngle * 0.01745329252;
|
||
vec2 normal = vec2(-sin(ang), cos(ang));
|
||
float proj = dot(uv - 0.5, normal);
|
||
|
||
// 一条柔和的对角反光(中心 0.0, 宽度 0.4)
|
||
float dist = abs(proj);
|
||
float main = exp(-(dist * dist) / 0.10);
|
||
|
||
// 加上次级反光(小一号,-0.3 处)
|
||
float side = exp(-((dist - 0.32) * (dist - 0.32)) / 0.06) * 0.5;
|
||
|
||
return main + side;
|
||
}
|
||
|
||
void main() {
|
||
vec2 uv = v_uv;
|
||
vec2 pN = (uv - 0.5) * vec2(u_resolution.x / u_resolution.y, 1.0);
|
||
float rb = roundedBox(pN, vec2(0.5 * u_resolution.x / u_resolution.y, 0.5), u_corner);
|
||
if (rb > 0.002) discard;
|
||
|
||
// === 1. 金属底色 ===
|
||
vec3 baseBg = vec3(u_backdropToneR, u_backdropToneG, u_backdropToneB);
|
||
// 把纯色底加一个非常轻的"金属反光梯度"(让白底变银底)
|
||
float metalShine = computeMetallicSheen(uv, u_stripeAngle);
|
||
vec3 silverBase = baseBg + vec3(0.20, 0.18, 0.16) * metalShine * 0.35;
|
||
silverBase = clamp(silverBase, 0.0, 1.0);
|
||
|
||
// === 2. 人像合成 ===
|
||
vec4 pc = samplePhoto(uv);
|
||
float photoMask = photoInside(uv);
|
||
float personAlpha = pc.a * photoMask;
|
||
// 人像用原图,背板用银底
|
||
vec3 metalWithPhoto = mix(silverBase, pc.rgb, personAlpha);
|
||
|
||
// === 3. 视角 ===
|
||
vec3 viewDir = normalize(vec3(u_view.x, u_view.y, 0.7));
|
||
vec3 lightDir = normalize(vec3(0.8, 0.6, 0.5));
|
||
|
||
// === 4. 彩虹色带(双条常驻)===
|
||
vec3 sheenBand = computeSheenBand(
|
||
uv, viewDir, lightDir,
|
||
u_stripeAngle, u_stripeIntensity, u_time
|
||
);
|
||
|
||
// === 5. 人像边缘彩虹圈(P0 核心)===
|
||
vec3 personHalo = computePersonEdgeHalo(uv, lightDir);
|
||
|
||
// === 6. 银粉星点 ===
|
||
float sparkleDensity = mix(0.4, 0.9, clamp(u_frostIntensity / 100.0, 0.0, 1.0));
|
||
vec3 sparkle = computeSilverSparkle(uv, sparkleDensity);
|
||
|
||
// === 7. 合成:背景 foil + 边缘 halo + 星点 ===
|
||
// foil 覆盖:背景 ~0.85,人物上 0.3(让人像也有轻微反光)
|
||
float foilWeight = mix(0.85, 0.30, personAlpha);
|
||
|
||
// screen 混合色带和 halo
|
||
vec3 sheenTotal = clamp(sheenBand, 0.0, 1.0);
|
||
vec3 screenBlend = 1.0 - (1.0 - metalWithPhoto) * (1.0 - sheenTotal);
|
||
vec3 col = mix(metalWithPhoto, screenBlend, foilWeight);
|
||
|
||
// 边缘彩虹圈:additive(不挡人物)
|
||
col += personHalo * (1.0 - personAlpha * 0.5) * 0.7;
|
||
|
||
// 银粉星点:additive
|
||
col += sparkle * 0.85;
|
||
|
||
// === 8. 磨砂(保留非常轻的银粉底纹)===
|
||
float frostAmt = clamp(u_frostIntensity / 100.0, 0.0, 1.0) * 0.4;
|
||
float frost = hash(floor(uv * u_resolution * 2.4));
|
||
col = mix(col, col * (0.96 + frost * 0.08), frostAmt * 0.20);
|
||
|
||
// === 9. 极弱 vignette ===
|
||
float vign = 1.0 - pow(length(pN) / 0.72, 2.2) * 0.06;
|
||
col *= vign;
|
||
|
||
gl_FragColor = vec4(col, 1.0);
|
||
}
|
||
`
|
||
|
||
function compileShader(gl, type, src) {
|
||
const sh = gl.createShader(type)
|
||
gl.shaderSource(sh, src)
|
||
gl.compileShader(sh)
|
||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||
const err = gl.getShaderInfoLog(sh)
|
||
gl.deleteShader(sh)
|
||
throw new Error(err || 'shader compile')
|
||
}
|
||
return sh
|
||
}
|
||
|
||
function createProgram(gl, vs, fs) {
|
||
const prog = gl.createProgram()
|
||
gl.attachShader(prog, compileShader(gl, gl.VERTEX_SHADER, vs))
|
||
gl.attachShader(prog, compileShader(gl, gl.FRAGMENT_SHADER, fs))
|
||
gl.linkProgram(prog)
|
||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||
const err = gl.getProgramInfoLog(prog)
|
||
gl.deleteProgram(prog)
|
||
throw new Error(err || 'program link')
|
||
}
|
||
return prog
|
||
}
|
||
|
||
function orthoTexImage(gl, tex, source) {
|
||
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||
gl.bindTexture(gl.TEXTURE_2D, null)
|
||
}
|
||
|
||
/**
|
||
* @param {WebGLRenderingContext} gl
|
||
* @param {{ width: number, height: number, canvas?: any }} opts
|
||
*/
|
||
export function createLaserPreviewWebgl(gl, opts) {
|
||
const w = Math.max(2, Math.floor(opts.width))
|
||
const h = Math.max(2, Math.floor(opts.height))
|
||
|
||
const program = createProgram(gl, VERT_SRC, FRAG_SRC)
|
||
const loc = {
|
||
a_uv: gl.getAttribLocation(program, 'a_uv'),
|
||
u_resolution: gl.getUniformLocation(program, 'u_resolution'),
|
||
u_time: gl.getUniformLocation(program, 'u_time'),
|
||
u_photoRect: gl.getUniformLocation(program, 'u_photoRect'),
|
||
u_corner: gl.getUniformLocation(program, 'u_corner'),
|
||
u_view: gl.getUniformLocation(program, 'u_view'),
|
||
u_lightDir: gl.getUniformLocation(program, 'u_lightDir'),
|
||
u_styleHue: gl.getUniformLocation(program, 'u_styleHue'),
|
||
u_frostIntensity: gl.getUniformLocation(program, 'u_frostIntensity'),
|
||
u_flowSpeed: gl.getUniformLocation(program, 'u_flowSpeed'),
|
||
u_stripeAngle: gl.getUniformLocation(program, 'u_stripeAngle'),
|
||
u_stripeIntensity: gl.getUniformLocation(program, 'u_stripeIntensity'),
|
||
u_backdropToneR: gl.getUniformLocation(program, 'u_backdropToneR'),
|
||
u_backdropToneG: gl.getUniformLocation(program, 'u_backdropToneG'),
|
||
u_backdropToneB: gl.getUniformLocation(program, 'u_backdropToneB'),
|
||
u_foilCoverage: gl.getUniformLocation(program, 'u_foilCoverage'),
|
||
u_personEdge: gl.getUniformLocation(program, 'u_personEdge'),
|
||
u_photo: gl.getUniformLocation(program, 'u_photo'),
|
||
}
|
||
|
||
const buf = gl.createBuffer()
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
||
const quad = new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1])
|
||
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW)
|
||
|
||
const texPhoto = gl.createTexture()
|
||
let photoReady = false
|
||
|
||
function bindUnit(unit, tex, loc) {
|
||
gl.activeTexture(gl.TEXTURE0 + unit)
|
||
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||
gl.uniform1i(loc, unit)
|
||
}
|
||
|
||
return {
|
||
gl,
|
||
w,
|
||
h,
|
||
program,
|
||
/** 释放 GPU 资源 */
|
||
destroy() {
|
||
gl.deleteProgram(program)
|
||
gl.deleteBuffer(buf)
|
||
gl.deleteTexture(texPhoto)
|
||
},
|
||
/** @param {HTMLImageElement|ImageBitmap|HTMLCanvasElement} img */
|
||
uploadPhoto(img) {
|
||
if (!img || !img.width) return
|
||
orthoTexImage(gl, texPhoto, img)
|
||
photoReady = true
|
||
},
|
||
/** @deprecated 预览改用纯色底,保留空实现兼容旧调用 */
|
||
uploadBackdrop() {},
|
||
hasPhoto() {
|
||
return photoReady
|
||
},
|
||
hasBackdrop() {
|
||
return false
|
||
},
|
||
/**
|
||
* @param {number} timeMs
|
||
* @param {object} u
|
||
* @param {{ dx: number, dy: number, dw: number, dh: number }} u.photoRect 像素 cover 矩形
|
||
* @param {object} [u.backRect]
|
||
* @param {boolean} [u.hasBackdrop]
|
||
* @param {number} [u.cornerRadiusPx]
|
||
* @param {number} [u.vivid] 0.75~1.35
|
||
* @param {number} [u.strength] 0.35~1.2
|
||
* @param {number} [u.styleHue] beam 风格色相偏置 0~360
|
||
*/
|
||
render(timeMs, u) {
|
||
if (!photoReady) return
|
||
const pr = u.photoRect
|
||
const cw = u.canvasW || w
|
||
const ch = u.canvasH || h
|
||
const nx = pr.dx / cw
|
||
const ny = pr.dy / ch
|
||
const nw = pr.dw / cw
|
||
const nh = pr.dh / ch
|
||
|
||
const rPx = typeof u.cornerRadiusPx === 'number' ? u.cornerRadiusPx : Math.min(32, cw * 0.065, ch * 0.048)
|
||
const cornerUv = rPx / Math.min(cw, ch)
|
||
|
||
const t = (timeMs || 0) * 0.001
|
||
const speed = typeof u.animSpeed === 'number' ? u.animSpeed : 1
|
||
const vx = Math.sin(t * 0.42 * speed) * 0.7 + Math.sin(t * 0.17 * speed) * 0.35
|
||
const vy = Math.cos(t * 0.31 * speed) * 0.55 + Math.cos(t * 0.21 * speed) * 0.28
|
||
const vz = t * 0.55 * speed
|
||
|
||
const cfg = u.gratingConfig || {}
|
||
const [br, bg, bb] = hexToRgb(cfg.backdropTone)
|
||
|
||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||
gl.disable(gl.DEPTH_TEST)
|
||
gl.disable(gl.STENCIL_TEST)
|
||
gl.clearColor(0, 0, 0, 0)
|
||
gl.clear(gl.COLOR_BUFFER_BIT)
|
||
|
||
gl.useProgram(program)
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
||
gl.enableVertexAttribArray(loc.a_uv)
|
||
gl.vertexAttribPointer(loc.a_uv, 2, gl.FLOAT, false, 0, 0)
|
||
|
||
gl.uniform2f(loc.u_resolution, cw, ch)
|
||
gl.uniform1f(loc.u_time, timeMs || 0)
|
||
gl.uniform4f(loc.u_photoRect, nx, ny, nw, nh)
|
||
gl.uniform1f(loc.u_corner, cornerUv)
|
||
gl.uniform3f(loc.u_view, vx, vy, vz)
|
||
gl.uniform3f(loc.u_lightDir, 0.8, 0.6, 0.5)
|
||
gl.uniform1f(loc.u_styleHue, typeof u.styleHue === 'number' ? u.styleHue : 0)
|
||
gl.uniform1f(loc.u_frostIntensity, typeof u.frostIntensity === 'number' ? u.frostIntensity : 8)
|
||
gl.uniform1f(loc.u_flowSpeed, typeof u.flowSpeed === 'number' ? u.flowSpeed : 0.4)
|
||
gl.uniform1f(loc.u_stripeAngle, cfg.stripeAngle ?? 135)
|
||
gl.uniform1f(loc.u_stripeIntensity, cfg.stripeIntensity ?? 0.32)
|
||
gl.uniform1f(loc.u_backdropToneR, br / 255)
|
||
gl.uniform1f(loc.u_backdropToneG, bg / 255)
|
||
gl.uniform1f(loc.u_backdropToneB, bb / 255)
|
||
gl.uniform1f(loc.u_foilCoverage, typeof cfg.foilCoverage === 'number' ? cfg.foilCoverage : 0.7)
|
||
|
||
// 人物边缘参数(目前 shader 用 alpha 梯度自动检测,但保留接口供扩展)
|
||
const pex = typeof u.personEdgeX === 'number' ? u.personEdgeX : 0.5
|
||
const pey = typeof u.personEdgeY === 'number' ? u.personEdgeY : 0.5
|
||
const peiR = typeof u.personEdgeInner === 'number' ? u.personEdgeInner : 0.28
|
||
const peoR = typeof u.personEdgeOuter === 'number' ? u.personEdgeOuter : 0.42
|
||
gl.uniform4f(loc.u_personEdge, pex, pey, peiR, peoR)
|
||
|
||
bindUnit(0, texPhoto, loc.u_photo)
|
||
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
||
gl.bindTexture(gl.TEXTURE_2D, null)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从本地路径或 URL 加载图片;App/H5 用 Image,小程序等用 canvas.createImage。
|
||
* @param {string} src
|
||
* @param {any} [canvasForCreateImage] type=webgl 的 canvas node
|
||
*/
|
||
export function loadTextureImage(src, canvasForCreateImage) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!src) {
|
||
reject(new Error('empty src'))
|
||
return
|
||
}
|
||
const c = canvasForCreateImage
|
||
if (c && typeof c.createImage === 'function') {
|
||
try {
|
||
const img = c.createImage()
|
||
img.onload = () => resolve(img)
|
||
img.onerror = () => reject(new Error('createImage load'))
|
||
img.src = src
|
||
return
|
||
} catch (e2) {
|
||
/* fall through */
|
||
}
|
||
}
|
||
if (typeof Image !== 'undefined') {
|
||
try {
|
||
const img = new Image()
|
||
try {
|
||
img.crossOrigin = 'anonymous'
|
||
} catch (e0) {
|
||
/* noop */
|
||
}
|
||
img.onload = () => resolve(img)
|
||
img.onerror = () => reject(new Error('image load'))
|
||
img.src = src
|
||
return
|
||
} catch (e) {
|
||
reject(e)
|
||
return
|
||
}
|
||
}
|
||
reject(new Error('no image loader'))
|
||
})
|
||
}
|
||
|
||
/** 各 beamEffect 对应色相偏置(度),与 2D 视觉大致同系 */
|
||
export const BEAM_STYLE_HUE = {
|
||
white: 0,
|
||
prism: 18,
|
||
aurora: 42,
|
||
sunset: 6,
|
||
neon: 72
|
||
}
|