Membangun Shader Bahan Hybrid di WebGL dengan Solid.js

 – Beragampengetahuan
10 mins read

Membangun Shader Bahan Hybrid di WebGL dengan Solid.js – Beragampengetahuan

Blackbird adalah situs eksperimental yang menarik yang saya gunakan sebagai cara untuk terbiasa dengan WebGL di dalam Solid.js. Ini mengeksplorasi kisah bagaimana SR-71 dibangun dengan detail teknis yang super. Efek wireframe yang dibahas di sini membantu memvisualisasikan teknologi di bawah permukaan SR-71 sambil menjaga penampilan logam yang dipoles terlihat sesuai dengan estetika posisi.

Inilah efeknya di situs web Blackbird:

Dalam tutorial ini, kami akan membangun kembali efek dari awal: Render model dua kali, sekali padat dan setelah rangka pemadaman, dan kemudian campur keduanya bersama -sama di shader untuk transisi animasi yang halus. Hasil akhirnya adalah teknik fleksibel yang dapat Anda gunakan untuk wahyu teknis, hologram, atau setiap saat Anda ingin menunjukkan struktur dan permukaan objek 3D.

Berikut adalah tiga hal: sifat material, target rendering, dan gradien shader hitam dan putih. Mari kita mulai!

Contents

Tapi pertama -tama, tentang padatan

Solid.js bukan nama kerangka kerja yang Anda dengar banyak, saya beralih pekerjaan pribadi saya absurd Pengalaman pengembang minimal, karena JSX masih merupakan hal terbesar sejak diiris roti. Anda benar -benar tidak perlu menggunakan padatan demo ini.Anda dapat mengupasnya dan menggunakan Vanilla JS. Tapi siapa tahu, Anda mungkin menyukainya πŸ™‚

tertarik? Lihat Solid.js.

Mengapa saya beralih

TLDR: Tumpukan penuh JSX tidak memiliki semua pendapat selanjutnya dan Nuxt, dan seperti 8kb Gzped, gurun.

Versi teknis: Ditulis dalam jsx, tetapi tidak menggunakan dom virtual, jadi “reaktif” (pikirkan useState()) Seluruh komponen tidak rerender, hanya ada satu simpul dom. Itu juga berjalan homomorfis, jadi "use client" Itu adalah masa lalu.

Siapkan skenario kami

Kami tidak memerlukan efek liar: mesh, kamera, renderer, dan adegan akan melakukannya. Saya menggunakan pangkalan Stage Kelas (untuk penamaan dramatis) dapat mengontrol ketika mereka diinisialisasi.

Ukuran jendela pelacakan objek global

window.innerWidth Dan window.innerHeight Saat Anda menggunakannya, Document Reflow dipicu (lebih lanjut di Document Reflow). Jadi saya memasukkannya ke dalam objek, memperbarui hanya jika perlu, dan kemudian membaca dari objek, alih -alih menggunakan window dan menyebabkan refluks. Perhatikan bahwa ini diatur ke 0 Bukan nilai aktual secara default. window Dinilai sebagai undefined Saat menggunakan SSR, kami ingin menunggu untuk mengatur aplikasi ini sampai aplikasi kami diinstal, kelas GL diinisialisasi, dan window Tentukan untuk menghindari kesalahan yang disukai semua orang: Properti yang tidak ditentukan tidak dapat dibaca (baca “jendela”).

// src/gl/viewport.js

export const viewport = {
  width: 0,
  height: 0,
  devicePixelRatio: 1,
  aspectRatio: 0,
};

export const resizeViewport = () => {
  viewport.width = window.innerWidth;
  viewport.height = window.innerHeight;

  viewport.aspectRatio = viewport.width / viewport.height;

  viewport.devicePixelRatio = Math.min(window.devicePixelRatio, 2);
};

Tiga-pointer dasar. Adegan JS, Penyaji dan Kamera

Sebelum memberikan apa pun, kami membutuhkan kerangka kerja kecil untuk menangani pengaturan adegan kami, membuat loop, dan logika tuning. Alih -alih menyebarkannya ke beberapa file, kami membungkusnya menjadi satu Stage Inisialisasi kelas kamera, renderer, dan adegan di satu tempat. Ini memudahkan organisasi siklus hidup WebGL kami, terutama setelah kami mulai menambahkan objek dan efek yang lebih kompleks.

// src/gl/stage.js

import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';
import { viewport, resizeViewport } from './viewport';

