Cara membuat adegan perjalanan cahaya cair menggunakan bahasa shading Three.js

 – Beragampengetahuan
13 mins read

Cara membuat adegan perjalanan cahaya cair menggunakan bahasa shading Three.js – Beragampengetahuan

Saya selalu terpesona oleh shader. Gagasan bahwa potongan kode dapat menciptakan beberapa efek visual paling menakjubkan yang pernah Anda lihat di video game, film, dan web mendorong saya untuk mempelajarinya sebanyak mungkin.

Selama perjalanan itu saya melihat video berjudul “Menggambar Karakter dengan Matematika” oleh Inigo Quilez. Sejujurnya, ini adalah contoh menarik dari sebuah teknologi yang disebut perjalanan ringan. Pada dasarnya, ini adalah cara untuk membuat atau merender adegan 2D dan 3D yang kompleks dalam satu shader fragmen tanpa memerlukan model atau material yang rumit.

Meskipun contoh ini mengesankan, namun juga cukup mengintimidasi! Jadi, untuk memudahkan kita memahami konsep ini, kita akan mengeksplorasi sesuatu seperti metaball, bentuk cairan kental yang terlihat sangat keren dan tampak menyerap satu sama lain dengan cara yang menarik.

Raymarching adalah topik yang sangat besar untuk dibahas, tetapi jika Anda tertarik untuk mendalami lebih dalam, tersedia beberapa sumber daya dan tutorial yang bagus dan mendalam. Dalam tutorial ini kita akan mengembangkan teknik ray marching berdasarkan tutorial Kishimisu: Pengantar Ray Marching, serta banyak referensi dari aset 3D SDF Inigo Quilez. Jika Anda menginginkan sesuatu yang lebih mendalam, saya sangat merekomendasikan Gambar Matematika: Studi Lembut tentang Perjalanan Cahaya oleh Maxime Heckel.

Dalam tutorial ini, kita akan menggunakan React Three Fiber (R3F) dan Three.js Shader Language (TSL) untuk membangun adegan perjalanan sinar sederhana dengan pencahayaan yang menarik. Anda memerlukan pengetahuan tentang Three.js dan React, namun teknik di sini dapat diterapkan pada bahasa bayangan apa pun, seperti GLSL dan kerangka kerja WebGL apa pun (jadi OGL atau vanilla pasti bisa dilakukan).

Contents

mempersiapkan

Kami akan menggunakan bahasa bayangan Three.js, bahasa baru dan berkembang yang dirancang untuk menurunkan hambatan masuk dalam membuat shader dengan menyediakan lingkungan yang mudah didekati bagi mereka yang kurang memahami hal-hal seperti GLSL atau WGSL.

TSL saat ini membutuhkan WebGPURenderer di Three.js. Artinya jika WebGPU tersedia, TSL yang kita tulis akan dikompilasi ke WGSL (bahasa bayangan yang digunakan di WebGPU) dan dikembalikan ke GLSL (WebGL) jika diperlukan. Saat kita menggunakan R3F, kita akan menyiapkan kanvas dan pemandangan yang sangat mendasar menggunakan satu bidang, dan seragam yang berisi informasi tentang resolusi layar yang akan kita gunakan dalam adegan perjalanan sinar. Pertama, kita perlu menyiapkan Canvas di R3F:

import { Canvas, CanvasProps } from '@react-three/fiber'
import { useEffect, useState } from 'react'

import { AdaptiveDpr } from '@react-three/drei'

import WebGPUCapabilities from 'three/examples/jsm/capabilities/WebGPU.js'
import WebGPURenderer from 'three/examples/jsm/renderers/webgpu/WebGPURenderer.js'
import { ACESFilmicToneMapping, SRGBColorSpace } from 'three'

const WebGPUCanvas = ({
  webglFallback = true,
  frameloop = 'always',
  children,
  debug,
  ...props
}) => {
  const [canvasFrameloop, setCanvasFrameloop] = useState('never')
  const [initialising, setInitialising] = useState(true)

  useEffect(() => {
    if (initialising) return

    setCanvasFrameloop(frameloop)
  }, [initialising, frameloop])

  const webGPUAvailable = WebGPUCapabilities.isAvailable()

  return (
    <Canvas
      {...props}
      id='gl'
      frameloop={canvasFrameloop}
      gl={(canvas) => {
        const renderer = new WebGPURenderer({
          canvas: canvas,
          antialias: true,
          alpha: true,
          forceWebGL: !webGPUAvailable,
        })
        renderer.toneMapping = ACESFilmicToneMapping
        renderer.outputColorSpace = SRGBColorSpace
        renderer.init().then(() => {
          setInitialising(false)
        })

        return renderer
      }}
    >
      <AdaptiveDpr />

      {children}
    </Canvas>
  )
}

