Как создать текст из стекла на холсте с преломлением и отражением? - PullRequest
1 голос
/ 18 февраля 2020

То, чего я хотел бы достичь, близко к этому там . Вы также можете просто взглянуть на эти скриншоты.

Фактический результат

Обратите внимание, как преломление развивается, поскольку страница прокручивается вниз / вверх. При прокрутке также есть источник света, идущий справа налево.

После прокрутки

В идеале Я хотел бы, чтобы текст имел этот прозрачный стеклянный отражающий аспект, как на приведенном примере. Но также, чтобы отразить то, что позади, что, кажется, не имеет место здесь. Действительно, когда холст остается один, рефракция все еще происходит, поэтому я подозреваю, что эффекты сделаны, зная, что будет отображаться на заднем плане. Что касается меня, я бы хотел, чтобы динамически изменить то, что позади. Еще раз, я думаю, что я мог быть достигнут таким образом по причине, может быть, проблема с производительностью

Удалены все не холстовые элементы

Действительно, похоже, что оно основано на фоне, но фон не находится внутри холста. Также, как вы можете видеть, на следующем изображении эффект преломления все еще происходит, даже если фон удален.

Refraction

Источник света все еще там, и я подозреваю, что он использует какой-то метод приведения лучей / трассировки лучей. Я совсем не знаком с рисованием на холсте (за исключением использования p5. js для простых вещей), и мне потребовалось много времени, чтобы найти трассировку лучей, не имея представления о том, что я ищу.

.... Вопросы ....

  1. Как получить прозрачный светоотражающий аспект на тексте? Должно ли это быть достигнуто с помощью графических c инструментов дизайна? (Я не знаю, как получить объект (см. Скриншот ниже), который, кажется, впоследствии имеет привязку текстуры. Я даже не уверен, правильно ли я использую словарный запас, но, предположив, что да, я не знаю, как чтобы создать такую ​​текстуру.) текстовый объект без "текстуры"

  2. Как преломить все, что будет размещено за стеклом объект? (Прежде чем я пришел к выводу, что мне нужно использовать canvas, не только потому, что я нашел этот пример, но и из-за других соображений, связанных с проектом, над которым я работаю. Я потратил много времени на изучение достаточного svg для достичь того, что вы можете увидеть на следующем скриншоте, и не смог достичь того, что было нацелено. Я не хочу делать то же самое с кастингом лучей, таким образом, мой третий вопрос. Надеюсь, это понятно ... Все же преломленная часть есть, но выглядит намного менее реалистично c, чем в приведенном примере.) SVG

  3. Является ли приведение / отслеживание лучей правильный путь для достижения преломления? Можно ли будет использовать его, если его луч отслеживает все объекты позади.

Спасибо за ваше время и заботу.

1 Ответ

3 голосов
/ 19 февраля 2020

Отражение и преломление

В Интернете так много учебных пособий для достижения этого эффекта, что я не вижу смысла повторять их.

Этот ответ представляет собой приближение с использованием карты норм вместо 3D-модель и плоские карты текстур для представления карт отражений и преломлений, а не трехмерные текстуры, традиционно используемые для получения отражений и преломлений.

Создание карты нормалей.

Приведенный ниже фрагмент создает карта нормалей из входного текста с различными вариантами. Процесс достаточно быстрый (не в режиме реального времени) и будет заменять 3D-модель в решении рендеринга webGL.

Сначала создается карта высоты текста, добавляется некоторое сглаживание, а затем конвертируется карта. на карту нормалей.

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

Это самая основная c форма, которая создает впечатление отражения и преломления.

Пример НЕ РЕАЛЬНОЕ преломление отражения.

Пример немного сложнее, так как различные текстуры имеют разные размеры и, следовательно, должны быть масштабированы во фрагментном шейдере для получения правильного размера.

Я также добавил некоторые оттенки к преломлению и отражениям и смешал отражение с помощью кривой.

Фон прокручивается до положения мыши. Чтобы соответствовать фону на странице, вы должны переместить холст поверх фона.

В шейдере фрагмента есть несколько #defines для управления настройками. Вы можете сделать их униформой или константой.

mixCurve управляет сочетанием рефракционных текстур. Значения <1> 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 более визуально приятным, чем симуляции на основе реальных моделей освещения. Хотя имейте в виду, что это не рефракция или отражения.

...