class Stage {
  init(element) {
    resizeViewport() // Set the initial viewport dimensions, helps to avoid using window inside of viewport.js for SSR-friendliness
    
    this.camera = new PerspectiveCamera(45, viewport.aspectRatio, 0.1, 1000);
    this.camera.position.set(0, 0, 2); // back the camera up 2 units so it isn't on top of the meshes we make later, you won't see them otherwise.

    this.renderer = new WebGLRenderer();
    this.renderer.setSize(viewport.width, viewport.height);
    element.appendChild(this.renderer.domElement); // attach the renderer to the dom so our canvas shows up

    this.renderer.setPixelRatio(viewport.devicePixelRatio); // Renders higher pixel ratios for screens that require it.

    this.scene = new Scene();
  }

  render() {
    this.renderer.render(this.scene, this.camera);
    requestAnimationFrame(this.render.bind(this));
// All of the scenes child classes with a render method will have it called automatically
    this.scene.children.forEach((child) => {
      if (child.render && typeof child.render === 'function') {
        child.render();
      }
    });
  }

  resize() {
    this.renderer.setSize(viewport.width, viewport.height);
    this.camera.aspect = viewport.aspectRatio;
    this.camera.updateProjectionMatrix();

// All of the scenes child classes with a resize method will have it called automatically
    this.scene.children.forEach((child) => {
      if (child.resize && typeof child.resize === 'function') {
        child.resize();
      }
    });
  }
}

export default new Stage();

Ada juga jala mewah

Setelah kami siap untuk panggung, kami dapat memberikan beberapa rendering yang menarik. Ring Knot sempurna: ia memiliki banyak kurva dan detail untuk menampilkan wireframes dan umpan solid. Kami akan mulai dari yang sederhana MeshNormalMaterial Dalam mode bingkai baris, kita dapat dengan jelas melihat strukturnya sebelum memasuki versi Blend Shader.

// src/gl/torus.js

import { Mesh, MeshBasicMaterial, TorusKnotGeometry } from 'three';

export default class Torus extends Mesh {
  constructor() {
    super();

    this.geometry = new TorusKnotGeometry(1, 0.285, 300, 26);
    this.material = new MeshNormalMaterial({
      color: 0xffff00,
      wireframe: true,
    });

    this.position.set(0, 0, -8); // Back up the mesh from the camera so its visible
  }
}

Rekaman cepat tentang cahaya

Untuk kesederhanaan, kami menggunakan bahan meta-normal, jadi kami tidak perlu mengacaukan lampu. Efek awal pada Blackbird memiliki enam lampu, Terlalu banyak waaays. GPU saya di M1 Max saya tersumbat selama 30fps, mencoba membuat model yang kompleks Dan Pencahayaan pukul 6 real-time. Tetapi mengurangi menjadi hanya 2 lampu (terlihat sama secara visual) yang menjalankan 120fps OK. 3.Js tidak seperti blender, Anda dapat memasukkan 14 lampu ke dalam 14 lampu dan menyiksa daging sapi dengan rendering saat tidur. Ada konsekuensi untuk lampu di WebGL 🫠

Sekarang, komponen JSX padat yang memegang segalanya

// src/components/GlCanvas.tsx

import { onMount, onCleanup } from 'solid-js';
import Stage from '~/gl/stage';

export default function GlCanvas() {
// let is used instead of refs, these aren't reactive
  let el;
  let gl;
  let observer;

  onMount(() => {
    if(!el) return
    gl = Stage;

    gl.init(el);
    gl.render();


    observer = new ResizeObserver((entry) => gl.resize());
    observer.observe(el); // use ResizeObserver instead of the window resize event. 
    // It is debounced AND fires once when initialized, no need to call resize() onMount
  });

  onCleanup(() => {
    if (observer) {
      observer.disconnect();
    }
  });


  return (
    <div
      ref={el}
      style={{
        position: 'fixed',
        inset: 0,
        height: '100lvh',
        width: '100vw',
      }}
      
    />
  );
}

let Digunakan untuk mendeklarasikan wasit, tidak ada formal useRef() Fungsi padat. Sinyal adalah satu -satunya cara untuk bereaksi. Baca lebih lanjut tentang wasit dalam solid.

Kemudian ketik komponen ke app.tsx:

// src/app.tsx

import { Router } from '@solidjs/router';
import { FileRoutes } from '@solidjs/start/router';
import { Suspense } from 'solid-js';
import GlCanvas from './components/GlCanvas';

