Cara Membuat Apple Fifth Avenue Cube di WebGL

123bisa.com

Tutorial tentang cara membuat kembali animasi Apple Fifth Avenue Cube menggunakan WebGL.


Lihat demoUnduh Sumber

Dari sponsor kami: Tumbuh dengan All-in-One Marketing Platform Mailchimp

Pada bulan September 2019 Apple membuka kembali pintu-pintu toko bersejarahnya di Fifth Avenue dan untuk merayakan acara khusus itu , Apple membuat halaman arahan dengan animasi kubus yang terbuat dari kaca yang sangat apik. Anda dapat melihat animasi asli di video ini .

Yang menarik perhatian saya adalah cara mereka bermain dengan kubus kaca yang terkenal untuk membuat pengumuman.

Sebagai Teknolog Kreatif, saya terus-menerus bereksperimen dan mempelajari potensi teknologi web, dan saya pikir mungkin menarik untuk mencoba meniru ini menggunakan WebGL.

Dalam tutorial ini saya akan menjelaskan langkah demi langkah teknik yang saya gunakan untuk membuat ulang animasi.

Anda akan membutuhkan tingkat pengetahuan menengah WebGL. Saya akan menghilangkan beberapa bagian kode untuk singkatnya dan menganggap Anda sudah tahu cara mengatur aplikasi WebGL. Teknik yang akan saya tampilkan dapat diterjemahkan ke pustaka / kerangka kerja WebGL apa pun.

Karena API WebGL sangat bertele-tele, saya memutuskan untuk pergi dengan Regl untuk eksperimen saya:

Regl adalah abstraksi fungsional baru untuk WebGL. Menggunakan Regl lebih mudah daripada menulis kode WebGL mentah karena Anda tidak perlu mengelola status atau mengikat; itu juga lebih ringan dan lebih cepat dan memiliki lebih sedikit overhead daripada banyak kerangka kerja 3d yang ada.

Menggambar kubus

Langkah pertama adalah membuat program untuk menggambar kubus.

Karena bentuk yang akan kita buat adalah prisma yang terbuat dari kaca, kita harus menjamin karakteristik berikut:

  • Itu harus transparan
  • Wajah internal kubus harus mencerminkan konten internal
  • Tepi kubus harus mendistorsi konten internal

Wajah depan dan belakang

Untuk mendapatkan yang kami inginkan, pada saat render kami akan menggambar bentuk dalam dua lintasan:

  1. Pada pass pertama kita hanya akan menggambar wajah belakang dengan pantulan internal.
  2. Pada lintasan kedua kita akan menggambar muka depan dengan konten setelah disamarkan dan terdistorsi di tepinya.

Menggambar bentuk dalam dua lintasan tidak berarti apa-apa selain memanggil program WebGL dua kali, tetapi dengan konfigurasi yang berbeda. WebGL memiliki konsep menghadap ke depan dan menghadap ke belakang dan ini memberi kita kemampuan untuk memutuskan apa yang harus diambil dengan memutar pada fitur wajah culling .

Dengan fitur yang dihidupkan, WebGL default ke “culling” kembali menghadap segitiga. “Memusnahkan” dalam hal ini adalah kata mewah untuk “tidak menggambar”.

– Dasar-dasar WebGL
// draw front faces
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);

// draw back faces
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT);

Sekarang kita telah melalui bagian pengaturan program, mari kita mulai membuat kubus.

Perbatasan berwarna

Yang ingin kami dapatkan adalah bentuk transparan dengan batas berwarna. Dari kubus putih datar, pada langkah pertama kita akan menambahkan warna pelangi dan kemudian kita akan menutupi dengan batas:

Pertama-tama buat fungsi GLSL yang mengembalikan pelangi:

const float PI2 = 6.28318530718;

