如何在玻璃上创建具有折射和反射效果的玻璃文字?

MysterMiam

我想要达到的目标与那里的目标差不多您也可以只看一下这些屏幕截图。

实际结果

请注意,随着页面向下/向上滚动,折射是如何演变的。滚动时,还有一个从右到左的光源。

滚动后

Ideally I'd like the text to have that transparent glass reflective aspect like on the example provided. But also, to refract what is behind, which does not seem to be the case here. Indeed, when the canvas is left alone, the refraction still happens, so i suspect the effects is done knowing what would be displayed in the background. As for me, I'd like to refract whats behind dynamically. Yet again i'm thinking that i might have been achieved this way for a reason, maybe performance issue

All non canvas elements removed

Indeed, it looks like it it based from the background, but the background is not within the canvas. Also, as you can see, on the next picture, the refraction effect is still hapenning even though the background is removed.

Refraction

The source of light is still there and i suspect it's using some kind of ray casting/ray tracing method. I'm not at all familiar with drawing in the canvas (except using p5.js for simple things),and it took me a long time to find ray tracing with no idea of what i'm looking for.

.... Questions ....

  1. How do i get the glass transparent reflective aspect on the text ? Should it be achieve with graphic design tools ? (I don't know how to get an object (see screenshot below) that seem to have the texture bind afterwards.I'm not even sure if i'm using the right vocabulary but assuming I am, I don't know how to make such texture.) text object no "texture"

  2. How to refract everything that would be placed behind the glass object? (Before I came to the conclusion that I needed to use canvas, not just because I found this exemple, but also because of other considerations related to the project I'm working on. I've invest a lot of time learning suffisant svg to achieve what you can see on the next screenshot,and failed to achieve what was aimed. I'm not willing to do so the same with ray casting thus my third question. I hope it's understandable...Still the refracted part is there but looks a lot less realistic than in the provided example.) SVG

  3. Is ray casting/ray tracing is the right path to dig in for achieving the refraction ? Will it be okay to use if its ray tracing every objects behind.

Thanks for your time and concern.

Blindman67

Reflection and Refraction

There are so many tutorials online to achieve this FX I can not see the point in repeating them.

This answer presents an approximation using a normal map in place of a 3D model, and flat texture maps to represent the reflection and refraction maps, rather than 3D textures traditionally used to get reflections and refraction.

Generating a normal map.

The snippet below generates a normal map from input text with various options. The process is reasonably quick (not real time) and will be the stand in for a 3D model in the webGL rendering solution.

It first creates a height map of the text, adds some smoothing, then converts the map to a normal map.

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>

Approximation

To render the text we need to create some shaders. As we are using a normal map the vertex shader can be very simple.

Vertex shader

We are using a quad to render the whole canvas. The vertex shader outputs the 4 corners and converts each corner to a texture coordinate.

#version 300 es
in vec2 vert;
out vec2 texCoord;
void main() { 
    texCoord = vert * 0.5 + 0.5;
    gl_Position = vec4(verts, 1, 1); 
}

Fragment shader

The fragment shader has 3 texture inputs. The normal map, and the reflection and refraction maps.

The fragment shader first works out if the pixel is part of the background, or on the text. If on the text it converts the RGB texture normal into a vector normal.

It then uses vector addition to get the reflected and refracted textures. Mixing those textures by the normal maps z value. In effect refraction is strongest when the normal is facing up and reflection strongest when normal facing away

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

That is the most basic form that will give the impression of reflection and refraction.

Example NOT REAL reflection refraction.

The example is a little more complex as the various textures have different sizes and thus need to be scaled in the fragment shader to be the correct size.

I have also added some tinting to both the refraction and reflections and mixed the reflection via a curve.

The background is scrolled to the mouse position. To match a background on the page you would move the canvas over the background.

There are a few #defines in the frag shader to control the settings. You could make them uniforms, or constants.

mixCurve controls the mix of reflect refract textures. Values < 1 > 0 ease out refraction, values > 1 ease out the reflection.

The normal map is one to one with rendered pixels. As 2D canvas rendering is rather poor quality you can get a better result by over sampling the normal map in the fragment shader.

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>

Personally I find this FX more visually pleasing than simulations based on real lighting models. Though keep in mind THIS IS NOT Refraction or Reflections.

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

如何在圆上创建玻璃效果?

来自分类Dev

如何使顶部具有可绘制效果的玻璃方形按钮?

来自分类Dev

如何在滚动条上制作蒙蒙的玻璃效果

来自分类Dev

纹理玻璃折射和透明度

来自分类Dev

在图像上添加磨砂玻璃效果

来自分类Dev

如何杀死所有玻璃鱼实例

来自分类Dev

JavaFX中的毛玻璃效果?

来自分类Dev

如何在GWT中将玻璃窗格覆盖在面板(div)上

来自分类Dev

是否可以创建没有语音命令的玻璃器皿?

来自分类Dev

如何使用自定义视图\布局创建玻璃卡?

来自分类Dev

玻璃:setText大小限制?如何显示全屏文字

来自分类Dev

动态背景JavaFX的霜玻璃效果

来自分类Dev

WPF XAML-图像的玻璃效果

来自分类Dev

如何计算玻璃汁的体积?

来自分类常见问题

如何使CSS玻璃/模糊效果适用于覆盖层?

来自分类Dev

如何使用CSS赋予图像哑光效果(非玻璃状)?

来自分类Dev

定制玻璃

来自分类Dev

如何强制重涂玻璃板?

来自分类Dev

如何捕获玻璃沉浸中的声音触发?

来自分类Dev

玻璃器皿如何用json开发

来自分类Dev

如何制作透明玻璃彩色背景

来自分类Dev

如何使“ ok玻璃”成为可能?

来自分类Dev

CSS模糊的玻璃效果滤镜不起作用

来自分类Dev

无论如何,有没有要在HTML内容上实现毛玻璃浮动div(类似于IO7)

来自分类Dev

如何在选择上创建淡入效果?

来自分类Dev

取下玻璃后如何在手机上使用触摸屏

来自分类Dev

如何在输入元素上创建具有不同颜色和宽度/高度的重叠边框?

来自分类Dev

如何在WPF中创建具有斜面压纹效果的按钮

来自分类Dev

可以在iOS SpriteKit中创建玻璃破碎动画吗?

Related 相关文章

热门标签

归档