Sekarang kita telah menyelesaikan pengaturannya, mari buat komponen dasar untuk adegan tersebut menggunakan MeshBasicNodeMaterial di mana kita akan menulis kode shader. Mulai sekarang, semua kode kita akan ditulis untuk materi ini.

import { useThree } from '@react-three/fiber'

import {
  MeshBasicNodeMaterial,
  uniform,
  uv,
  vec3,
  viewportResolution
} from 'three/nodes'

const raymarchMaterial = new MeshBasicNodeMaterial()

raymarchMaterial.colorNode = vec3(uv(), 1)

const Raymarch = () => {
  const { width, height } = useThree((state) => state.viewport)

  return (
    <mesh scale={[width, height, 1]}>
      <planeGeometry args={[1, 1]} />
      <primitive object={raymarchMaterial} attach='material' />
    </mesh>
  )
}

Buat lingkaran Raymarching

Raymarching, di mana paling Dasar, melibatkan langkah sedikit demi sedikit (disebut berbaris) di sepanjang pancaran sinar dari suatu titik asal (seperti kamera) dan menguji perpotongan dengan objek dalam pemandangan. Proses ini berlanjut hingga suatu benda tertabrak, atau kita mencapai jarak maksimal dari titik asal. Karena ini ditangani dalam shader fragmen, proses ini terjadi untuk setiap piksel gambar keluaran dalam pemandangan. (Perhatikan bahwa semua fungsi baru seperti float atau vec3 diimpor dari Three/nodes).

const sdf = tslFn(([pos]: any) => {
  // This is our main "scene" where objects will go, but for now return 0
  return float(0)
})

