Old school tv edge-warping effect?
Asked Answered
H

1

7

I am trying to achieve an old school tv effect. But I'm having trouble finding a good way to warp the edges of the screen, an effect you can see in this example.

Mario

I'm not asking anything about getting a glitchy, or static effect. Just the warping around the edges of the screen. I know this is definitely not possible with just plain css. Would this be possible in webGL or html5 Canvas?

Hsinking answered 20/10, 2017 at 21:56 Comment(7)
Indeed yes it is possible to acheive this using WebGL or even Contex2D... But if you speak about the rounded black border (or rounded border shape), i don't think WebGL/Contex2D are the simplest way to achieve this...Rochdale
@Sedenion Do you have a better way in mind? Perhaps a framework I could look into?Hsinking
My first idea is to use an PNG image as mask with a transparent center, and edges with the desired shape, with the same color as the page background. You place the PNG image as image in the <div>, and what you want to display within, as background of the <div>. But maybe this is not suitable for what you want to do.Rochdale
@Sedenion Yeah I see what you're saying. I don't think the shape would be too difficult to accomplish, it's the actual bulge effect that I'm having trouble with.Hsinking
Ok, I see, so indeed you need more elaborate image processing. With Context2D you will face the same limitation as my previous idea. With WebGL, you can map the image as texture on a mesh with the proper shape and volume (simplest way i see), which naturaly induce the right deformation (as a real curved screen) but you'll need the proper mesh with UV coordinates. Or using some post-processing shader with lot of mathematics (interesting but complex way)...Rochdale
@Sedenion is it possible to use that WebGL capability with an html element rather than an image? I was hoping to make the content inside of the "TV" interactive.Hsinking
Dynamically loading/changing a texture with some "onclick" or "onchange" trigger is easy, this isn't the biggest dilem you will face. However, rendering HTML content in WebGL needs HTML content to be transformed into an image, and here, things becom way more complexe. WebGL only know bitmap natively, it does'nt know anything about "CSS" or "DOM".Rochdale
K
25

Distortion effects can be achieved in a fragment shader, by displacing the texture coordinates. Strips or noise can be achieved by lighten or darken texels. You simple have to draw a rectangle over the entire viewport and to draw the source texture to rectangle, with the applied distortion effects.

A border distortion can be achieved by scaling the normalized device coordinates of the viewport. The scalind has to be increased in the corners of the viewport. After teh distortin the coordinates have be transformed to texture coordiantes (uv = ndc * 0.5 + 0.5):

vec2 ndc_pos = vertPos;
vec2 testVec = ndc_pos.xy / max(abs(ndc_pos.x), abs(ndc_pos.y));
float len = max(1.0,length( testVec ));
ndc_pos *= mix(1.0, mix(1.0,len,max(abs(ndc_pos.x), abs(ndc_pos.y))), u_distortion);
vec2 texCoord = vec2(ndc_pos.s, -ndc_pos.t) * 0.5 + 0.5;

Stripes can be achieved by lighten or darken the texels, according to the v (t) component of the texture coordinate:

float stripTile = texCoord.t * mix(10.0, 100.0, u_stripe);
float stripFac = 1.0 + 0.25 * u_stripe * (step(0.5, stripTile-float(int(stripTile))) - 0.5);

For an RGB displacement, the red, green and blue channels have to read from separated texels. For this for the separate channels have to be used slightly shifted texture coordinates:

float texR = texture2D( u_texture, texCoord.st-vec2(u_rgbshift) ).r;
float texG = texture2D( u_texture, texCoord.st ).g;
float texB = texture2D( u_texture, texCoord.st+vec2(u_rgbshift) ).b;


Original texture:
enter image description here


Preview:
enter image description here


Note, it should be easy to play around with the code and to apply your own effects or to fit them to your requirement.
See The WebGL example for the full fragment shader source code:

var canvas;
var gl;
var prog;
var bufObj = {};
var textureObj;
var maskTextureObj;
var ShProg = {};

function  render(delteMS){

    var vp = [canvas.width, canvas.height];
    var distortion = document.getElementById( "distortion" ).value / 100.0;
    var rgbShift = document.getElementById( "rgbshift" ).value / 1000.0;
    var stripes = document.getElementById( "stripes" ).value / 100.0;

    gl.viewport( 0, 0, canvas.width, canvas.height );
    gl.enable( gl.DEPTH_TEST );
    gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
    gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    var texUnit = 0;
    gl.activeTexture( gl.TEXTURE0 + texUnit );
    gl.bindTexture( gl.TEXTURE_2D, textureObj );
    ShProg.Use( progDraw );
    ShProg.SetI1( progDraw, "u_texture", texUnit );
    ShProg.SetF1( progDraw, "u_distortion", distortion );
    ShProg.SetF1( progDraw, "u_stripe", stripes );
    ShProg.SetF1( progDraw, "u_rgbshift", rgbShift );
    VertexBuffer.Draw( bufRect );

    requestAnimationFrame(render);
}  

