Bezserverové 3D vykreslování WebGL s ThreeJS

Tento obrázek nahoře byl vykreslen ve funkci bez serveru při načtení stránky (nedělám si legraci, zkontrolujte zdroj obrázku) 🤓

Tento příspěvek se původně objevil na https://www.rainer.im/blog/serverless-3d-rendering .

3D vykreslování je nákladný úkol, jehož výpočet na serverech s akcelerací GPU často trvá dlouho.

Prohlížeče jsou stále schopnější. Web je výkonnější než kdy jindy. A serverless je nejrychleji rostoucí model cloudové služby. Musí existovat způsob, jak využít tyto technologie pro levné vykreslování 3D obsahu ve velkém měřítku.

Tady je nápad:

  • Vytvořte aplikaci React a zobrazte 3D model pomocí funkce React-Three-Fiber
  • Vytvořte funkci bez serveru, která spustí bezhlavý prohlížeč zobrazující obsah WebGL
  • Počkejte, až se načte obsah WebGL a vrátí vykreslený obrázek

K tomu použijeme NextJS.

Finální projekt je na GitHubu.

3D prohlížeč

Začněme vytvořením nové aplikace NextJS. Zavedeme projekt ze startéru strojového skriptu NextJS.

npx create-next-app --ts
# or
yarn create next-app --typescript

Spuštěn npm run dev by vám měla představit stránku „Vítejte v NextJS“. Skvělé.

Vytvořme stránku, která bude zobrazovat 3D model.

touch pages/index.tsx
// pages/index.tsx

export default function ViewerPage() {
  return <></>;
}

Aby to bylo jednoduché, budeme používat React Three Fiber a Drei, sbírku pomocníků a abstrakcí kolem React Three Fiber.

Pojďme nainstalovat obě závislosti:

npm install three @react-three/fiber
npm install @react-three/drei

Nastavíme 3D prohlížeč. K získání pěkného prostředí pro vykreslování použijeme komponentu Stage.

// pages/index.tsx

import { Canvas } from "@react-three/fiber";
import { Stage } from "@react-three/drei";
import { Suspense } from "react";

export default function ViewerPage() {
  return (
    <Canvas
      gl={{ preserveDrawingBuffer: true, antialias: true, alpha: true }}
      shadows
    >
      <Suspense fallback={null}>
        <Stage
          contactShadow
          shadows
          adjustCamera
          intensity={1}
          environment="city"
          preset="rembrandt"
        ></Stage>
      </Suspense>
    </Canvas>
  );
}

Nyní musíme načíst 3D model. Budeme načítat aktivum glTF, přenosový formát, který se vyvíjí v „JPG 3D aktiv“. Více o tom v budoucích příspěvcích!

Pojďme vytvořit komponentu pro načtení jakéhokoli glTF aktiva:

mkdir components
touch components/gltf-model.tsx

Projdeme také graf scény glTF, abychom umožnili vrhání stínů na sítě glTF:

// components/gltf-model.tsx

import { useGLTF } from "@react-three/drei";
import { useLayoutEffect } from "react";

interface GLTFModelProps {
  model: string;
  shadows: boolean;
}

export default function GLTFModel(props: GLTFModelProps) {
  const gltf = useGLTF(props.model);

  useLayoutEffect(() => {
    gltf.scene.traverse((obj: any) => {
      if (obj.isMesh) {
        obj.castShadow = obj.receiveShadow = props.shadows;
        obj.material.envMapIntensity = 0.8;
      }
    });
  }, [gltf.scene, props.shadows]);

  return <primitive object={gltf.scene} />;
}

Budeme používat podklad glTF stažený z ukázkových modelů glTF KhronosGroup zde.

Přidejme GLB (binární verzi glTF) do /public adresář. Můžete také předat GLB hostované jinde do useGLTF háček.

Možná budete muset nainstalovat npm i @types/three aby typové kontroly prošly.

Pojďme přidat GLTFModel na naši stránku prohlížeče:

// pages/index.tsx

import { Canvas } from "@react-three/fiber";
import { Stage } from "@react-three/drei";
import { Suspense } from "react";
import GLTFModel from "../components/gltf-model";