const raymarch = tslFn(() => {
  // Use frag coordinates to get an aspect-fixed UV
  const _uv = uv().mul(viewportResolution.xy).mul(2).sub(viewportResolution.xy).div(viewportResolution.y)

  // Initialize the ray and its direction
  const rayOrigin = vec3(0, 0, -3)
  const rayDirection = vec3(_uv, 1).normalize()

  // Total distance travelled - note that toVar is important here so we can assign to this variable
  const t = float(0).toVar()

  // Calculate the initial position of the ray - this var is declared here so we can use it in lighting calculations later
  const ray = rayOrigin.add(rayDirection.mul

  loop({ start: 1, end: 80 }, () => {
    const d = sdf(ray) // current distance to the scene

    t.addAssign(d) // "march" the ray

    ray.assign(rayOrigin.add(rayDirection.mul

    // If we're close enough, it's a hit, so we can do an early return
    If(d.lessThan(0.001), () => {
      Break()
    })

    // If we've travelled too far, we can return now and consider that this ray didn't hit anything
    If(t.greaterThan(100), () => {
      Break()
    })
  })

  // Some very basic shading here - objects that are closer to the rayOrigin will be dark, and objects further away will be lighter
  return vec3(t.mul(0.2))
})()

raymarchMaterial.colorNode = raymarch

Anda mungkin memperhatikan bahwa kami tidak benar-benar mengujinya tepat persimpangan, dan kami tidak menggunakan jarak tetap untuk setiap langkah. Jadi, bagaimana kita tahu jika cahaya kita “menabrak” suatu objek di tempat kejadian? Jawabannya adalah bahwa adegan tersebut terdiri dari bidang jarak bertanda tangan (SDF).

SDF didasarkan pada konsep penghitungan jarak terpendek dari titik mana pun dalam ruang ke permukaan suatu bentuk. Oleh karena itu, SDF mengembalikan nilai positif jika titik tersebut terletak di luar bentuk, nilai negatif jika terletak di dalam bentuk, dan nol jika terletak tepat di permukaan.

Dengan mengingat hal ini, kita sebenarnya hanya perlu menentukan apakah cahaya tersebut “cukup dekat” dengan permukaan untuk mengenainya. Setiap langkah berturut-turut memindahkan jarak ke permukaan terdekat, jadi setelah kita melewati ambang kecil yang mendekati 0, kita secara efektif “menabrak” suatu permukaan, sehingga memungkinkan kita untuk kembali lebih awal.

(Jika kita terus melakukannya hingga jaraknya 0, kita sebenarnya akan terus menjalankan perulangan sampai kita kehabisan iterasi, yang akan memberikan hasil yang kita inginkan, namun kurang efisien.)

Tambahkan bentuk SDF

Fungsi SDF yang kami miliki di sini adalah fungsi praktis untuk membuat pemandangan. Di sini kita dapat menambahkan beberapa bentuk SDF, memanipulasi posisi dan properti setiap bentuk untuk mendapatkan hasil yang kita inginkan. Mari kita mulai dengan sebuah bola dan merendernya di tengah area pandang:

const sdSphere = tslFn(([p, r]) => {
  return p.length().sub(r)
})

const sdf = tslFn(([pos]) => {
  // Update the sdf function to add our sphere here
  const sphere = sdSphere(pos, 0.3)

  return sphere
})

Kita dapat mengubah ukurannya dengan mengubah jari-jarinya, atau mengubah posisinya sepanjang sumbu z (lebih dekat atau lebih jauh dari titik asal)

Kita juga dapat melakukan beberapa hal keren lainnya di sini, seperti mengubah posisinya berdasarkan waktu dan kurva sin (perhatikan bahwa semua fungsi baru seperti sin atau timerLocal diimpor dari Three/node):

const timer = timerLocal(1)

const sdf = tslFn(([pos]) => {
  // Translate the position along the x-axis so the shape moves left to right
  const translatedPos = pos.add(vec3(sin(timer), 0, 0))
  const sphere = sdSphere(translatedPos, 0.5)

  return sphere
})

// Note: that we can also use oscSine() in place of sin(timer), but as it is in the range
// 0 to 1, we need to remap it to the range -1 to 1
const sdf = tslFn(([pos]) => {
  const translatedPos = pos.add(vec3(oscSine().mul(2).sub(1), 0, 0))
  const sphere = sdSphere(translatedPos, 0.5)

  return sphere
})

Sekarang kita dapat menambahkan bola kedua yang tidak bergerak di tengah layar sehingga kita dapat menunjukkan kesesuaiannya dengan pemandangan:

const sdf = tslFn(([pos]: any) => {
  const translatedPos = pos.add(vec3(sin(timer), 0, 0))
  const sphere = sdSphere(translatedPos, 0.5)
  const secondSphere = sdSphere(pos, 0.3)

  return min(secondSphere, sphere)
})

Lihat bagaimana kita bisa menggabungkan bentuk yang tumpang tindih menggunakan fungsi min di sini. Ini membutuhkan dua SDF masukan dan menentukan yang terdekat, sehingga secara efektif membuat satu bidang. Tapi ujung-ujungnya tajam; di manakah kotorannya? Di sinilah lebih banyak matematika berperan.

Minimum Halus: Senjata Rahasia

Minimum yang dihaluskan adalah minimum, tetapi mulus! Artikel Inigo Quilez adalah sumber terbaik untuk informasi lebih lanjut tentang cara kerjanya, tapi mari kita terapkan menggunakan TSL dan lihat hasilnya:

const smin = tslFn(([a, b, k]: any) => {
  const h = max(k.sub(abs(a.sub(b))), 0).div(k)
  return min(a, b).sub(h.mul(h).mul(k).mul(0.25))
})

const sdf = tslFn(([pos]: any) => {
  const translatedPos = pos.add(vec3(sin(timer), 0, 0))
  const sphere = sdSphere(translatedPos, 0.5)
  const secondSphere = sdSphere(pos, 0.3)

  return smin(secondSphere, sphere, 0.3)
})

ini dia! Kegelapan kita! Namun hasilnya di sini sangat datar, jadi mari kita lakukan pencahayaan agar mendapatkan tampilan yang sangat keren

Tambahkan pencahayaan

Sejauh ini kita telah menggunakan bayangan bidang yang sangat sederhana berdasarkan jarak ke permukaan tertentu, sehingga pemandangan kita “terlihat” 3D, namun kita dapat membuatnya terlihat sangat keren dengan sedikit pencahayaan.

Menambahkan lampu adalah cara terbaik untuk menciptakan kedalaman dan semangat, jadi mari tambahkan berbagai efek pencahayaan berbeda di TSL. Bagian ini agak “ekstra” jadi saya tidak akan membahas semua jenis pencahayaan. Jika Anda ingin mempelajari lebih lanjut tentang pencahayaan dan shader umum yang digunakan di sini, saya merekomendasikan kursus berbayar luar biasa berikut ini:

Dalam demo ini kami akan menambahkan pencahayaan ambient, pencahayaan hemispheric, pencahayaan difus dan specular, serta efek Fresnel. Kedengarannya banyak, tetapi setiap efek pencahayaan hanya memiliki beberapa garis! Untuk sebagian besar teknik ini, kita perlu menghitung normal, sekali lagi berkat Inigo Quilez.

const calcNormal = tslFn(([p]) => {
  const eps = float(0.0001)
  const h = vec2(eps, 0)
  return normalize(
    vec3(
      sdf(p.add(h.xyy)).sub(sdf(p.sub(h.xyy))),
      sdf(p.add(h.yxy)).sub(sdf(p.sub(h.yxy))),
      sdf(p.add(h.yyx)).sub(sdf(p.sub(h.yyx))),
    ),
  )
})

const raymarch = tslFn(() => {
  // Use frag coordinates to get an aspect-fixed UV
  const _uv = uv().mul(resolution.xy).mul(2).sub(resolution.xy).div(resolution.y)

  // Initialize the ray and its direction
  const rayOrigin = vec3(0, 0, -3)
  const rayDirection = vec3(_uv, 1).normalize()

  // Total distance travelled - note that toVar is important here so we can assign to this variable
  const t = float(0).toVar()

  // Calculate the initial position of the ray - this var is declared here so we can use it in lighting calculations later
  const ray = rayOrigin.add(rayDirection.mul

  loop({ start: 1, end: 80 }, () => {
    const d = sdf(ray) // current distance to the scene

    t.addAssign(d) // "march" the ray

    ray.assign(rayOrigin.add(rayDirection.mul

    // If we're close enough, it's a hit, so we can do an early return
    If(d.lessThan(0.001), () => {
      Break()
    })

    // If we've travelled too far, we can return now and consider that this ray didn't hit anything
    If(t.greaterThan(100), () => {
      Break()
    })
  })

  return lighting(rayOrigin, ray)
})()

Normal adalah vektor yang tegak lurus terhadap vektor lain, jadi dalam hal ini Anda dapat menganggap normal sebagai cara cahaya berinteraksi dengan permukaan suatu benda (bayangkan bagaimana cahaya dipantulkan dari suatu permukaan). Kami akan menggunakannya dalam banyak perhitungan pencahayaan:

const lighting = tslFn(([ro, r]) => {
  const normal = calcNormal(r)
  const viewDir = normalize(ro.sub(r))

  // Step 1: Ambient light
  const ambient = vec3(0.2)

  // Step 2: Diffuse lighting - gives our shape a 3D look by simulating how light reflects in all directions
  const lightDir = normalize(vec3(1, 1, 1))
  const lightColor = vec3(1, 1, 0.9)
  const dp = max(0, dot(lightDir, normal))

  const diffuse = dp.mul(lightColor)

  // Steo 3: Hemisphere light - a mix between a sky and ground colour based on normals
  const skyColor = vec3(0, 0.3, 0.6)
  const groundColor = vec3(0.6, 0.3, 0.1)

  const hemiMix = normal.y.mul(0.5).add(0.5)
  const hemi = mix(groundColor, skyColor, hemiMix)

  // Step 4: Phong specular - Reflective light and highlights
  const ph = normalize(reflect(lightDir.negate(), normal))
  const phongValue = max(0, dot(viewDir, ph)).pow(32)

  const specular = vec3(phongValue).toVar()

  // Step 5: Fresnel effect - makes our specular highlight more pronounced at different viewing angles
  const fresnel = float(1)
    .sub(max(0, dot(viewDir, normal)))
    .pow(2)

  specular.mulAssign(fresnel)

  // Lighting is a mix of ambient, hemi, diffuse, then specular added at the end
  // We're multiplying these all by different values to control their intensity

  // Step 1
  const lighting = ambient.mul(0.1)

  // Step 2
  lighting.addAssign(diffuse.mul(0.5))

  // Step 3
  lighting.addAssign(hemi.mul(0.2))

  const finalColor = vec3(0.1).mul(lighting).toVar()

  // Step 4 & 5
  finalColor.addAssign(specular)

  return finalColor
})

ke mana harus pergi dari sini

Jadi kami berhasil! Ada banyak hal yang harus dipelajari, namun hasilnya bisa luar biasa, dan ada banyak hal yang dapat Anda lakukan dari sini. Berikut beberapa hal yang bisa dicoba:

  • Tambahkan kubus atau persegi panjang dan putar.
  • Tambahkan sedikit noise pada bentuknya dan buat menjadi kasar.
  • Jelajahi fitur kombo lainnya (maksimum).
  • Gunakan fract atau mod untuk beberapa duplikasi domain yang menarik.

Saya harap Anda menikmati pengenalan singkat tentang raymarching dan TSL ini. Jika Anda memiliki pertanyaan, beri tahu saya di X.

Kredit dan Referensi

Buat animasi perpindahan bola menggunakan material Three.js khusus

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

#Cara #membuat #adegan #perjalanan #cahaya #cair #menggunakan #bahasa #shading #Three.js

Tinggalkan Balasan

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