export default function App() {
  return (
    <Router
      root={(props) => (
        <Suspense>
          {props.children}
          <GlCanvas />
        </Suspense>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

Setiap bagian 3D yang saya gunakan terikat ke elemen tertentu pada halaman (biasanya untuk jadwal dan pengguliran), jadi saya membuat komponen terpisah untuk mengontrol setiap kelas. Ini membantu saya tetap terorganisir ketika saya memiliki 5 atau 6 momen WebGL di halaman.

// src/components/WireframeDemo.tsx

import { createEffect, createSignal, onMount } from 'solid-js'
import Stage from '~/gl/stage';
import Torus from '~/gl/torus';

export default function WireframeDemo() {
  let el;
  const [element, setElement] = createSignal(null);
  const [actor, setActor] = createSignal(null);

  createEffect(() => {
    setElement(el);
    if (!element()) return;

    setActor(new Torus()); // Stage is initialized when the page initially mounts, 
    // so it's not available until the next tick. 
    // A signal forces this update to the next tick, 
    // after Stage is available.

    Stage.scene.add(actor());
  });

  return <div ref={el} />;
}

createEffect() Alih-alih onMount(): Ini akan secara otomatis melacak dependensi (elementDan actor Dalam hal ini) dan saat diubah, tidak lagi useEffect() Array dengan dependensi πŸ™ƒ. Baca lebih lanjut tentang Efeksi Kreasi di Solid.

Kemudian, tempatkan komponen pada rute minimum:

// src/routes/index.tsx

import WireframeDemo from '~/components/WiframeDemo';

export default function Home() {
  return (
    <main>
      <WireframeDemo />
    </main>
  );
}
Bagan menunjukkan struktur folder proyek kode

Sekarang Anda akan melihat ini:

Rainbow Ring Knot

Beralih material ke wireframe

SAYA Dicintai Gaya Wireframe Situs Web Blackbird! Ini sesuai dengan nuansa prototipe cerita, model bertekstur terlalu bersih, wireframe agak “kotor” dan belum dipoles. Anda dapat membuat gambar hampir dalam tiga potong material.

// /gl/torus.js

  this.material.wireframe = true
  this.material.needsUpdate = true;
Rainbow Ring Knot Berubah dari Wireframe ke Warna Solid

Namun, kami ingin melakukan ini secara dinamis hanya bagian dari model, bukan selama proses.

Masukkan target rendering.

Bagian yang menarik: memberikan target

Target rendering adalah topik yang sangat mendalam, tetapi mereka bermuara pada ini: apa pun yang Anda lihat di layar adalah bingkai yang diberikan oleh GPU, di WebGL Anda dapat mengekspor bingkai itu dan menggunakannya kembali sebagai tekstur pada kisi -kisi lain, dan Anda membuat “target” untuk output yang diberikan, yaitu target render.

Karena kami akan membutuhkan dua tujuan ini, kami dapat menyelesaikan kelas dan menggunakannya kembali.

// src/gl/render-target.js

import { WebGLRenderTarget } from 'three';
import { viewport } from '../viewport';
import Torus from '../torus';
import Stage from '../stage';

export default class RenderTarget extends WebGLRenderTarget {
  constructor() {
    super();

    this.width = viewport.width * viewport.devicePixelRatio;
    this.height = viewport.height * viewport.devicePixelRatio;
  }

  resize() {
    const w = viewport.width * viewport.devicePixelRatio;
    const h = viewport.height * viewport.devicePixelRatio;

    this.setSize(w, h)
  }
}

Ini hanya output dari tekstur, itu saja.

Sekarang kita dapat menyelesaikan kelas yang akan mengkonsumsi output ini. Saya tahu, ini banyak kursus, tetapi memisahkan unit tunggal seperti itu dapat membantu saya melacak di mana hal -hal terjadi. Pasta 800 baris adalah mimpi buruk saat men-debug WebGL.

// src/gl/targeted-torus.js

import {
  Mesh,
  MeshNormalMaterial,
  PerspectiveCamera,
  PlaneGeometry,
} from 'three';
import Torus from './torus';
import { viewport } from './viewport';
import RenderTarget from './render-target';
import Stage from './stage';

export default class TargetedTorus extends Mesh {
  targetSolid = new RenderTarget();
  targetWireframe = new RenderTarget();

  scene = new Torus(); // The shape we created earlier
  camera = new PerspectiveCamera(45, viewport.aspectRatio, 0.1, 1000);
  
  constructor() {
    super();

    this.geometry = new PlaneGeometry(1, 1);
    this.material = new MeshNormalMaterial();
  }

  resize() {
    this.targetSolid.resize();
    this.targetWireframe.resize();

    this.camera.aspect = viewport.aspectRatio;
    this.camera.updateProjectionMatrix();
  }
}

Sekarang, ganti milik kami WireframeDemo.tsx Komponen digunakan TargetedTorus Ambil kelas, tidak Torus:

// src/components/WireframeDemo.tsx 

import { createEffect, createSignal, onMount } from 'solid-js';
import Stage from '~/gl/stage';
import TargetedTorus from '~/gl/targeted-torus';

export default function WireframeDemo() {
  let el;
  const [element, setElement] = createSignal(null);
  const [actor, setActor] = createSignal(null);

  createEffect(() => {
    setElement(el);
    if (!element()) return;

    setActor(new TargetedTorus()); // << change me

    Stage.scene.add(actor());
  });

  return <div ref={el} data-gl="wireframe" />;
}

“Sekarang yang saya lihat hanyalah nathan persegi biru, rasanya kita akan mundur dan menunjukkan bentuk keren lagi”.

Shh, aku bersumpah itu desain!

Dari meshnormalalmalalal ke shadermaterialalal

Sekarang kita dapat membuat cincin kita ke output dan menggunakannya sebagai tekstur ShaderMaterial. MeshNormalMaterial Kami tidak diizinkan menggunakan tekstur, kami membutuhkan shader. intern targeted-torus.js menghapus MeshNormalMaterial Kemudian beralih ke:

// src/gl/targeted-torus.js

this.material = new ShaderMaterial({
  vertexShader: `
    varying vec2 v_uv;

    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      v_uv = uv;
    }
  `,
  fragmentShader: `
    varying vec2 v_uv;
    varying vec3 v_position;

    void main() {
      gl_FragColor = vec4(0.67, 0.08, 0.86, 1.0);
    }
  `,
});

Sekarang kami memiliki file banyak Pesawat ungu yang lebih indah dengan bantuan dua shader:

  • Vertex Shader memanipulasi posisi simpul material, kami tidak akan menyentuhnya lagi
  • Fragmen shader memberikan warna dan properti untuk setiap piksel bahan kami. Shader ini memberi tahu setiap piksel itu ungu

Gunakan Tekstur Target Rendering

Untuk menunjukkan cincin kita, bukan warna ungu, kita bisa lewat uniforms:

// src/gl/targeted-torus.js

this.material = new ShaderMaterial({
  vertexShader: `
    varying vec2 v_uv;

    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      v_uv = uv;
    }
  `,
  fragmentShader: `
    varying vec2 v_uv;
    varying vec3 v_position;

    // declare 2 uniforms
    uniform sampler2D u_texture_solid;
    uniform sampler2D u_texture_wireframe;

    void main() {
      // declare 2 images
      vec4 wireframe_texture = texture2D(u_texture_wireframe, v_uv);
      vec4 solid_texture = texture2D(u_texture_solid, v_uv);

      // set the color to that of the image
      gl_FragColor = solid_texture;
    }
  `,
  uniforms: {
    u_texture_solid: { value: this.targetSolid.texture },
    u_texture_wireframe: { value: this.targetWireframe.texture },
  },
});

Dan di kami TargetedTorus Kelas (ini Stage kelas):

// src/gl/targeted-torus.js

render() {
  this.material.uniforms.u_texture_solid.value = this.targetSolid.texture;

  Stage.renderer.render(this.scene, this.camera);
  Stage.renderer.setRenderTarget(this.targetSolid);
  Stage.renderer.clear();
  Stage.renderer.setRenderTarget(null);
}

Cincin itu kembali. Kami telah meneruskan tekstur gambar ke shader dan output render aslinya.

Campur wireframe dan bahan padat dengan shader

Sebelum proyek ini, shader adalah keajaiban gelap bagi saya. Ini adalah pertama kalinya saya menggunakannya dalam produksi dan saya terbiasa dengan tempat Anda berpikir di dalam kotak. Shader adalah koordinat 0 hingga 1, yang menurut saya sulit dimengerti. Namun, saya menggunakan Photoshop dan setelah efek dan melakukan hierarki beberapa kali. Aplikasi ini melakukan banyak shader kerja yang sama yang dapat: komputasi GPU. Ini membuatnya jauh lebih mudah. Pertama -tama, untuk menggambarkan atau menggambar apa yang saya inginkan, memikirkan tentang bagaimana saya melakukan ini di photoshop, Kemudian Tanyakan pada diri Anda bagaimana melakukannya dengan shader. Photoshop atau AE Entry Shader adalah Jauh Ketika Anda tidak memiliki fondasi yang mendalam di shader Anda, pajak atas pajak spiritual kurang dipungut.

Isi dua target rendering

Saat ini, kami hanya menyimpan data solidTarget Render target secara normal. Kami akan memperbarui loop render sehingga shader kami memiliki keduanya dan mereka wireframeTarget Tersedia pada saat yang sama.

// src/gl/targeted-torus.js

render() {
  // Render wireframe version to wireframe render target
  this.scene.material.wireframe = true;
  Stage.renderer.setRenderTarget(this.targetWireframe);
  Stage.renderer.render(this.scene, this.camera);
  this.material.uniforms.u_texture_wireframe.value = this.targetWireframe.texture;

  // Render solid version to solid render target
  this.scene.material.wireframe = false;
  Stage.renderer.setRenderTarget(this.targetSolid);
  Stage.renderer.render(this.scene, this.camera);
  this.material.uniforms.u_texture_solid.value = this.targetSolid.texture;

  // Reset render target
  Stage.renderer.setRenderTarget(null);
}

Dengan cara ini Anda berakhir dengan aliran di bawah topeng, yang terlihat seperti ini:

Bagan dengan garis merah menjelaskan data yang dilewati

Memudar di antara dua tekstur

Shader fragmen kami akan mendapatkan beberapa pembaruan, 2 menambahkan:

  • SmoothStep menciptakan kemiringan linier antara 2 nilai. Sinar UV hanya berkisar dari 0 hingga 1, jadi dalam hal ini kami menggunakan .15 Dan .65 Dengan batas (sepertinya membuat efek lebih jelas dari 0 dan 1). Kami kemudian menggunakan nilai x UV untuk menentukan nilai mana yang akan dimasukkan ke dalam langkah perataan.
  • vec4 mixed = mix(wireframe_texture, solid_texture, blend); Campurkan apa yang dikatakannya adalah mencampur 2 nilai bersama -sama pada rasio yang ditentukan dengan pencampuran. .5 Ini perpecahan yang sempurna.
// src/gl/targeted-torus.js

fragmentShader: `
  varying vec2 v_uv;
  varying vec3 v_position;

  // declare 2 uniforms
  uniform sampler2D u_texture_solid;
  uniform sampler2D u_texture_wireframe;

  void main() {
    // declare 2 images
    vec4 wireframe_texture = texture2D(u_texture_wireframe, v_uv);
    vec4 solid_texture = texture2D(u_texture_solid, v_uv);

    float blend = smoothstep(0.15, 0.65, v_uv.x);
    vec4 mixed = mix(wireframe_texture, solid_texture, blend);        

    gl_FragColor = mixed;
  }
`,

dan kemakmuran, mencampur:

Rainbow Ring Knot dengan Tekstur Wireframe

Sejujurnya, ini terlihat membosankan karena kita dapat menambahkan konsentrasi pada ini dengan sihir kecil GSAP.

// src/gl/torus.js

import {
  Mesh,
  MeshNormalMaterial,
  TorusKnotGeometry,
} from 'three';
import gsap from 'gsap';

export default class Torus extends Mesh {
  constructor() {
    super();

    this.geometry = new TorusKnotGeometry(1, 0.285, 300, 26);
    this.material = new MeshNormalMaterial();

    this.position.set(0, 0, -8);

    // add me!
    gsap.to(this.rotation, {
      y: 540 * (Math.PI / 180), // needs to be in radians, not degrees
      ease: 'power3.inOut',
      duration: 4,
      repeat: -1,
      yoyo: true,
    });
  }
}

Terima kasih!

Selamat, Anda telah secara resmi menghabiskan bagian yang dapat diukur dari hari itu mencampurkan kedua bahan itu bersama -sama. Layak, bukan? Setidaknya, saya harap ini menyelamatkan Anda Beberapa Senam mental merencanakan sepasang tujuan memberikan.

Apakah ada masalah? Ketuk saya di Twitter!

rencana pengembangan website



metode pengembangan website

jelaskan beberapa rencana untuk pengembangan website, proses pengembangan website, kekuatan dan kelemahan bisnis pengembangan website
, jasa pengembangan website, tahap pengembangan website, biaya pengembangan website

#Membangun #Shader #Bahan #Hybrid #WebGL #dengan #Solid.js

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *