<canvas id="webgl-canvas"></canvas>

<!-- vertexShader -->
<script id="js-vertex-shader" type="x-shader/x-vertex">
  attribute vec3 aBary;
  uniform float time;
  uniform float mouse;
  varying vec3 vNormal;
  varying vec3 eyeVector;
  varying vec3 vBary;
  
  //	Classic Perlin 3D Noise 
  //	by Stefan Gustavson
  //
  vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
  vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
  vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}

  float cnoise(vec3 P){
    vec3 Pi0 = floor(P); // Integer part for indexing
    vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
    Pi0 = mod(Pi0, 289.0);
    Pi1 = mod(Pi1, 289.0);
    vec3 Pf0 = fract(P); // Fractional part for interpolation
    vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
    vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
    vec4 iy = vec4(Pi0.yy, Pi1.yy);
    vec4 iz0 = Pi0.zzzz;
    vec4 iz1 = Pi1.zzzz;

    vec4 ixy = permute(permute(ix) + iy);
    vec4 ixy0 = permute(ixy + iz0);
    vec4 ixy1 = permute(ixy + iz1);

    vec4 gx0 = ixy0 / 7.0;
    vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
    gx0 = fract(gx0);
    vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
    vec4 sz0 = step(gz0, vec4(0.0));
    gx0 -= sz0 * (step(0.0, gx0) - 0.5);
    gy0 -= sz0 * (step(0.0, gy0) - 0.5);

    vec4 gx1 = ixy1 / 7.0;
    vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
    gx1 = fract(gx1);
    vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
    vec4 sz1 = step(gz1, vec4(0.0));
    gx1 -= sz1 * (step(0.0, gx1) - 0.5);
    gy1 -= sz1 * (step(0.0, gy1) - 0.5);

    vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
    vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
    vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
    vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
    vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
    vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
    vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
    vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);

    vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
    g000 *= norm0.x;
    g010 *= norm0.y;
    g100 *= norm0.z;
    g110 *= norm0.w;
    vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
    g001 *= norm1.x;
    g011 *= norm1.y;
    g101 *= norm1.z;
    g111 *= norm1.w;

    float n000 = dot(g000, Pf0);
    float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
    float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
    float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
    float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
    float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
    float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
    float n111 = dot(g111, Pf1);

    vec3 fade_xyz = fade(Pf0);
    vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
    vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
    float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 
    return 2.2 * n_xyz;
  }

  void main() {
    vBary = aBary;
    // 頂点法線ベクトルを視点座標系に変換する行列であるnormalMatrixを頂点法線ベクトルのnormalに乗算したものを、
    // normalize関数を使って正規化する。
    vNormal = normalize(normalMatrix * normal);
    
    // cnoise関数にvNormalとtimeの値を足したものを渡し、pow関数で3乗にしたものにmouseの値を乗算する。
    // これにより、mouseの値に応じてノイズが発生するようになる。
    float noisy = mouse * pow(cnoise(vNormal + time), 3.0);
    vec3 newPosition = position + noisy * normal;
    // modelMatrixを乗算し、newPosition(position)をオブジェクト座標からワールド座標に変換したものをworldPositionとする。
    vec4 worldPosition = modelMatrix * vec4(newPosition , 1.0);
    // worldPositionからcameraPositionを引いたものを正規化することで求められる視線ベクトルをeyeVectorとする。
    eyeVector = normalize(worldPosition.xyz - cameraPosition);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
  }
</script>

<!-- fragmentShader -->
<script id="js-fragment-shader" type="x-shader/x-fragment">
  precision highp float;
  
  uniform sampler2D t1;
  varying vec3 vNormal;
  varying vec3 eyeVector;
  float PI = 3.141592653589793238;

  vec2 hash22(vec2 p) {
    p = fract(p * vec2(5.3983, 5.4427));
    p += dot(p.xy, p.xy + vec2(21.5351, 14.3137));
    return fract(vec2(p.x * p.y * 95.4337, p.x * p.y * 97.597));
  }

  void main()	{
    // dFdx&dFdy関数(偏微分)に頂点法線ベクトル(vPosition)の値を渡して計算し、
    // cross関数でその外積を求めることで、面に対する法線ベクトルが求められ、
    // その結果、フラットシェーディングの表現ができる。
    vec3 X = dFdx(vNormal);
    vec3 Y = dFdy(vNormal);
    vec3 normal = normalize(cross(X, Y));
    float diffuse = dot(normal, vec3(1.0));
    vec2 rand = hash22(vec2(floor(diffuse * 10.0)));
    vec2 uvv = vec2(
      sign(rand.x - 0.5) * 1.0 + (rand.x - 0.5) * 0.6,
      sign(rand.y - 0.5) * 1.0 + (rand.y - 0.5) * 0.6
    );
    float fresnel = pow(1.0 + dot(eyeVector, normal), 2.0);
    vec2 uv = uvv * gl_FragCoord.xy / vec2(1000.0);
    vec3 refracted = refract(eyeVector, normal, 1.0 / 3.0);
    uv += 0.2 * refracted.xy;
    vec4 t = texture2D(t1, uv);

    gl_FragColor = t * (1.0 - fresnel);
  }
</script>

<!-- fragmentShader -->
<script id="js-fragment-shader-1" type="x-shader/x-fragment">
  precision highp float;

  varying vec3 vBary;

  void main()	{
    float width = 2.0;
    // fwidth関数を使って、dFdx(vBary)&dFdy(vBary)の絶対値を求める。
    vec3 d = fwidth(vBary);
    // smoothstep関数を使って、vBaryの値を線形補間する。
    vec3 s = smoothstep(d * (width + 0.5), d * (width - 0.5), vBary);
    // max関数(×2)使って、s.xと、s.yとs.zの大きい方の値とで大きい方の値を返す。
    float line = max(s.x , max(s.y, s.z));
    
    // lineの値が0.1より小さい時は、何も出力しないようにする。
    if(line < 0.1) discard;

    // 1.0(白)と0.0(黒)を、mix関数を使って1.0からlineの値を引いた値で線形補間する。
    // この時、lineの値を1.0から引くことで、出力される色が白になる。
    gl_FragColor = vec4(mix(vec3(1.0), vec3(0.0), 1.0 - line), 1.0);
  }
</script>
/**
 * Pixelation shader
 */

const PostProcessing = {
	uniforms: {
		'tDiffuse': { value: null },
    'howmuchrgbshifticanhaz': { value: 0.0 },
		'resolution': { value: null },
		'pixelSize': { value: 1.0 },
    'time': { value: 0.0 }
	},

	vertexShader: `
		varying highp vec2 vUv;
			void main() {
				vUv = uv;
				gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		}`,

	fragmentShader: `
		uniform sampler2D tDiffuse;
		uniform float pixelSize;
		uniform vec2 resolution;
    uniform float time;
    uniform float howmuchrgbshifticanhaz;
		varying highp vec2 vUv;

    float hash(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); }

		void main(){
      vec2 shift = vec2(0.01, 0.01) * howmuchrgbshifticanhaz;
    
      // make bw
      vec4 t = texture2D(tDiffuse, vUv);
      vec4 t1 = texture2D(tDiffuse, vUv + shift);
      vec4 t2 = texture2D(tDiffuse, vUv - shift);
      vec3 color = vec3((t.r + t.b + t.g) / 5.0);
      vec3 color1 = vec3((t1.r + t1.b + t1.g) / 5.0);
      vec3 color2 = vec3((t2.r + t2.b + t2.g) / 5.0);

      // rgb shift
      color = vec3(color1.r, color.g, color2.b);
      
      // noise
      float val = hash(vUv + time) * 0.3;
      
			vec2 dxy = pixelSize / resolution;
			vec2 coord = dxy * floor( vUv / dxy );
			gl_FragColor = texture2D(tDiffuse, vUv);
      gl_FragColor = vec4(color + vec3(val), 1.0);
		}`
};

class Stage {
  constructor() {
    this.renderParam = {
      width: window.innerWidth,
      height: window.innerHeight
    }
    
    this.cameraParam = {
      fov: 70,
      near: 0.01,
      far: 1000,
      lookAt: new THREE.Vector3(0, 0, 0),
      x: 0,
      y: 0,
      z: 2
    }
    
    this.scene = null;
    this.camera = null;
    this.renderer = null;
    
    this.isInitialized = false;
  }
  
  init() {
    this._setScene();
    this._setRender();
    this._setCamera();
    
    this.isInitialized = true;
  }
  
  _setScene() {
    this.scene = new THREE.Scene();
  }
  
  _setRender() {
    this.renderer = new THREE.WebGLRenderer({
      canvas: document.getElementById("webgl-canvas"),
      antialias: true,
      alpha: true
    });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize( this.renderParam.width, this.renderParam.height );
  }
  
  _setCamera() {
    const windowWidth = window.innerWidth;
    const windowHeight = window.innerHeight;
    
    if( !this.isInitialized ){
      this.camera = new THREE.PerspectiveCamera(
        this.cameraParam.fov,
        this.renderParam.width / this.renderParam.height,
        this.cameraParam.near,
        this.cameraParam.far
      );
      
      this.camera.position.set(
        this.cameraParam.x,
        this.cameraParam.y,
        this.cameraParam.z
      );
      this.camera.lookAt(this.cameraParam.lookAt);
    }
    
    this.camera.aspect = windowWidth / windowHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(windowWidth, windowHeight);
  }
  
  _render() {
    this.renderer.render(this.scene, this.camera);
  }
  
  onResize() {
    this._setCamera();
  }
  
  onRaf() {
    this._render();
  }
}