export default function ViewerPage() {
  return (
    <Canvas
      gl={{ preserveDrawingBuffer: true, antialias: true, alpha: true }}
      shadows
    >
      <Suspense fallback={null}>
        <Stage
          contactShadow
          shadows
          adjustCamera
          intensity={1}
          environment="city"
          preset="rembrandt"
        >
          <GLTFModel model={"/DamagedHelmet.glb"} shadows={true} />
        </Stage>
      </Suspense>
    </Canvas>
  );
}

Aktualizujte styles/globals.css jak nastavit plátno na výšku obrazovky:

// styles/globals.css

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

canvas {
  height: 100vh;
}

Když je to na místě, měli byste nyní vidět 3D model vykreslený na http://localhost:3000/

Bezserverové vykreslování

Pojďme využít 3D prohlížeč na straně klienta a poskytnout přístup k 2D vykreslování prostřednictvím rozhraní API.

Aby to bylo jednoduché, rozhraní API vezme jako vstup jakoukoli adresu URL 3D modelu a jako odpověď vrátí obrázek tohoto 3D modelu.

API

GET:/api/render?model={URL}

Odpověď:image/png

Vytvořte trasu API

mkdir api
touch api/render.ts

⚠️ Upozorňujeme, že vytváříme nový adresář API a nepoužíváme stávající pages/api . Je to proto, aby se zabránilo sdílení zdrojů funkcí a překročení limitu velikosti bezserverových funkcí na Vercelu (kde budeme aplikaci nasazovat). Více informací zde a zde.

⚠️ Také, aby byly funkce bez serveru vyzvednuty z kořenového adresáře, budete muset spustit
vercel dev lokálně pro testování trasy API (na rozdíl od npm run dev ).

Nastavíme počáteční funkci:

// api/render.ts

import type { NextApiRequest, NextApiResponse } from "next";

export default (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: "Hello World" });
};

Díky tomu již máte trasu API živou na http://localhost:3000/api/render .

V zákulisí bude vykreslování probíhat ve funkci AWS Lambda. Pro práci s bezhlavým prohlížečem proto musíme použít vlastní verzi Chromium.

Pojďme nainstalovat závislosti:

npm i chrome-aws-lambda
npm i puppeteer

Dokončeme naši renderovací funkci:

import type { NextApiRequest, NextApiResponse } from 'next'
const chrome = require('chrome-aws-lambda')
const puppeteer = require('puppeteer')

const getAbsoluteURL = (path: string) => {
  if (process.env.NODE_ENV === 'development') {
    return `http://localhost:3000${path}`
  }
  return `https://${process.env.VERCEL_URL}${path}`
}

export default async (req: NextApiRequest, res: NextApiResponse) => {
  let {
    query: { model }
  } = req

  if (!model) return res.status(400).end(`No model provided`)

  let browser

  if (process.env.NODE_ENV === 'production') {
    browser = await puppeteer.launch({
      args: chrome.args,
      defaultViewport: chrome.defaultViewport,
      executablePath: await chrome.executablePath,
      headless: chrome.headless,
      ignoreHTTPSErrors: true
    })
  } else {
    browser = await puppeteer.launch({
      headless: true
    })
  }

  const page = await browser.newPage()
  await page.setViewport({ width: 512, height: 512 })
  await page.goto(getAbsoluteURL(`?model=${model}`))
  await page.waitForFunction('window.status === "ready"')

  const data = await page.screenshot({
    type: 'png'
  })

  await browser.close()
  // Set the s-maxage property which caches the images then on the Vercel edge
  res.setHeader('Cache-Control', 's-maxage=10, stale-while-revalidate')
  res.setHeader('Content-Type', 'image/png')
  // Write the image to the response with the specified Content-Type
  res.end(data)
}

Zde je to, co se děje ve funkci

  • Při místním vývoji spusťte verzi Chrome optimalizovanou pro Lambda v prostředí bez serveru nebo pomocí loutkového herce
  • Přejděte na adresu URL zobrazující 3D model předaný v parametru dotazu
  • Počkejte na vykreslení 3D modelu
  • Uložte výsledek obrázku do mezipaměti
  • Vraťte obrázek

