如何使用画布(canvas)创建一个具有折射和反射效果的玻璃质感文本?

3
我希望您能够接近这个链接所示的内容,您也可以查看这些屏幕截图。

实际结果

注意随着页面向下/向上滚动,折射会如何演变。滚动时,还有一束光从右向左移动。

滚动后

理想情况下,我希望文本具有像提供的示例那样透明玻璃反射效果。但是,它似乎没有折射背后的内容。事实上,当画布被单独留下时,仍然发生折射,因此我怀疑该效果是在了解背景中将显示什么的情况下完成的。对于我来说,我希望动态地折射背后的内容。再一次,我认为这可能出于某种原因而被实现,也许是性能问题。 移除所有非画布元素

实际上,从背景来看,它似乎是基于背景的,但背景不在画布内。同时,如下图所示,即使移除了背景,折射效果仍然存在。

折射

光源仍然存在,我怀疑它使用了某种射线投射/追踪方法。我对画布绘图不熟悉(除了用p5.js处理简单的事情),并且花费了很长时间才找到射线追踪,但我不知道自己在寻找什么。
....问题....
  1. 如何使文本具有玻璃透明反射效果?需要使用图形设计工具吗?(我不知道如何获取一个似乎在纹理绑定之后的对象(请参见下面的屏幕截图)。我甚至不确定我是否使用了正确的词汇,但是假设我是,我也不知道如何制作这样的纹理。) 文本对象无“纹理”

  2. 如何折射放置在玻璃物体后面的所有东西?(在我得出需要使用画布的结论之前,不仅因为我发现了这个例子,还因为与我正在处理的项目相关的其他考虑,我已经投入了大量时间学习足够的SVG来实现您可以看到下一个屏幕截图中的内容,并未达到预期目标。我不想用光线投射重复同样的事情,因此提出了第三个问题。我希望这是可以理解的...仍然存在折射部分,但看起来比提供的示例要不真实得多。) SVG

  3. 光线投射/光线跟踪是否是实现折射的正确途径?如果对其进行光线跟踪并跟踪所有对象,这样做是否可以?

感谢您的时间和关注。
1个回答

5

反射与折射

有很多在线教程可以实现这种特效,我看不到重复它们的意义。

本回答介绍了一种使用法线贴图代替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>

近似

为了渲染文本,我们需要创建一些着色器。由于我们使用了法线贴图,因此顶点着色器可以非常简单。

顶点着色器

我们使用一个四边形来渲染整个画布。顶点着色器输出四个角落,并将每个角转换为纹理坐标。

#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);
    }   
}

这是最基本的形式,可以给人以反射和折射的印象。

虚假的反射和折射示例。

此示例稍微复杂一些,因为各种纹理具有不同的大小,因此需要在片段着色器中对它们进行缩放以成为正确的大小。

我还添加了一些色调到折射和反射中,并通过曲线混合反射。

背景滚动到鼠标位置。如果要匹配页面上的背景,则应将画布移动到背景上方。

着色器中有一些 #defines 来控制设置。您可以将它们设为统一变量或常量。

mixCurve 控制反射折射纹理的混合。值 < 1 > 0 可以使折射变得更平滑,而值 > 1 可以使反射变得更平滑。

法线贴图与渲染像素一一对应。由于二维画布渲染质量比较差,因此在片段着色器中进行过取样可以获得更好的结果。

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 比基于真实光照模型的模拟更加视觉上令人愉悦。不过请记住,这不是折射或反射。


非常感谢。第一句话真的很有帮助。还有不真实反射折射的例子。干杯! - MysterMiam
2
https://medium.com/@beclamide/advanced-realtime-glass-refraction-simulation-with-webgl-71bdce7ab825 - MysterMiam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接