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
- Blog