vec4 radialRainbow(vec2 st, float tick) {
  vec2 toCenter = vec2(0.5) - st;
  float angle = mod((atan(toCenter.y, toCenter.x) / PI2) + 0.5 + sin(tick), 1.0);

  // colors
  vec4 a = vec4(0.15, 0.58, 0.96, 1.0);
  vec4 b = vec4(0.29, 1.00, 0.55, 1.0);
  vec4 c = vec4(1.00, 0.0, 0.85, 1.0);
  vec4 d = vec4(0.92, 0.20, 0.14, 1.0);
  vec4 e = vec4(1.00, 0.96, 0.32, 1.0);

  float step = 1.0 / 10.0;

  vec4 color = a;

  color = mix(color, b, smoothstep(step * 1.0, step * 2.0, angle));
  color = mix(color, a, smoothstep(step * 2.0, step * 3.0, angle));
  color = mix(color, b, smoothstep(step * 3.0, step * 4.0, angle));
  color = mix(color, c, smoothstep(step * 4.0, step * 5.0, angle));
  color = mix(color, d, smoothstep(step * 5.0, step * 6.0, angle));
  color = mix(color, c, smoothstep(step * 6.0, step * 7.0, angle));
  color = mix(color, d, smoothstep(step * 7.0, step * 8.0, angle));
  color = mix(color, e, smoothstep(step * 8.0, step * 9.0, angle));
  color = mix(color, a, smoothstep(step * 9.0, step * 10.0, angle));

  return color;
}

#pragma glslify: export(radialRainbow);

Glslify adalah sistem modul node.js-style yang memungkinkan kita membagi kode GLSL menjadi modul.

https://github.com/glslify/glslify

Sebelum melanjutkan, mari kita bicara sedikit tentang gl_FragCoord.

Hanya tersedia dalam bahasa fragmen, gl_FragCoord adalah variabel input yang berisi nilai koordinat relatif kanvas jendela (x, y, z, 1 / w) untuk fragmen.

– khronos.org

Jika Anda perhatikan, fungsi tersebut radialRainbowmemerlukan variabel yang disebut stsebagai parameter pertama, yang nilainya harus koordinat piksel relatif terhadap kanvas dan, seperti UVs, pergi antara 0 dan 1. Variabel stadalah hasil pembagian gl_FragCoorddengan resolusi:

/**
 * gl_FragCoord: pixel coordinates
 * u_resolution: the resolution of our canvas
 */
vec2 st = gl_FragCoord.xy / u_resolution;

Gambar berikut menjelaskan perbedaan antara menggunakan UVsdan st.

Setelah kami dapat merender gradien radial, mari buat fungsi untuk mendapatkan batas:

float borders(vec2 uv, float strokeWidth) {
  vec2 borderBottomLeft = smoothstep(vec2(0.0), vec2(strokeWidth), uv);

  vec2 borderTopRight = smoothstep(vec2(0.0), vec2(strokeWidth), 1.0 - uv);

  return 1.0 - borderBottomLeft.x * borderBottomLeft.y * borderTopRight.x * borderTopRight.y;
}

#pragma glslify: export(borders);

Dan kemudian shader fragmen terakhir kami:

precision mediump float;

uniform vec2 u_resolution;
uniform float u_tick;

varying vec2 v_uv;
varying float v_depth;

#pragma glslify: borders = require(borders.glsl);
#pragma glslify: radialRainbow = require(radial-rainbow.glsl);

void main() {
  // screen coordinates
  vec2 st = gl_FragCoord.xy / u_resolution;

  vec4 bordersColor = radialRainbow(st, u_tick);

  // opacity factor based on the z value
  float depth = clamp(smoothstep(-1.0, 1.0, v_depth), 0.6, 0.9);

  bordersColor *= vec4(borders(v_uv, 0.011)) * depth;

  gl_FragColor = bordersColor;
}

Menggambar konten

Harap dicatat bahwa  logo Apple  adalah merek dagang  Apple Inc. , terdaftar di AS dan negara lain. Kami hanya menggunakannya di sini untuk tujuan demonstrasi.

Sekarang kita memiliki kubus, saatnya untuk menambahkan logo Apple dan semua teks.