class Mesh {
  constructor(stage) {
    this.geometryParam = {
      width: 1,
      height: 1
    }
    
    this.t = new THREE.TextureLoader().load('https://i.postimg.cc/3xb2X6Mh/image.jpg');
    // wrapS:テクスチャが水平方向にどのようにラップされるか(UVのUに対応)
    // wrapT:テクスチャが垂直方向にどのようにラップされるか(UVのVに対応)
    // MirroredRepeatWrapping:テクスチャが無限に繰り返され、繰り返されるごとにミラーリングされる。
    this.t.wrapS = this.t.wrapT = THREE.MirroredRepeatWrapping;

    this.uniforms =  {
      time: { type: "f", value: 0.0 },
      mouse: { type: "f", value: 0.0 },
      t1: { type: "t", value: this.t }
    }

    this.stage = stage;
    this.mouse = 0;
  }
  
  init() {
    this._setMesh();
    this._setPostEffect();
    this.onMouseMove();
  }
  
  _setMesh() {
    const geometry = new THREE.IcosahedronGeometry(
      this.geometryParam.width,
      this.geometryParam.height
    );
    const geometry1 = new THREE.IcosahedronGeometry(
      1.001,
      this.geometryParam.height
    );
    const length = geometry1.attributes.position.array.length;
    const bary = [];
    
    for(let i = 0; i < length / 3; i++) {
      bary.push(0, 0, 1, 0, 1, 0, 1, 0, 0);
    }
    
    const aBary = new Float32Array(bary);
    geometry1.setAttribute("aBary", new THREE.BufferAttribute(aBary, 3), );

    this.material = new THREE.ShaderMaterial({
      vertexShader: document.getElementById("js-vertex-shader").textContent,
      fragmentShader: document.getElementById("js-fragment-shader").textContent,
      uniforms: this.uniforms,
      side: THREE.DoubleSide
    });
    
    this.material1 = new THREE.ShaderMaterial({
      vertexShader: document.getElementById("js-vertex-shader").textContent,
      fragmentShader: document.getElementById("js-fragment-shader-1").textContent,
      uniforms: this.uniforms,
      side: THREE.DoubleSide
    });
    
    this.mesh = new THREE.Mesh(geometry, this.material);
    this.meshLine = new THREE.Mesh(geometry1, this.material1);
    this.stage.scene.add(this.mesh);
    this.stage.scene.add(this.meshLine);
  }
  
  _setPostEffect() {
    this.composer = new THREE.EffectComposer(this.stage.renderer);
    this.composer.addPass(new THREE.RenderPass(this.stage.scene, this.stage.camera));
    this.customPass = new THREE.ShaderPass( PostProcessing );
    this.customPass.uniforms[ "resolution" ].value = new THREE.Vector2(window.innerWidth, window.innerHeight);
    this.customPass.uniforms[ "resolution" ].value.multiplyScalar(window.devicePixelRatio);
    this.composer.addPass(this.customPass);
  }
  
  _render() {
    this.uniforms.time.value += 0.001;
    this.mouse -= (this.mouse - this.speed) * 0.05;
    this.speed *= 0.99;
    this.stage.scene.rotation.x = this.uniforms.time.value;
    this.stage.scene.rotation.y = this.uniforms.time.value;
    this.material.uniforms.mouse.value = this.mouse;
    this.material1.uniforms.mouse.value = this.mouse;
    this.customPass.uniforms.time.value = this.uniforms.time.value;
    this.customPass.uniforms.howmuchrgbshifticanhaz.value = this.mouse / 5.0;
    this.composer.render();
  }
  
  onMouseMove() {
    this.lastX = 0;
    this.lastY = 0;
    this.speed = 0;

    window.addEventListener("mousemove", (e) => {
      this.speed = Math.sqrt((e.pageX - this.lastX) ** 2 + (e.pageY - this.lastY) ** 2.0) * 0.1;
      this.lastX = e.pageX;
      this.lastY = e.pageY;
    });
  }
  
  onResize() {
    this.composer.setSize(window.innerWidth, window.innerHeight);
  }
  
  onRaf() {
    this._render();
  }
}

(() => {  
  const stage = new Stage();
  const mesh = new Mesh(stage);
  
  stage.init();
  mesh.init();
  
  window.addEventListener("resize", () => {
    stage.onResize();
    mesh.onResize();
  });
  
  const _raf = () => {
    window.requestAnimationFrame(() => {
      stage.onRaf();
      mesh.onRaf();

      _raf();
    });
  }
  
  _raf();
})();
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
  2. https://unpkg.com/three@0.128.0/examples/js/postprocessing/EffectComposer.js
  3. https://unpkg.com/three@0.128.0/examples/js/postprocessing/RenderPass.js
  4. https://unpkg.com/three@0.128.0/examples/js/postprocessing/ShaderPass.js
  5. https://unpkg.com/three@0.128.0/examples/js/shaders/CopyShader.js