function resize() {
    vp_size = [window.innerWidth, window.innerHeight];
    //vp_size = [256, 256]
    canvas.width = vp_size[0];
    canvas.height = vp_size[1];
}

function init() {

    canvas = document.getElementById( "retro-canvas");
    gl = canvas.getContext( "experimental-webgl" );
    //gl = canvas.getContext( "webgl2" );
    if ( !gl )
    return;

    var texCX = 128;
    var texCY = 128;
    var texPlan = [];
    for (ix = 0; ix < texCX; ++ix) {
        for (iy = 0; iy < texCY; ++iy) {
            var val_x = Math.sin( Math.PI * 6.0 * ix / texCX )
            var val_y = Math.sin( Math.PI * 6.0 * iy / texCY )
            texPlan.push( 128 + 127 * val_x, 63, 128 + 127 * val_y, 255 );
        }
    }

    textureObj = Texture.LoadTexture2D( "https://raw.githubusercontent.com/Rabbid76/graphics-snippets/master/resource/texture/supermario.jpg" );
    
    progDraw = ShProg.Create( 
    [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
        { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
    ] );
    progDraw.inPos = gl.getAttribLocation( progDraw.progObj, "inPos" );
    if ( progDraw.progObj == 0 )
        return;

    bufRect = VertexBuffer.Create(
    [ { data :  [ -1, -1, 1, -1, 1, 1, -1, 1 ], attrSize : 2, attrLoc : progDraw.inPos } ],
    [ 0, 1, 2, 0, 2, 3 ] );

    window.onresize = resize;
    resize();
    requestAnimationFrame(render);
}

var ShProg = {
Create: function (shaderList) {
    var shaderObjs = [];
    for (var i_sh = 0; i_sh < shaderList.length; ++i_sh) {
        var shderObj = this.Compile(shaderList[i_sh].source, shaderList[i_sh].stage);
        if (shderObj) shaderObjs.push(shderObj);
    }
    var prog = {}
    prog.progObj = this.Link(shaderObjs)
    if (prog.progObj) {
        prog.attrInx = {};
        var noOfAttributes = gl.getProgramParameter(prog.progObj, gl.ACTIVE_ATTRIBUTES);
        for (var i_n = 0; i_n < noOfAttributes; ++i_n) {
            var name = gl.getActiveAttrib(prog.progObj, i_n).name;
            prog.attrInx[name] = gl.getAttribLocation(prog.progObj, name);
        }
        prog.uniLoc = {};
        var noOfUniforms = gl.getProgramParameter(prog.progObj, gl.ACTIVE_UNIFORMS);
        for (var i_n = 0; i_n < noOfUniforms; ++i_n) {
            var name = gl.getActiveUniform(prog.progObj, i_n).name;
            prog.uniLoc[name] = gl.getUniformLocation(prog.progObj, name);
        }
    }
    return prog;
},
AttrI: function (prog, name) { return prog.attrInx[name]; },
UniformL: function (prog, name) { return prog.uniLoc[name]; },
Use: function (prog) { gl.useProgram(prog.progObj); },
SetI1: function (prog, name, val) { if (prog.uniLoc[name]) gl.uniform1i(prog.uniLoc[name], val); },
SetF1: function (prog, name, val) { if (prog.uniLoc[name]) gl.uniform1f(prog.uniLoc[name], val); },
SetF2: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform2fv(prog.uniLoc[name], arr); },
SetF3: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform3fv(prog.uniLoc[name], arr); },
SetF4: function (prog, name, arr) { if (prog.uniLoc[name]) gl.uniform4fv(prog.uniLoc[name], arr); },
SetM33: function (prog, name, mat) { if (prog.uniLoc[name]) gl.uniformMatrix3fv(prog.uniLoc[name], false, mat); },
SetM44: function (prog, name, mat) { if (prog.uniLoc[name]) gl.uniformMatrix4fv(prog.uniLoc[name], false, mat); },
Compile: function (source, shaderStage) {
    var shaderScript = document.getElementById(source);
    if (shaderScript)
        source = shaderScript.text;
    var shaderObj = gl.createShader(shaderStage);
    gl.shaderSource(shaderObj, source);
    gl.compileShader(shaderObj);
    var status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
    if (!status) alert(gl.getShaderInfoLog(shaderObj));
    return status ? shaderObj : null;
},
Link: function (shaderObjs) {
    var prog = gl.createProgram();
    for (var i_sh = 0; i_sh < shaderObjs.length; ++i_sh)
        gl.attachShader(prog, shaderObjs[i_sh]);
    gl.linkProgram(prog);
    status = gl.getProgramParameter(prog, gl.LINK_STATUS);
    if ( !status ) alert(gl.getProgramInfoLog(prog));
    return status ? prog : null;
} };

