如何使用折射和反射在 canvas 中创建由玻璃制成的文本?
How to create a text made of glass in canvas with refraction and reflection?
我想达到的目标与此接近there。你也可以看看那些截图。
The actual result
注意折射如何随着页面滚动而变化 down/up。滚动,还有一个从右到左的光源。
After scrolling
理想情况下,我希望文本具有透明玻璃反光效果,就像所提供的示例一样。而且,要折射背后的东西,这里似乎并非如此。确实,当 canvas 单独放置时,折射仍然会发生,所以我怀疑效果是在知道背景中显示的内容的情况下完成的。至于我,我想动态地折射后面的东西。再一次,我在想我可能是出于某种原因以这种方式实现的,也许是性能问题
All non canvas elements removed
的确,从背景来看是这样的,但是背景不在canvas之内。另外,正如您所看到的,在下一张图片中,即使去除了背景,折射效果仍然存在。
Refraction
光源仍然存在,我怀疑它使用了某种光线追踪方法casting/ray。我一点也不熟悉 canvas 中的绘图(除了使用 p5.js 来做简单的事情),我花了很长时间才找到光线追踪,却不知道我在找什么.
..... 问题....
如何在文本上获得玻璃透明反光效果?应该用图形设计工具来实现吗? (我不知道如何获得一个似乎有纹理绑定的对象(见下面的截图)afterwards.I我什至不确定我是否使用了正确的词汇但假设我是,我不知道知道如何制作这种纹理。)
text object no "texture"
如何折射放置在玻璃物体后面的所有东西? (在我得出需要使用 canvas 的结论之前,不仅是因为我找到了这个例子,还因为与我正在从事的项目相关的其他考虑因素。我投入了很多时间学习足够的 svg 以实现您在下一个屏幕截图中可以看到的内容,但未能实现目标。我不愿意对光线投射这样做,因此我的第三个问题。我希望它是可以理解的......仍然是折射部分在那里,但看起来比提供的示例中的真实性要差得多。)
SVG
光线 casting/ray 追踪是实现折射的正确途径吗?如果它的光线追踪每一个后面的物体是否可以使用。
感谢您的时间和关心。
反射和折射
网上有很多教程可以实现这个 FX,我看不出重复它们的意义。
此答案提供了一个近似值,使用法线贴图代替 3D 模型,并使用平面纹理贴图来表示反射和折射贴图,而不是传统上用于获取反射和折射的 3D 纹理。
正在生成法线贴图。
下面的代码片段使用各种选项从输入文本生成法线贴图。这个过程相当快(不是实时的)并且将在 webGL 渲染解决方案中代替 3D 模型。
它首先创建文本的高度贴图,添加一些平滑,然后将贴图转换为法线贴图。
text.addEventListener("keyup", createNormalMap)
createNormalMap();
function createNormalMap(){
text.focus();
setTimeout(() => {
const can = normalMapText(text.value, "Arial Black", 96, 8, 2, 0.1, true, "round");
result.innerHTML = "";
result.appendChild(can);
}, 0);
}
function normalMapText(text, font, size, bevel, smooth = 0, curve = 0.5, smoothNormals = true, corners = "round") {
const canvas = document.createElement("canvas");
const mask = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ctxMask = mask.getContext("2d");
ctx.font = size + "px " + font;
const tw = ctx.measureText(text).width;
const cx = (mask.width = canvas.width = tw + bevel * 3) / 2;
const cy = (mask.height = canvas.height = size + bevel * 3) / 2;
ctx.font = size + "px " + font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineJoin = corners;
const step = 255 / (bevel + 1);
var j, i = 0, val = step;
while (i < bevel) {
ctx.lineWidth = bevel - i;
const v = ((val / 255) ** curve) * 255;
ctx.strokeStyle = `rgb(${v},${v},${v})`;
ctx.strokeText(text, cx, cy);
i++;
val += step;
}
ctx.fillStyle = "#FFF";
ctx.fillText(text, cx, cy);
if (smooth >= 1) {
ctxMask.drawImage(canvas, 0, 0);
ctx.filter = "blur(" + smooth + "px)";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "destination-in";
ctx.filter = "none";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "source-over";
}
const w = canvas.width, h = canvas.height, w4 = w << 2;
const imgData = ctx.getImageData(0,0,w,h);
const d = imgData.data;
const heightBuf = new Uint8Array(w * h);
j = i = 0;
while (i < d.length) {
heightBuf[j++] = d[i]
i += 4;
}
var x, y, xx, yy, zz, xx1, yy1, zz1, xx2, yy2, zz2, dist;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
if(d[i + 3]) { // only pixels with alpha > 0
const idx = x + y * w;
const x1 = 1;
const z1 = heightBuf[idx - 1] === undefined ? 0 : heightBuf[idx - 1] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w] === undefined ? 0 : heightBuf[idx - w] - heightBuf[idx];
const y2 = -1;
const x3 = 1;
const z3 = heightBuf[idx - w - 1] === undefined ? 0 : heightBuf[idx - w - 1] - heightBuf[idx];
const y3 = -1;
xx = y3 * z2 - z3 * y2
yy = z3 * x2 - x3 * z2
zz = x3 * y2 - y3 * x2
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
xx /= dist;
yy /= dist;
zz /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5;
xx += xx1 / dist;
yy += yy1 / dist;
zz += zz1 / dist;
if (smoothNormals) {
const x1 = 2;
const z1 = heightBuf[idx - 2] === undefined ? 0 : heightBuf[idx - 2] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w * 2] === undefined ? 0 : heightBuf[idx - w * 2] - heightBuf[idx];
const y2 = -2;
const x3 = 2;
const z3 = heightBuf[idx - w * 2 - 2] === undefined ? 0 : heightBuf[idx - w * 2 - 2] - heightBuf[idx];
const y3 = -2;
xx2 = y3 * z2 - z3 * y2
yy2 = z3 * x2 - x3 * z2
zz2 = x3 * y2 - y3 * x2
dist = (xx2 * xx2 + yy2 * yy2 + zz2 * zz2) ** 0.5 * 2;
xx2 /= dist;
yy2 /= dist;
zz2 /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5 * 2;
xx2 += xx1 / dist;
yy2 += yy1 / dist;
zz2 += zz1 / dist;
xx += xx2;
yy += yy2;
zz += zz2;
}
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
d[i+0] = ((xx / dist) + 1.0) * 128;
d[i+1] = ((yy / dist) + 1.0) * 128;
d[i+2] = 255 - ((zz / dist) + 1.0) * 128;
}
i += 4;
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
<input id="text" type="text" value="Normal Map" />
<div id="result"></div>
近似值
要渲染文本,我们需要创建一些着色器。由于我们使用的是法线贴图,因此顶点着色器可以非常简单。
顶点着色器
我们正在使用四边形来渲染整个 canvas。顶点着色器输出 4 个角并将每个角转换为纹理坐标。
#version 300 es
in vec2 vert;
out vec2 texCoord;
void main() {
texCoord = vert * 0.5 + 0.5;
gl_Position = vec4(verts, 1, 1);
}
片段着色器
片段着色器有 3 个纹理输入。法线贴图、反射贴图和折射贴图。
片段着色器首先确定像素是背景的一部分还是在文本上。如果在文本上,它将 RGB 纹理法线转换为矢量法线。
然后使用矢量加法得到反射和折射的纹理。通过法线贴图 z 值混合这些纹理。实际上,法线朝上时折射最强,法线朝外时反射最强
#version 300 es
uniform sampler2D normalMap;
uniform sampler2D refractionMap;
uniform sampler2D reflectionMap;
in vec2 texCoord;
out vec4 pixel;
void main() {
vec4 norm = texture(normalMap, texCoord);
if (norm.a > 0) {
vec3 normal = normalize(norm.rgb - 0.5);
vec2 tx1 = textCoord + normal.xy * 0.1;
vec2 tx2 = textCoord - normal.xy * 0.2;
pixel = vec4(mix(texture(refractionMap, tx2).rgb, texture(reflectionMap, tx1).rgb, abs(normal.z)), norm.a);
} else {
pixel = texture(refactionMap, texCoord);
}
}
这是最基本的形式,会给人以反射和折射的印象。
示例不是真实的反射折射。
这个例子有点复杂,因为各种纹理有不同的大小,因此需要在片段着色器中缩放到正确的大小。
我还为折射和反射添加了一些着色,并通过曲线混合了反射。
背景滚动到鼠标位置。要匹配页面上的背景,您可以将 canvas 移到背景上。
片段着色器中有几个#defines 来控制设置。你可以让他们成为制服,或者常量。
mixCurve
控制反射折射纹理的混合。值 < 1 > 0 缓和折射,值 > 1 缓和反射。
法线贴图与渲染像素是一对一的。由于 2D canvas 渲染质量很差,您可以通过在片段着色器中对法线贴图进行过度采样来获得更好的结果。
const vertSrc = `#version 300 es
in vec2 verts;
out vec2 texCoord;
void main() {
texCoord = verts * vec2(0.5, -0.5) + 0.5;
gl_Position = vec4(verts, 1, 1);
}
`
const fragSrc = `#version 300 es
precision highp float;
#define refractStrength 0.1
#define reflectStrength 0.2
#define refractTint vec3(1,0.95,0.85)
#define reflectTint vec3(1,1.25,1.42)
#define mixCurve 0.3
uniform sampler2D normalMap;
uniform sampler2D refractionMap;
uniform sampler2D reflectionMap;
uniform vec2 scrolls;
in vec2 texCoord;
out vec4 pixel;
void main() {
vec2 nSize = vec2(textureSize(normalMap, 0));
vec2 scaleCoords = nSize / vec2(textureSize(refractionMap, 0));
vec2 rCoord = (texCoord - scrolls) * scaleCoords;
vec4 norm = texture(normalMap, texCoord);
if (norm.a > 0.99) {
vec3 normal = normalize(norm.rgb - 0.5);
vec2 tx1 = rCoord + normal.xy * scaleCoords * refractStrength;
vec2 tx2 = rCoord - normal.xy * scaleCoords * reflectStrength;
vec3 c1 = texture(refractionMap, tx1).rgb * refractTint;
vec3 c2 = texture(reflectionMap, tx2).rgb * reflectTint;
pixel = vec4(mix(c2, c1, abs(pow(normal.z,mixCurve))), 1.0);
} else {
pixel = texture(refractionMap, rCoord);
}
}
`
var program, loc;
function normalMapText(text, font, size, bevel, smooth = 0, curve = 0.5, smoothNormals = true, corners = "round") {
const canvas = document.createElement("canvas");
const mask = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ctxMask = mask.getContext("2d");
ctx.font = size + "px " + font;
const tw = ctx.measureText(text).width;
const cx = (mask.width = canvas.width = tw + bevel * 3) / 2;
const cy = (mask.height = canvas.height = size + bevel * 3) / 2;
ctx.font = size + "px " + font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineJoin = corners;
const step = 255 / (bevel + 1);
var j, i = 0, val = step;
while (i < bevel) {
ctx.lineWidth = bevel - i;
const v = ((val / 255) ** curve) * 255;
ctx.strokeStyle = `rgb(${v},${v},${v})`;
ctx.strokeText(text, cx, cy);
i++;
val += step;
}
ctx.fillStyle = "#FFF";
ctx.fillText(text, cx, cy);
if (smooth >= 1) {
ctxMask.drawImage(canvas, 0, 0);
ctx.filter = "blur(" + smooth + "px)";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "destination-in";
ctx.filter = "none";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "source-over";
}
const w = canvas.width, h = canvas.height, w4 = w << 2;
const imgData = ctx.getImageData(0,0,w,h);
const d = imgData.data;
const heightBuf = new Uint8Array(w * h);
j = i = 0;
while (i < d.length) {
heightBuf[j++] = d[i]
i += 4;
}
var x, y, xx, yy, zz, xx1, yy1, zz1, xx2, yy2, zz2, dist;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
if(d[i + 3]) { // only pixels with alpha > 0
const idx = x + y * w;
const x1 = 1;
const z1 = heightBuf[idx - 1] === undefined ? 0 : heightBuf[idx - 1] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w] === undefined ? 0 : heightBuf[idx - w] - heightBuf[idx];
const y2 = -1;
const x3 = 1;
const z3 = heightBuf[idx - w - 1] === undefined ? 0 : heightBuf[idx - w - 1] - heightBuf[idx];
const y3 = -1;
xx = y3 * z2 - z3 * y2
yy = z3 * x2 - x3 * z2
zz = x3 * y2 - y3 * x2
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
xx /= dist;
yy /= dist;
zz /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5;
xx += xx1 / dist;
yy += yy1 / dist;
zz += zz1 / dist;
if (smoothNormals) {
const x1 = 2;
const z1 = heightBuf[idx - 2] === undefined ? 0 : heightBuf[idx - 2] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w * 2] === undefined ? 0 : heightBuf[idx - w * 2] - heightBuf[idx];
const y2 = -2;
const x3 = 2;
const z3 = heightBuf[idx - w * 2 - 2] === undefined ? 0 : heightBuf[idx - w * 2 - 2] - heightBuf[idx];
const y3 = -2;
xx2 = y3 * z2 - z3 * y2
yy2 = z3 * x2 - x3 * z2
zz2 = x3 * y2 - y3 * x2
dist = (xx2 * xx2 + yy2 * yy2 + zz2 * zz2) ** 0.5 * 2;
xx2 /= dist;
yy2 /= dist;
zz2 /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5 * 2;
xx2 += xx1 / dist;
yy2 += yy1 / dist;
zz2 += zz1 / dist;
xx += xx2;
yy += yy2;
zz += zz2;
}
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
d[i+0] = ((xx / dist) + 1.0) * 128;
d[i+1] = ((yy / dist) + 1.0) * 128;
d[i+2] = 255 - ((zz / dist) + 1.0) * 128;
}
i += 4;
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
function createChecker(size, width, height) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width * size;
canvas.height = height * size;
for(var y = 0; y < size; y ++) {
for(var x = 0; x < size; x ++) {
const xx = x * width;
const yy = y * height;
ctx.fillStyle ="#888";
ctx.fillRect(xx,yy,width,height);
ctx.fillStyle ="#DDD";
ctx.fillRect(xx,yy,width/2,height/2);
ctx.fillRect(xx+width/2,yy+height/2,width/2,height/2);
}
}
return canvas;
}
const mouse = {x:0, y:0};
addEventListener("mousemove",e => {mouse.x = e.pageX; mouse.y = e.pageY });
var normMap = normalMapText("GLASSY", "Arial Black", 128, 24, 1, 0.1, true, "round");
canvas.width = normMap.width;
canvas.height = normMap.height;
const locations = {updates: []};
const fArr = arr => new Float32Array(arr);
const gl = canvas.getContext("webgl2", {premultipliedAlpha: false, antialias: false, alpha: false});
const textures = {};
setup();
function texture(gl, image, {min = "LINEAR", mag = "LINEAR", wrapX = "REPEAT", wrapY = "REPEAT"} = {}) {
const texture = gl.createTexture();
target = gl.TEXTURE_2D;
gl.bindTexture(target, texture);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl[min]);
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, gl[mag]);
gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl[wrapX]);
gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl[wrapY]);
gl.texImage2D(target, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
function bindTexture(texture, unit) {
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, texture);
}
function Location(name, data, type = "fv", autoUpdate = true) {
const glUpdateCall = gl["uniform" + data.length + type].bind(gl);
const loc = gl.getUniformLocation(program, name);
locations[name] = {data, update() {glUpdateCall(loc, data)}};
autoUpdate && locations.updates.push(locations[name]);
return locations[name];
}
function compileShader(src, type, shader = gl.createShader(type)) {
gl.shaderSource(shader, src);
gl.compileShader(shader);
return shader;
}
function setup() {
program = gl.createProgram();
gl.attachShader(program, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0,1,2,0,2,3]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, fArr([-1,-1,1,-1,1,1,-1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc = gl.getAttribLocation(program, "verts"));
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
Location("scrolls", [0, 0]);
Location("normalMap", [0], "i", false).update();
Location("refractionMap", [1], "i", false).update();
Location("reflectionMap", [2], "i", false).update();
textures.norm = texture(gl,normMap);
textures.reflect = texture(gl,createChecker(8,128,128));
textures.refract = texture(gl,createChecker(8,128,128));
gl.viewport(0, 0, normMap.width, normMap.height);
bindTexture(textures.norm, 0);
bindTexture(textures.reflect, 1);
bindTexture(textures.refract, 2);
loop();
}
function draw() {
for(const l of locations.updates) { l.update() }
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
function loop() {
locations.scrolls.data[0] = -1 + mouse.x / canvas.width;
locations.scrolls.data[1] = -1 + mouse.y / canvas.height;
draw();
requestAnimationFrame(loop);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
就我个人而言,我发现此 FX 比基于真实照明模型的模拟在视觉上更令人愉悦。但请记住,这不是折射或反射。
我想达到的目标与此接近there。你也可以看看那些截图。
The actual result
注意折射如何随着页面滚动而变化 down/up。滚动,还有一个从右到左的光源。
After scrolling
理想情况下,我希望文本具有透明玻璃反光效果,就像所提供的示例一样。而且,要折射背后的东西,这里似乎并非如此。确实,当 canvas 单独放置时,折射仍然会发生,所以我怀疑效果是在知道背景中显示的内容的情况下完成的。至于我,我想动态地折射后面的东西。再一次,我在想我可能是出于某种原因以这种方式实现的,也许是性能问题
All non canvas elements removed
的确,从背景来看是这样的,但是背景不在canvas之内。另外,正如您所看到的,在下一张图片中,即使去除了背景,折射效果仍然存在。
Refraction
光源仍然存在,我怀疑它使用了某种光线追踪方法casting/ray。我一点也不熟悉 canvas 中的绘图(除了使用 p5.js 来做简单的事情),我花了很长时间才找到光线追踪,却不知道我在找什么.
..... 问题....
如何在文本上获得玻璃透明反光效果?应该用图形设计工具来实现吗? (我不知道如何获得一个似乎有纹理绑定的对象(见下面的截图)afterwards.I我什至不确定我是否使用了正确的词汇但假设我是,我不知道知道如何制作这种纹理。) text object no "texture"
如何折射放置在玻璃物体后面的所有东西? (在我得出需要使用 canvas 的结论之前,不仅是因为我找到了这个例子,还因为与我正在从事的项目相关的其他考虑因素。我投入了很多时间学习足够的 svg 以实现您在下一个屏幕截图中可以看到的内容,但未能实现目标。我不愿意对光线投射这样做,因此我的第三个问题。我希望它是可以理解的......仍然是折射部分在那里,但看起来比提供的示例中的真实性要差得多。) SVG
光线 casting/ray 追踪是实现折射的正确途径吗?如果它的光线追踪每一个后面的物体是否可以使用。
感谢您的时间和关心。
反射和折射
网上有很多教程可以实现这个 FX,我看不出重复它们的意义。
此答案提供了一个近似值,使用法线贴图代替 3D 模型,并使用平面纹理贴图来表示反射和折射贴图,而不是传统上用于获取反射和折射的 3D 纹理。
正在生成法线贴图。
下面的代码片段使用各种选项从输入文本生成法线贴图。这个过程相当快(不是实时的)并且将在 webGL 渲染解决方案中代替 3D 模型。
它首先创建文本的高度贴图,添加一些平滑,然后将贴图转换为法线贴图。
text.addEventListener("keyup", createNormalMap)
createNormalMap();
function createNormalMap(){
text.focus();
setTimeout(() => {
const can = normalMapText(text.value, "Arial Black", 96, 8, 2, 0.1, true, "round");
result.innerHTML = "";
result.appendChild(can);
}, 0);
}
function normalMapText(text, font, size, bevel, smooth = 0, curve = 0.5, smoothNormals = true, corners = "round") {
const canvas = document.createElement("canvas");
const mask = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ctxMask = mask.getContext("2d");
ctx.font = size + "px " + font;
const tw = ctx.measureText(text).width;
const cx = (mask.width = canvas.width = tw + bevel * 3) / 2;
const cy = (mask.height = canvas.height = size + bevel * 3) / 2;
ctx.font = size + "px " + font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineJoin = corners;
const step = 255 / (bevel + 1);
var j, i = 0, val = step;
while (i < bevel) {
ctx.lineWidth = bevel - i;
const v = ((val / 255) ** curve) * 255;
ctx.strokeStyle = `rgb(${v},${v},${v})`;
ctx.strokeText(text, cx, cy);
i++;
val += step;
}
ctx.fillStyle = "#FFF";
ctx.fillText(text, cx, cy);
if (smooth >= 1) {
ctxMask.drawImage(canvas, 0, 0);
ctx.filter = "blur(" + smooth + "px)";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "destination-in";
ctx.filter = "none";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "source-over";
}
const w = canvas.width, h = canvas.height, w4 = w << 2;
const imgData = ctx.getImageData(0,0,w,h);
const d = imgData.data;
const heightBuf = new Uint8Array(w * h);
j = i = 0;
while (i < d.length) {
heightBuf[j++] = d[i]
i += 4;
}
var x, y, xx, yy, zz, xx1, yy1, zz1, xx2, yy2, zz2, dist;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
if(d[i + 3]) { // only pixels with alpha > 0
const idx = x + y * w;
const x1 = 1;
const z1 = heightBuf[idx - 1] === undefined ? 0 : heightBuf[idx - 1] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w] === undefined ? 0 : heightBuf[idx - w] - heightBuf[idx];
const y2 = -1;
const x3 = 1;
const z3 = heightBuf[idx - w - 1] === undefined ? 0 : heightBuf[idx - w - 1] - heightBuf[idx];
const y3 = -1;
xx = y3 * z2 - z3 * y2
yy = z3 * x2 - x3 * z2
zz = x3 * y2 - y3 * x2
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
xx /= dist;
yy /= dist;
zz /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5;
xx += xx1 / dist;
yy += yy1 / dist;
zz += zz1 / dist;
if (smoothNormals) {
const x1 = 2;
const z1 = heightBuf[idx - 2] === undefined ? 0 : heightBuf[idx - 2] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w * 2] === undefined ? 0 : heightBuf[idx - w * 2] - heightBuf[idx];
const y2 = -2;
const x3 = 2;
const z3 = heightBuf[idx - w * 2 - 2] === undefined ? 0 : heightBuf[idx - w * 2 - 2] - heightBuf[idx];
const y3 = -2;
xx2 = y3 * z2 - z3 * y2
yy2 = z3 * x2 - x3 * z2
zz2 = x3 * y2 - y3 * x2
dist = (xx2 * xx2 + yy2 * yy2 + zz2 * zz2) ** 0.5 * 2;
xx2 /= dist;
yy2 /= dist;
zz2 /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5 * 2;
xx2 += xx1 / dist;
yy2 += yy1 / dist;
zz2 += zz1 / dist;
xx += xx2;
yy += yy2;
zz += zz2;
}
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
d[i+0] = ((xx / dist) + 1.0) * 128;
d[i+1] = ((yy / dist) + 1.0) * 128;
d[i+2] = 255 - ((zz / dist) + 1.0) * 128;
}
i += 4;
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
<input id="text" type="text" value="Normal Map" />
<div id="result"></div>
近似值
要渲染文本,我们需要创建一些着色器。由于我们使用的是法线贴图,因此顶点着色器可以非常简单。
顶点着色器
我们正在使用四边形来渲染整个 canvas。顶点着色器输出 4 个角并将每个角转换为纹理坐标。
#version 300 es
in vec2 vert;
out vec2 texCoord;
void main() {
texCoord = vert * 0.5 + 0.5;
gl_Position = vec4(verts, 1, 1);
}
片段着色器
片段着色器有 3 个纹理输入。法线贴图、反射贴图和折射贴图。
片段着色器首先确定像素是背景的一部分还是在文本上。如果在文本上,它将 RGB 纹理法线转换为矢量法线。
然后使用矢量加法得到反射和折射的纹理。通过法线贴图 z 值混合这些纹理。实际上,法线朝上时折射最强,法线朝外时反射最强
#version 300 es
uniform sampler2D normalMap;
uniform sampler2D refractionMap;
uniform sampler2D reflectionMap;
in vec2 texCoord;
out vec4 pixel;
void main() {
vec4 norm = texture(normalMap, texCoord);
if (norm.a > 0) {
vec3 normal = normalize(norm.rgb - 0.5);
vec2 tx1 = textCoord + normal.xy * 0.1;
vec2 tx2 = textCoord - normal.xy * 0.2;
pixel = vec4(mix(texture(refractionMap, tx2).rgb, texture(reflectionMap, tx1).rgb, abs(normal.z)), norm.a);
} else {
pixel = texture(refactionMap, texCoord);
}
}
这是最基本的形式,会给人以反射和折射的印象。
示例不是真实的反射折射。
这个例子有点复杂,因为各种纹理有不同的大小,因此需要在片段着色器中缩放到正确的大小。
我还为折射和反射添加了一些着色,并通过曲线混合了反射。
背景滚动到鼠标位置。要匹配页面上的背景,您可以将 canvas 移到背景上。
片段着色器中有几个#defines 来控制设置。你可以让他们成为制服,或者常量。
mixCurve
控制反射折射纹理的混合。值 < 1 > 0 缓和折射,值 > 1 缓和反射。
法线贴图与渲染像素是一对一的。由于 2D canvas 渲染质量很差,您可以通过在片段着色器中对法线贴图进行过度采样来获得更好的结果。
const vertSrc = `#version 300 es
in vec2 verts;
out vec2 texCoord;
void main() {
texCoord = verts * vec2(0.5, -0.5) + 0.5;
gl_Position = vec4(verts, 1, 1);
}
`
const fragSrc = `#version 300 es
precision highp float;
#define refractStrength 0.1
#define reflectStrength 0.2
#define refractTint vec3(1,0.95,0.85)
#define reflectTint vec3(1,1.25,1.42)
#define mixCurve 0.3
uniform sampler2D normalMap;
uniform sampler2D refractionMap;
uniform sampler2D reflectionMap;
uniform vec2 scrolls;
in vec2 texCoord;
out vec4 pixel;
void main() {
vec2 nSize = vec2(textureSize(normalMap, 0));
vec2 scaleCoords = nSize / vec2(textureSize(refractionMap, 0));
vec2 rCoord = (texCoord - scrolls) * scaleCoords;
vec4 norm = texture(normalMap, texCoord);
if (norm.a > 0.99) {
vec3 normal = normalize(norm.rgb - 0.5);
vec2 tx1 = rCoord + normal.xy * scaleCoords * refractStrength;
vec2 tx2 = rCoord - normal.xy * scaleCoords * reflectStrength;
vec3 c1 = texture(refractionMap, tx1).rgb * refractTint;
vec3 c2 = texture(reflectionMap, tx2).rgb * reflectTint;
pixel = vec4(mix(c2, c1, abs(pow(normal.z,mixCurve))), 1.0);
} else {
pixel = texture(refractionMap, rCoord);
}
}
`
var program, loc;
function normalMapText(text, font, size, bevel, smooth = 0, curve = 0.5, smoothNormals = true, corners = "round") {
const canvas = document.createElement("canvas");
const mask = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ctxMask = mask.getContext("2d");
ctx.font = size + "px " + font;
const tw = ctx.measureText(text).width;
const cx = (mask.width = canvas.width = tw + bevel * 3) / 2;
const cy = (mask.height = canvas.height = size + bevel * 3) / 2;
ctx.font = size + "px " + font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineJoin = corners;
const step = 255 / (bevel + 1);
var j, i = 0, val = step;
while (i < bevel) {
ctx.lineWidth = bevel - i;
const v = ((val / 255) ** curve) * 255;
ctx.strokeStyle = `rgb(${v},${v},${v})`;
ctx.strokeText(text, cx, cy);
i++;
val += step;
}
ctx.fillStyle = "#FFF";
ctx.fillText(text, cx, cy);
if (smooth >= 1) {
ctxMask.drawImage(canvas, 0, 0);
ctx.filter = "blur(" + smooth + "px)";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "destination-in";
ctx.filter = "none";
ctx.drawImage(mask, 0, 0);
ctx.globalCompositeOperation = "source-over";
}
const w = canvas.width, h = canvas.height, w4 = w << 2;
const imgData = ctx.getImageData(0,0,w,h);
const d = imgData.data;
const heightBuf = new Uint8Array(w * h);
j = i = 0;
while (i < d.length) {
heightBuf[j++] = d[i]
i += 4;
}
var x, y, xx, yy, zz, xx1, yy1, zz1, xx2, yy2, zz2, dist;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
if(d[i + 3]) { // only pixels with alpha > 0
const idx = x + y * w;
const x1 = 1;
const z1 = heightBuf[idx - 1] === undefined ? 0 : heightBuf[idx - 1] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w] === undefined ? 0 : heightBuf[idx - w] - heightBuf[idx];
const y2 = -1;
const x3 = 1;
const z3 = heightBuf[idx - w - 1] === undefined ? 0 : heightBuf[idx - w - 1] - heightBuf[idx];
const y3 = -1;
xx = y3 * z2 - z3 * y2
yy = z3 * x2 - x3 * z2
zz = x3 * y2 - y3 * x2
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
xx /= dist;
yy /= dist;
zz /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5;
xx += xx1 / dist;
yy += yy1 / dist;
zz += zz1 / dist;
if (smoothNormals) {
const x1 = 2;
const z1 = heightBuf[idx - 2] === undefined ? 0 : heightBuf[idx - 2] - heightBuf[idx];
const y1 = 0;
const x2 = 0;
const z2 = heightBuf[idx - w * 2] === undefined ? 0 : heightBuf[idx - w * 2] - heightBuf[idx];
const y2 = -2;
const x3 = 2;
const z3 = heightBuf[idx - w * 2 - 2] === undefined ? 0 : heightBuf[idx - w * 2 - 2] - heightBuf[idx];
const y3 = -2;
xx2 = y3 * z2 - z3 * y2
yy2 = z3 * x2 - x3 * z2
zz2 = x3 * y2 - y3 * x2
dist = (xx2 * xx2 + yy2 * yy2 + zz2 * zz2) ** 0.5 * 2;
xx2 /= dist;
yy2 /= dist;
zz2 /= dist;
xx1 = y1 * z3 - z1 * y3
yy1 = z1 * x3 - x1 * z3
zz1 = x1 * y3 - y1 * x3
dist = (xx1 * xx1 + yy1 * yy1 + zz1 * zz1) ** 0.5 * 2;
xx2 += xx1 / dist;
yy2 += yy1 / dist;
zz2 += zz1 / dist;
xx += xx2;
yy += yy2;
zz += zz2;
}
dist = (xx * xx + yy * yy + zz * zz) ** 0.5;
d[i+0] = ((xx / dist) + 1.0) * 128;
d[i+1] = ((yy / dist) + 1.0) * 128;
d[i+2] = 255 - ((zz / dist) + 1.0) * 128;
}
i += 4;
}
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
function createChecker(size, width, height) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width * size;
canvas.height = height * size;
for(var y = 0; y < size; y ++) {
for(var x = 0; x < size; x ++) {
const xx = x * width;
const yy = y * height;
ctx.fillStyle ="#888";
ctx.fillRect(xx,yy,width,height);
ctx.fillStyle ="#DDD";
ctx.fillRect(xx,yy,width/2,height/2);
ctx.fillRect(xx+width/2,yy+height/2,width/2,height/2);
}
}
return canvas;
}
const mouse = {x:0, y:0};
addEventListener("mousemove",e => {mouse.x = e.pageX; mouse.y = e.pageY });
var normMap = normalMapText("GLASSY", "Arial Black", 128, 24, 1, 0.1, true, "round");
canvas.width = normMap.width;
canvas.height = normMap.height;
const locations = {updates: []};
const fArr = arr => new Float32Array(arr);
const gl = canvas.getContext("webgl2", {premultipliedAlpha: false, antialias: false, alpha: false});
const textures = {};
setup();
function texture(gl, image, {min = "LINEAR", mag = "LINEAR", wrapX = "REPEAT", wrapY = "REPEAT"} = {}) {
const texture = gl.createTexture();
target = gl.TEXTURE_2D;
gl.bindTexture(target, texture);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl[min]);
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, gl[mag]);
gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl[wrapX]);
gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl[wrapY]);
gl.texImage2D(target, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
function bindTexture(texture, unit) {
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, texture);
}
function Location(name, data, type = "fv", autoUpdate = true) {
const glUpdateCall = gl["uniform" + data.length + type].bind(gl);
const loc = gl.getUniformLocation(program, name);
locations[name] = {data, update() {glUpdateCall(loc, data)}};
autoUpdate && locations.updates.push(locations[name]);
return locations[name];
}
function compileShader(src, type, shader = gl.createShader(type)) {
gl.shaderSource(shader, src);
gl.compileShader(shader);
return shader;
}
function setup() {
program = gl.createProgram();
gl.attachShader(program, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0,1,2,0,2,3]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, fArr([-1,-1,1,-1,1,1,-1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc = gl.getAttribLocation(program, "verts"));
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
Location("scrolls", [0, 0]);
Location("normalMap", [0], "i", false).update();
Location("refractionMap", [1], "i", false).update();
Location("reflectionMap", [2], "i", false).update();
textures.norm = texture(gl,normMap);
textures.reflect = texture(gl,createChecker(8,128,128));
textures.refract = texture(gl,createChecker(8,128,128));
gl.viewport(0, 0, normMap.width, normMap.height);
bindTexture(textures.norm, 0);
bindTexture(textures.reflect, 1);
bindTexture(textures.refract, 2);
loop();
}
function draw() {
for(const l of locations.updates) { l.update() }
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
function loop() {
locations.scrolls.data[0] = -1 + mouse.x / canvas.width;
locations.scrolls.data[1] = -1 + mouse.y / canvas.height;
draw();
requestAnimationFrame(loop);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
就我个人而言,我发现此 FX 比基于真实照明模型的模拟在视觉上更令人愉悦。但请记住,这不是折射或反射。