60
loading...
This website collects cookies to deliver better user experience
npx create-next-app --ts
# or
yarn create next-app --typescript
npm run dev
should present you with the "Welcome to NextJS" page. Cool.touch pages/index.tsx
// pages/index.tsx
export default function ViewerPage() {
return <></>;
}
npm install three @react-three/fiber
npm install @react-three/drei
// 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>
);
}
mkdir components
touch components/gltf-model.tsx
// 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} />;
}
/public
directory. You could also pass a GLB hosted elsewhere to the useGLTF
hook.npm i @types/three
for the type checks to pass.// 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>
);
}
styles/globals.css
to set the canvas to screen height:// 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;
}
http://localhost:3000/
/api/render?model={URL}
image/png
mkdir api
touch api/render.ts
vercel dev
locally to test the API route (as opposed to npm run dev
).// api/render.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ name: "Hello World" });
};
http://localhost:3000/api/render
.npm i chrome-aws-lambda
npm i puppeteer
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)
}
await page.waitForFunction('window.status === "ready"')
.onLoad
method to the GLTFModel
component. We'll also add a router to pass a model
query parameter to the GLTFModel
component:// 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>
)
}
gltf-model.tsx
component with a useEffect
hook: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} />;
}
http://localhost:3000/api/render?model=/DamagedHelmet.glb
stale-while-revalidate
header by setting it in our serverless function.touch vercel.json
{
"functions": {
"api/render.ts": {
"maxDuration": 30,
"memory": 3008
}
}
}