var VertexBuffer = {
Create: function(attribs, indices, type) {
    var buffer = { buf: [], attr: [], inx: gl.createBuffer(), inxLen: indices.length, primitive_type: type ? type : gl.TRIANGLES };
    for (var i=0; i<attribs.length; ++i) {
        buffer.buf.push(gl.createBuffer());
        buffer.attr.push({ size : attribs[i].attrSize, loc : attribs[i].attrLoc, no_of: attribs[i].data.length/attribs[i].attrSize });
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer.buf[i]);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array( attribs[i].data ), gl.STATIC_DRAW);
    }
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    if ( buffer.inxLen > 0 ) {
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.inx);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
    }
    return buffer;
},
Draw: function(bufObj) {
    for (var i=0; i<bufObj.buf.length; ++i) {
        gl.bindBuffer(gl.ARRAY_BUFFER, bufObj.buf[i]);
        gl.vertexAttribPointer(bufObj.attr[i].loc, bufObj.attr[i].size, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray( bufObj.attr[i].loc);
    }
    if ( bufObj.inxLen > 0 ) {
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufObj.inx);
        gl.drawElements(bufObj.primitive_type, bufObj.inxLen, gl.UNSIGNED_SHORT, 0);
        gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
    }
    else
        gl.drawArrays(bufObj.primitive_type, 0, bufObj.attr[0].no_of );
    for (var i=0; i<bufObj.buf.length; ++i)
        gl.disableVertexAttribArray(bufObj.attr[i].loc);
    gl.bindBuffer( gl.ARRAY_BUFFER, null );
} };

var Texture = {};
Texture.HandleLoadedTexture2D = function( image, texture, flipY ) {
    gl.activeTexture( gl.TEXTURE0 );
    gl.bindTexture( gl.TEXTURE_2D, texture );
    gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, flipY != undefined && flipY == true );
    gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image );
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE );
    gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE );
    gl.bindTexture( gl.TEXTURE_2D, null );
    return texture;
}
Texture.LoadTexture2D = function( name ) {
    var texture = gl.createTexture();
    texture.image = new Image();
    texture.image.setAttribute('crossorigin', 'anonymous');
    texture.image.onload = function () {
        Texture.HandleLoadedTexture2D( texture.image, texture, false )
    }
    texture.image.src = name;
    return texture;
}
       
init();
<style>
html,body { margin: 0; overflow: hidden; }
#gui { position : absolute; top : 0; left : 0; }  
</style>
<script id="draw-shader-vs" type="x-shader/x-vertex">
    precision mediump float;

    attribute vec2 inPos;
    varying   vec2 vertPos;

    void main()
    {
        vertPos     = inPos;
        gl_Position = vec4( inPos, 0.0, 1.0 );
    }
</script>

<script id="draw-shader-fs" type="x-shader/x-fragment">
    precision mediump float;

    varying vec2      vertPos;
    uniform sampler2D u_texture;
    uniform float     u_distortion;
    uniform float     u_stripe;
    uniform float     u_rgbshift;
    
    void main()
    {
        // distortion
        vec2 ndc_pos = vertPos;
        vec2 testVec = ndc_pos.xy / max(abs(ndc_pos.x), abs(ndc_pos.y));
        float len = max(1.0,length( testVec ));
        ndc_pos *= mix(1.0, mix(1.0,len,max(abs(ndc_pos.x), abs(ndc_pos.y))), u_distortion);
        vec2 texCoord = vec2(ndc_pos.s, -ndc_pos.t) * 0.5 + 0.5;

        // stripes
        float stripTile = texCoord.t * mix(10.0, 100.0, u_stripe);
        float stripFac = 1.0 + 0.25 * u_stripe * (step(0.5, stripTile-float(int(stripTile))) - 0.5);
        
        // rgb shift
        float texR = texture2D( u_texture, texCoord.st-vec2(u_rgbshift) ).r;
        float texG = texture2D( u_texture, texCoord.st ).g;
        float texB = texture2D( u_texture, texCoord.st+vec2(u_rgbshift) ).b;
        
        float clip = step(0.0, texCoord.s) * step(texCoord.s, 1.0) * step(0.0, texCoord.t) * step(texCoord.t, 1.0); 
        gl_FragColor  = vec4( vec3(texR, texG, texB) * stripFac * clip, 1.0 );
    }
</script>

<div>
<form id="gui" name="inputs">
    <table>
        <tr> <td> <font color= #FF8000>distortion</font> </td>
            <td> <input type="range" id="distortion" min="0" max="100" value="10"/> 
        </td> </tr>
        <tr> <td> <font color= #FF8000>stripes</font> </td>
            <td> <input type="range" id="stripes"  min="0" max="100" value="70"/> 
        </td> </tr>
        <tr> <td> <font color= #FF8000>RGB shift</font> </td>
            <td> <input type="range" id="rgbshift" min="0" max="100" value="10"/> 
        </td> </tr>
    </table>
</form>
</div>

<canvas id="retro-canvas" style="border: none;"></canvas>
Krak answered 21/10, 2017 at 18:22 Comment(2)
Wow thank you so much! Super detailed answer. I actually have some basic experience with fragment and vertex shaders with (Löve2d)[love2d.org/], just wasn't sure about the intricacies of implementing one into a web page. Super helpful!🙏Hsinking
Amazing, thanks a lot for demonstrating such a complete example!Barros

© 2022 - 2024 — McMap. All rights reserved.