Jika Anda perhatikan, konten tidak hanya dirender di dalam kubus, tetapi juga pada tiga wajah belakang sebagai pantulan – itu berarti membuatnya empat kali. Untuk menjaga kinerja tinggi, kami hanya akan menggambarnya di luar layar saat render untuk kemudian menggunakannya di berbagai fragmen.

Di WebGL kita bisa melakukannya berkat FBO:

Frame buffer object architecture (FBO) adalah ekstensi untuk OpenGL untuk melakukan rendering di luar layar yang fleksibel, termasuk rendering ke tekstur. Dengan mengambil gambar yang biasanya ditarik ke layar, dapat digunakan untuk menerapkan berbagai macam filter gambar, dan efek pasca pemrosesan.

– Wikipedia

Di Regl cukup sederhana untuk bermain dengan FBO:

...

// here we'll put the logo and the texts
const textures = [
  ...
]

// we create the FBO
const contentFbo = regl.framebuffer()

// animate is executed at render time
const animate = ({viewportWidth, viewportHeight}) => {
  contentFbo.resize(viewportWidth, viewportHeight)

  // we tell WebGL to render off-screen, inside the FBO
  contentFbo.use(() => {
    /**
     * – Content program
     * It'll run as many times as the textures number
     */
    content({
      textures
    })
  })

  /**
   * – Cube program
   * It'll run twice, once for the back faces and once for front faces
   * Together with front faces we'll render the content as well
   */
  cube([
    {
      pass: 1,
      cullFace: 'FRONT',
    },
    {
      pass: 2,
      cullFace: 'BACK',
      texture: contentFbo, // we pass the FBO as a normal texture
    },
  ])
}

regl.frame(animate)

Dan kemudian perbarui shader fragmen kubus untuk membuat konten:

precision mediump float;

uniform vec2 u_resolution;
uniform float u_tick;
uniform int u_pass;
uniform sampler2D u_texture;

varying vec2 v_uv;
varying float v_depth;

#pragma glslify: borders = require(borders.glsl);
#pragma glslify: radialRainbow = require(radial-rainbow.glsl);

void main() {
  // screen coordinates
  vec2 st = gl_FragCoord.xy / u_resolution;

  vec4 texture;
  vec4 bordersColor = radialRainbow(st, u_tick);

  // opacity factor based on the z value
  float depth = clamp(smoothstep(-1.0, 1.0, v_depth), 0.6, 0.9);

  bordersColor *= vec4(borders(v_uv, 0.011)) * depth;

  if (u_pass == 2) {
    texture = texture2D(u_texture, st);
  }

  gl_FragColor = texture + bordersColor;
}

Masking

Dalam animasi Apple setiap permukaan kubus menunjukkan tekstur yang berbeda, itu berarti bahwa kita harus membuat topeng khusus yang mengikuti rotasi kubus.

Kami akan memberikan informasi untuk menutupi tekstur di dalam FBO yang akan kami sampaikan ke program konten.

Untuk setiap tekstur, mari kaitkan perbedaan maskId– setiap ID berhubungan dengan warna yang akan kita gunakan sebagai data uji:

const textures = [
  {
    texture: logoTexture,
    maskId: 1,
  },
  {
    texture: logoTexture,
    maskId: 2,
  },
  {
    texture: logoTexture,
    maskId: 3,
  },
  {
    texture: text1Texture,
    maskId: 4,
  },
  {
    texture: text2Texture,
    maskId: 5,
  },
]

Untuk membuat masing maskId– masing sesuai dengan warna, kita hanya perlu mengubahnya dalam biner dan kemudian membacanya sebagai RGB:

MaskID 1 => [0, 0, 1] => Blue
MaskID 2 => [0, 1, 0] => Lime
MaskID 3 => [0, 1, 1] => Cyan
MaskID 4 => [1, 0, 0] => Red
MaskID 5 => [1, 0, 1] => Magenta

