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]) {
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>