如何在具有折射和反射的画布中创建由玻璃制成的文本?
Posted
技术标签:
【中文标题】如何在具有折射和反射的画布中创建由玻璃制成的文本?【英文标题】:How to create a text made of glass in canvas with refraction and reflection? 【发布时间】:2020-06-01 20:59:27 【问题描述】:我想要实现的目标与there 接近。你也可以看看那些截图。
The actual result
请注意折射如何随着页面向下/向上滚动而变化。滚动,还有一个从右到左的光源。
After scrolling
理想情况下,我希望文本具有像提供的示例中那样的透明玻璃反射方面。而且,要折射背后的东西,这里似乎并非如此。事实上,当画布被单独放置时,折射仍然发生,所以我怀疑效果已经完成,知道会在背景中显示什么。至于我,我想动态地折射背后的东西。再一次,我在想我可能以这种方式实现了某种原因,也许是性能问题
All non canvas elements removed
确实,它看起来像是基于背景,但背景不在画布内。此外,如您所见,在下一张图片中,即使背景被移除,折射效果仍在发生。
Refraction
光源仍然存在,我怀疑它使用了某种光线投射/光线追踪方法。我对在画布上绘图一点也不熟悉(除了使用 p5.js 来做简单的事情),而且我花了很长时间才找到光线追踪,却不知道我在找什么。
....问题....
如何在文本上获得玻璃透明反射面?应该用图形设计工具来实现吗? (我不知道如何获得一个之后似乎有纹理绑定的对象(见下面的屏幕截图)。我什至不确定我是否使用了正确的词汇,但假设我是,我不知道如何制作这样的纹理。) text object no "texture"
如何折射将放置在玻璃对象后面的所有内容? (在我得出我需要使用画布的结论之前,不仅仅是因为我找到了这个例子,还因为与我正在从事的项目相关的其他考虑因素。我花了很多时间学习足够的 svg实现您在下一个屏幕截图中看到的内容,但未能实现目标。我不愿意对光线投射这样做,因此我的第三个问题。我希望这是可以理解的......仍然存在折射部分但是看起来比提供的示例中的真实性要低得多。) SVG
光线投射/光线追踪是实现折射的正确途径吗?如果它的光线追踪后面的每个物体,是否可以使用。
感谢您的时间和关注。
【问题讨论】:
【参考方案1】:反射和折射
网上有很多教程来实现这个效果,我看不出重复它们的意义。
这个答案提供了一个近似值,使用法线贴图代替 3D 模型,并使用平面纹理贴图来表示反射和折射贴图,而不是传统上用于获得反射和折射的 3D 纹理。
生成法线贴图。
下面的 sn-p 从带有各种选项的输入文本生成法线贴图。该过程相当快(不是实时的),并且将在 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>
近似值
要渲染文本,我们需要创建一些着色器。由于我们使用的是法线贴图,因此顶点着色器可以非常简单。
顶点着色器
我们使用四边形来渲染整个画布。顶点着色器输出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);
这是最基本的形式,可以给人以反射和折射的印象。
示例 NOT REAL 反射折射。
该示例稍微复杂一些,因为各种纹理具有不同的大小,因此需要在片段着色器中缩放为正确的大小。
我还为折射和反射添加了一些着色,并通过曲线混合反射。
背景滚动到鼠标位置。要匹配页面上的背景,您可以将画布移到背景上。
片段着色器中有几个#defines 来控制设置。您可以将它们设为统一或常量。
mixCurve
控制反射折射纹理的混合。值 0 缓和折射,值 > 1 缓和反射。
法线贴图与渲染像素是一对一的。由于 2D 画布渲染质量相当差,您可以通过在片段着色器中对法线贴图进行过度采样来获得更好的结果。
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 比基于真实照明模型的模拟在视觉上更令人愉悦。虽然请记住,这不是折射或反射。
【讨论】:
非常感谢。第一句话真的很有帮助。以及非真实反射折射的例子。干杯 medium.com/@beclamide/…以上是关于如何在具有折射和反射的画布中创建由玻璃制成的文本?的主要内容,如果未能解决你的问题,请参考以下文章
Ray Tracing in One Weekend 超详解 光线追踪1-7 Dielectric 半径为负,实心球体镂空技巧