Topeng akan menjadi apa-apa selain kubus kami dengan wajah diisi dengan salah satu warna yang ditunjukkan di atas – jelas dalam hal ini kita hanya perlu menggambar wajah depan:

...

maskFbo.use(() => {
  cubeMask([
    {
      cullFace: 'BACK',
      colorFaces: [
        [0, 1, 1], // front face => mask 3
        [0, 0, 1], // right face => mask 1
        [0, 1, 0], // back face => mask 2
        [0, 1, 1], // left face => mask 3
        [1, 0, 0], // top face => mask 4
        [1, 0, 1], // bottom face => mask 5
      ]
    },
  ])
});

contentFbo.use(() => {
  content({
    textures,
    mask: maskFbo
  })
})

...

Topeng kita akan terlihat seperti ini:

Sekarang kita memiliki topeng yang tersedia di dalam fragmen program konten, mari tuliskan tesnya:

precision mediump float;

uniform vec2 u_resolution;
uniform sampler2D u_texture;
uniform int u_maskId;
uniform sampler2D u_mask;

varying vec2 v_uv;

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution;

  vec4 texture = texture2D(u_texture, v_uv);

  vec4 mask = texture2D(u_mask, st);

  // convert the mask color from binary (rgb) to decimal
  int maskId = int(mask.r * 4.0 + mask.g * 2.0 + mask.b * 1.0);

  // if the test passes then draw the texture
  if (maskId == u_maskId) {
    gl_FragColor = texture;
  } else {
    discard;
  }
}

Distorsi

Distorsi pada tepinya adalah karakteristik yang memberikan perasaan material kaca.

Efeknya dicapai dengan hanya menggeser piksel di dekat tepi ke tengah setiap wajah – video berikut menunjukkan cara kerjanya:

Agar setiap piksel bergerak, kami membutuhkan dua informasi:

  1. Berapa banyak untuk memindahkan piksel
  2. Arah di mana kita ingin memindahkan piksel

Dua informasi ini terdapat di dalam Peta Pemindahan yang, seperti sebelumnya untuk mask, kami akan menyimpan dalam FBO yang akan kami sampaikan ke program konten:

...

displacementFbo.use(() => {
  cubeDisplacement([
    {
      cullFace: 'BACK'
    },
  ])
});

contentFbo.use(() => {
  content({
    textures,
    mask: maskFbo,
    displacement: displacementFbo
  })
})

...

Peta perpindahan yang akan kita gambar akan terlihat seperti ini:

Mari kita lihat secara detail bagaimana ini dibuat.

The saluran hijau adalah panjang , yang adalah berapa banyak bergerak pixel – lebih hijau semakin besar perpindahan. Karena distorsi harus ada hanya di tepinya, kita hanya perlu menggambar bingkai hijau di setiap wajah.

Untuk mendapatkan bingkai hijau kita hanya perlu menggunakan kembali fungsi perbatasan dan meletakkan hasilnya di gl_FragColorsaluran hijau:

precision mediump float;

varying vec2 v_uv;

#pragma glslify: borders = require(borders.glsl);

void main() {
  // Green channel – how much to move the pixel
  float length = borders(v_uv, 0.028) + borders(v_uv, 0.06) * 0.3;

  gl_FragColor = vec4(0.0, length, 0.0, 1.0);
}

The channel merah adalah arah , yang nilainya adalah sudut dalam radian. Menemukan nilai ini lebih rumit karena kita memerlukan posisi setiap titik relatif terhadap dunia – karena kubus kita berputar, bahkan UVsmengikutinya dan karenanya kita kehilangan referensi. Untuk menghitung posisi setiap piksel dalam kaitannya dengan pusat, kita membutuhkan dua variabel yang bervariasi dari vertex shader:

  1. v_point: posisi dunia dari piksel saat ini.
  2. v_center: posisi dunia dari pusat wajah.

Vertex shader:

precision mediump float;

attribute vec3 a_position;
attribute vec3 a_center;
attribute vec2 a_uv;

uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_world;