Všimněte si řádku await page.waitForFunction('window.status === "ready"') .

Tato funkce čeká na dokončení vykreslování. Aby to fungovalo, budeme muset aktualizovat naši stránku prohlížeče a přidat onLoad metoda na GLTFModel komponent. Přidáme také směrovač, který předá model dotaz na parametr GLTFModel komponent:

// pages/index.tsx

import { Canvas } from '@react-three/fiber'
import { Stage } from '@react-three/drei'
import { Suspense } from 'react'
import GLTFModel from '../components/gltf-model'
import { useRouter } from 'next/router'

const handleOnLoaded = () => {
  console.log('Model loaded')
  window.status = 'ready'
}

export default function ViewerPage() {
  const router = useRouter()
  const { model } = router.query
  if (!model) return <>No model provided</>

  return (
    <Canvas gl={{ preserveDrawingBuffer: true, antialias: true, alpha: true }} camera={{ fov: 35 }} shadows>
      <Suspense fallback={null}>
        <Stage contactShadow shadows adjustCamera intensity={1} environment="city" preset="rembrandt">
          <GLTFModel model={model as string} shadows={true} onLoaded={handleOnLoaded} />
        </Stage>
      </Suspense>
    </Canvas>
  )
}

Také budeme muset aktualizovat naše gltf-model.tsx komponenta s useEffect háček:

import { useGLTF } from "@react-three/drei";
import { useLayoutEffect, useEffect } from "react";

interface GLTFModelProps {
  model: string;
  shadows: boolean;
  onLoaded: any;
}

export default function GLTFModel(props: GLTFModelProps) {
  const gltf = useGLTF(props.model);

  useLayoutEffect(() => {
    gltf.scene.traverse((obj: any) => {
      if (obj.isMesh) {
        obj.castShadow = obj.receiveShadow = props.shadows;
        obj.material.envMapIntensity = 0.8;
      }
    });
  }, [gltf.scene, props.shadows]);

  useEffect(() => {
    props.onLoaded();
  }, []);

  return <primitive object={gltf.scene} />;
}

Testovací jízda

Podívejme se, zda je naše API funkční.

http://localhost:3000/api/render?model=/DamagedHelmet.glb

Boom 💥 model glTF vykreslený na straně serveru:

Vykreslení tohoto 3D modelu trvá ~5 sekund. Při nasazení do CDN je obraz obsluhován za ~50 ms po počátečním požadavku. Pozdější požadavky spouštějí revalidaci (znovu vykreslování na pozadí).

⚡ Ukládání do mezipaměti ⚡

Využíváme výhod stale-while-revalidate nastavením v naší funkci bez serveru.

Tímto způsobem můžeme obsluhovat zdroj z mezipaměti CDN při aktualizaci mezipaměti na pozadí . Je to užitečné v případech, kdy se obsah často mění, ale jeho generování trvá značné množství času (tj. vykreslování!).

Maxage jsme nastavili na 10 sekund. Pokud se požadavek zopakuje do 10 sekund, předchozí obrázek je považován za nový – zobrazí se cache HIT.

Pokud se požadavek zopakuje o více než 10 sekund později, obrázek je stále okamžitě podávané z mezipaměti. Na pozadí je spuštěn požadavek na opětovné ověření a aktualizovaný obrázek je doručen pro další požadavek.

Nasazení

V tomto příkladu nasazujeme službu do Vercelu spuštěním vercel pomocí jejich CLI.

⚡ Zvyšte výkon funkce ⚡

Výkon funkce můžete zlepšit konfigurací více dostupné paměti. Zvýšení paměti upgraduje výkon CPU a sítě základních lambd AWS.

Zde je návod, jak nakonfigurovat Lambda tak, aby měla 3x paměť než výchozí konfigurace.

touch vercel.json

{
  "functions": {
    "api/render.ts": {
      "maxDuration": 30,
      "memory": 3008
    }
  }
}

Finální projekt a fungující API najdete na GitHubu.

Děkuji za přečtení!

Tento příspěvek se původně objevil na https://www.rainer.im/blog/serverless-3d-rendering .

Najít mě jinde

  • Twitter
  • Blog