varying vec3 v_center;
varying vec3 v_point;
varying vec2 v_uv;

void main() {
  vec4 position = u_projection * u_view * u_world * vec4(a_position, 1.0);
  vec4 center = u_projection * u_view * u_world * vec4(a_center, 1.0);

  v_point = position.xyz;
  v_center = center.xyz;
  v_uv = a_uv;

  gl_Position = position;
}

Pada titik ini, dalam fragmen, kita hanya perlu menemukan jarak dari pusat, menghitung sudut relatif dalam radian dan meletakkan hasilnya di gl_FragColorsaluran merah – di sini shader diperbarui:

precision mediump float;

varying vec3 v_center;
varying vec3 v_point;
varying vec2 v_uv;

const float PI2 = 6.283185307179586;

#pragma glslify: borders = require(borders.glsl);

void main() {
  // Red channel – which direction to move the pixel
  vec2 toCenter = v_center.xy - v_point.xy;
  float direction = (atan(toCenter.y, toCenter.x) / PI2) + 0.5;

  // Green channel – how much to move the pixel
  float length = borders(v_uv, 0.028) + borders(v_uv, 0.06) * 0.3;

  gl_FragColor = vec4(direction, length, 0.0, 1.0);
}

Sekarang kami memiliki peta perpindahan kami, mari perbarui shader fragmen konten:

precision mediump float;

uniform vec2 u_resolution;
uniform sampler2D u_texture;
uniform int u_maskId;
uniform sampler2D u_mask;

varying vec2 v_uv;

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution;

  vec4 displacement = texture2D(u_displacement, st);
  // get the direction by taking the displacement red channel and convert it in a vector2
  vec2 direction = vec2(cos(displacement.r * PI2), sin(displacement.r * PI2));
  // get the length by taking the displacement green channel
  float length = displacement.g;

  vec2 newUv = v_uv;
  
  // calculate the new uvs
  newUv.x += (length * 0.07) * direction.x;
  newUv.y += (length * 0.07) * direction.y;

  vec4 texture = texture2D(u_texture, newUv);

  vec4 mask = texture2D(u_mask, st);

  // convert the mask color from binary (rgb) to decimal
  int maskId = int(mask.r * 4.0 + mask.g * 2.0 + mask.b * 1.0);

  // if the test passes then draw the texture
  if (maskId == u_maskId) {
    gl_FragColor = texture;
  } else {
    discard;
  }
}

Refleksi

Karena refleksi adalah topik yang cukup kompleks, saya hanya akan memberikan pengantar singkat tentang cara kerjanya sehingga Anda dapat lebih mudah memahami sumber yang saya bagikan.

Sebelum melanjutkan, perlu dipahami konsep kamera di WebGL. Kamera tidak lain adalah kombinasi dari dua matriks: view dan projection matrix .

The  matriks proyeksi  digunakan untuk mengkonversi koordinat ruang dunia ke klip koordinat ruang. Matriks proyeksi yang umum digunakan, matriks  perspektif , digunakan untuk meniru  efek  kamera tipikal yang berfungsi sebagai penampil bagi penampil di dunia virtual 3D. The  tampilan matrix  bertanggung jawab untuk memindahkan benda-benda di tempat kejadian untuk mensimulasikan posisi kamera sedang berubah.

– developer.mozilla.org

 Saya menyarankan agar Anda juga mengenal konsep-konsep ini sebelum kita menggali lebih dalam:

Dalam lingkungan 3D, refleksi diperoleh dengan membuat kamera untuk setiap permukaan reflektif dan menempatkannya berdasarkan posisi penonton – yaitu mata kamera utama.

Dalam kasus kami, setiap permukaan kubus adalah permukaan reflektif, itu berarti kami membutuhkan 6 kamera berbeda yang posisinya tergantung pada penampil dan rotasi kubus.

WebGL Cubemaps

Setiap kamera menghasilkan tekstur untuk setiap wajah kubus. Alih-alih membuat framebuffer tunggal untuk setiap wajah, kita dapat menggunakan teknik pemetaan kubus.

Jenis tekstur lainnya adalah  cubemap . Ini terdiri dari 6 tekstur yang mewakili 6 wajah kubus. Alih-alih koordinat tekstur tradisional yang memiliki 2 dimensi, cubemap menggunakan normal, dengan kata lain arah 3D. Bergantung pada arah, titik-titik normal salah satu dari 6 wajah kubus dipilih dan kemudian di dalam wajah itu piksel diambil sampelnya untuk menghasilkan warna.

– Dasar-dasar WebGL

Jadi kita hanya perlu menyimpan apa yang enam kamera “melihat” di sel kanan – ini adalah bagaimana kami cubemap akan terlihat seperti:

Mari perbarui fungsi bernyawa kami dengan menambahkan refleksi:

...

// this is a normal FBO
const contentFbo = regl.framebuffer()

// this is a cube FBO, that means it composed by 6 textures
const reflectionFbo = regl.framebufferCube(1024)

// animate is executed at render time
const animate = ({viewportWidth, viewportHeight}) => {
  contentFbo.resize(viewportWidth, viewportHeight)

  contentFbo.use(() => {
    ...
  })

  /**
   * – Reflection program
   * we'll iterate 6 times over the reflectionFbo and draw inside the 
   * result of each camera
   */
  reflection({
    reflectionFbo,
    cameraConfig,
    texture: contentFbo
  })

  /**
   * – Cube program
   * with the back faces we'll render the reflection as well
   */
  cube([
    {
      pass: 1,
      cullFace: 'FRONT',
      reflection: reflectionFbo,
    },
    {
      pass: 2,
      cullFace: 'BACK',
      texture: contentFbo,
    },
  ])
}

regl.frame(animate)

Dan kemudian perbarui shader fragmen kubus.

Dalam fragmen shader kita perlu menggunakan samplerCube, bukan sampler2D dan menggunakan teksturCube bukan tekstur2D. teksturCube mengambil arah vec3 sehingga kami melewati normalisasi normal. Karena normal bervariasi dan akan diinterpolasi, kita perlu menormalkannya.

– Dasar-dasar WebGL
precision mediump float;

uniform vec2 u_resolution;
uniform float u_tick;
uniform int u_pass;
uniform sampler2D u_texture;
uniform samplerCube u_reflection;

varying vec2 v_uv;
varying float v_depth;
varying vec3 v_normal;

#pragma glslify: borders = require(borders.glsl);
#pragma glslify: radialRainbow = require(radial-rainbow.glsl);

void main() {
  // screen coordinates
  vec2 st = gl_FragCoord.xy / u_resolution;

  vec4 texture;
  vec4 bordersColor = radialRainbow(st, u_tick);

  // opacity factor based on the z value
  float depth = clamp(smoothstep(-1.0, 1.0, v_depth), 0.6, 0.9);

  bordersColor *= vec4(borders(v_uv, 0.011)) * depth;

  // if u_pass is 1, we're drawing back faces
  if (u_pass == 1) {
    vec3 normal = normalize(v_normal);
    texture = textureCube(u_reflection, normal);
  }

  // if u_pass is 1, we're drawing back faces
  if (u_pass == 2) {
    texture = texture2D(u_texture, st);
  }

  gl_FragColor = texture + bordersColor;
}

Kesimpulan

Artikel ini mungkin memberi Anda gambaran umum tentang teknik yang saya gunakan untuk meniru animasi Apple. Jika Anda ingin mempelajari lebih lanjut, saya sarankan Anda mengunduh sumbernya dan melihat cara kerjanya. Jika Anda memiliki pertanyaan, jangan ragu untuk bertanya kepada saya di Twitter ( @lorenzocadamuro ); harap kamu menikmatinya!

Sumber : https://tympanus.net/codrops/2019/12/20/how-to-create-the-apple-fifth-avenue-cube-in-webgl/

Dukung kami berkembang dengan Subscribe