151 | Model Color
152 |
153 |
setShow(!show)}
159 | />
160 |
161 |
162 | {show && }
163 |
164 |
165 |
166 | );
167 | }
168 |
169 | function SwitchButton({ label, checked, onChange, disabled = false }) {
170 | const className = `${styles.switchWrapper} ${
171 | disabled ? styles.disabled : ""
172 | }`;
173 |
174 | return (
175 |
176 | {label}
177 | {}}
182 | height={8}
183 | width={24}
184 | onColor="#2196f3"
185 | handleDiameter={16}
186 | className={styles.switch}
187 | />
188 |
189 | );
190 | }
191 |
192 | const IconButton = React.memo(function ({
193 | className,
194 | label,
195 | onClick,
196 | title,
197 | selected,
198 | ...rest
199 | }: IconButtonProps) {
200 | return (
201 |
207 |
{" "}
208 | {label &&
{label}
}
209 |
210 | );
211 | });
212 |
--------------------------------------------------------------------------------
/src/components/Settings/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Settings";
2 |
--------------------------------------------------------------------------------
/src/constants/name.ts:
--------------------------------------------------------------------------------
1 | import { name, version } from "../../package.json";
2 |
3 | export const MODEL_NAME = `${name}-${version}`;
4 |
--------------------------------------------------------------------------------
/src/hooks/useLoader.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from "react";
2 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
3 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
4 | import { useThree } from "react-three-fiber";
5 | import { MODEL_NAME } from "@constants/name";
6 |
7 | export function useLoader(path: string, onLoad: () => void) {
8 | const { scene } = useThree();
9 |
10 | const loader = useMemo(() => {
11 | const loader = new GLTFLoader();
12 | const dracoLoader = new DRACOLoader();
13 | dracoLoader.setDecoderPath("/draco-gltf/");
14 | loader.setDRACOLoader(dracoLoader);
15 | return loader;
16 | }, []);
17 |
18 | useEffect(() => {
19 | if (!path) return;
20 |
21 | if (scene.getObjectByName(MODEL_NAME)) {
22 | scene.remove(scene.getObjectByName(MODEL_NAME));
23 | }
24 | loader.load(`/models/${path}.glb`, (gltf) => {
25 | scene.add(gltf.scene);
26 | gltf.scene.name = MODEL_NAME;
27 |
28 | onLoad();
29 | });
30 | }, [path, loader]);
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/useSobelRenderPass.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef } from "react";
2 | import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
3 | import { useFrame, useThree } from "react-three-fiber";
4 | import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
5 | import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass";
6 | import { SobelOperatorShader } from "three/examples/jsm/shaders/SobelOperatorShader";
7 | import { usePostProcessing } from "@stores/postProcessing";
8 |
9 | export function useSobelRenderPass() {
10 | const { scene, camera, gl, size } = useThree();
11 | const soberRenderPass = usePostProcessing((state) => state.sobelRenderPass);
12 |
13 | const composer = useMemo(() => {
14 | const composer = new EffectComposer(gl);
15 | const renderPass = new RenderPass(scene, camera);
16 | composer.addPass(renderPass);
17 |
18 | const sobelEffect = new ShaderPass(SobelOperatorShader);
19 | sobelEffect.uniforms["resolution"].value.x =
20 | window.innerWidth * window.devicePixelRatio;
21 | sobelEffect.uniforms["resolution"].value.y =
22 | window.innerHeight * window.devicePixelRatio;
23 |
24 | composer.addPass(sobelEffect);
25 | return composer;
26 | }, [camera, scene, gl]);
27 |
28 | useEffect(() => {
29 | composer.setSize(size.width, size.height);
30 | }, [composer, size]);
31 |
32 | useFrame(
33 | (_, delta) => {
34 | if (soberRenderPass) {
35 | composer.render(delta);
36 | }
37 | },
38 | soberRenderPass ? 1 : 0
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/hooks/useTransformOnClick.ts:
--------------------------------------------------------------------------------
1 | import { useThree } from "react-three-fiber";
2 | import { useEffect, useMemo } from "react";
3 | import { Mesh, SkinnedMesh } from "three";
4 | import { TransformControls } from "three/examples/jsm/controls/TransformControls";
5 | import { convertPointerToCoordinate } from "@utils/convertPointerToCoordinate";
6 | import { usePostProcessing } from "@stores/postProcessing";
7 | import { MODEL_NAME } from "@constants/name";
8 |
9 | export function useTransformOnClick(orbitalControls) {
10 | const { raycaster, gl, camera, scene } = useThree();
11 | const transformControls = useMemo
(
12 | () => new TransformControls(camera, gl.domElement),
13 | []
14 | );
15 | const sobelRenderPass = usePostProcessing((state) => state.sobelRenderPass);
16 |
17 | useEffect(() => {
18 | function handleChange(e) {
19 | orbitalControls.enabled = !e.value;
20 | }
21 |
22 | transformControls.addEventListener("dragging-changed", handleChange);
23 | transformControls.mode = "rotate";
24 | transformControls.axis = "local";
25 | scene.add(transformControls);
26 |
27 | return () => {
28 | transformControls.removeEventListener("dragging-changed", handleChange);
29 | transformControls.dispose();
30 | };
31 | }, [transformControls]);
32 |
33 | useEffect(() => {
34 | function handleClick(ev: PointerEvent) {
35 | if (sobelRenderPass) return;
36 | ev.preventDefault();
37 |
38 | raycaster.setFromCamera(
39 | convertPointerToCoordinate(ev, gl.domElement),
40 | camera
41 | );
42 |
43 | const intersects = raycaster.intersectObject(
44 | scene.getObjectByName(MODEL_NAME),
45 | true
46 | );
47 |
48 | const intersectedBoneMesh = intersects.filter(
49 | (x) => x.object instanceof Mesh && !(x.object instanceof SkinnedMesh)
50 | );
51 |
52 | if (intersectedBoneMesh.length) {
53 | const boneMesh = intersectedBoneMesh[0].object as Mesh;
54 | const rootBone = boneMesh.parent;
55 |
56 | transformControls.attach(rootBone);
57 | }
58 | // checking if orbitalControls is enabled tells us whether the uer clicked
59 | // somewhere on the canvas or if it's just the pointerup event that is triggered when
60 | // we stop the dragging of transformControls. We detach the transformControl only if
61 | // the user has clicked somewhere else in the canvas.
62 | else if (orbitalControls.enabled) {
63 | transformControls.detach();
64 | }
65 | }
66 |
67 | gl.domElement.addEventListener("pointerup", handleClick);
68 | return () => {
69 | gl.domElement.addEventListener("pointerup", handleClick);
70 | };
71 | }, [sobelRenderPass, orbitalControls]);
72 | }
73 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/main.scss";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | export default function MyApp({ Component, pageProps }) {
6 | return (
7 | <>
8 |
9 | Reference
10 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import dynamic from "next/dynamic";
3 | import { Settings } from "@components/Settings";
4 | import GitHubCorners from "react-github-corner";
5 |
6 | const Scene = dynamic(() => import("@components/ModelLoader/ModelLoader"), {
7 | ssr: false,
8 | });
9 |
10 | export default function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/stores/environment.ts:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 |
3 | const [useEnvironment] = create((set) => ({
4 | showGrid: true,
5 | toggleGrid: () =>
6 | set((state) => ({
7 | showGrid: !state.showGrid,
8 | })),
9 | }));
10 |
11 | export { useEnvironment };
12 |
13 | interface IEnvironmentState {
14 | showGrid: boolean;
15 | toggleGrid: () => void;
16 | }
17 |
--------------------------------------------------------------------------------
/src/stores/material.ts:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 |
3 | const [useMaterial] = create((set) => ({
4 | materialColor: "#fff",
5 | setMaterialColor: (color) =>
6 | set({
7 | materialColor: color.hex,
8 | }),
9 | }));
10 |
11 | export { useMaterial };
12 |
--------------------------------------------------------------------------------
/src/stores/mode.ts:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 |
3 | interface IMode {
4 | editMode: boolean;
5 | toggleEditMode: () => void;
6 | }
7 |
8 | const [useMode] = create((set) => ({
9 | editMode: true,
10 | toggleEditMode: () =>
11 | set((state) => ({
12 | editMode: !state.editMode,
13 | })),
14 | }));
15 |
16 | export { useMode };
17 |
--------------------------------------------------------------------------------
/src/stores/postProcessing.ts:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 |
3 | const [usePostProcessing] = create((set) => ({
4 | sobelRenderPass: false,
5 | toggleSobelRenderPass: () => {
6 | set((state) => ({
7 | sobelRenderPass: !state.sobelRenderPass,
8 | }));
9 | },
10 | }));
11 |
12 | export { usePostProcessing };
13 |
14 | interface IPostProcessing {
15 | sobelRenderPass: boolean;
16 | toggleSobelRenderPass: () => void;
17 | }
18 |
--------------------------------------------------------------------------------
/src/stores/scene.ts:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 | import { WebGLRenderer } from "three";
3 |
4 | interface ILoadedModel {
5 | model: string;
6 | setModel: (name: string) => void;
7 | renderer: WebGLRenderer;
8 | setRenderer: (renderer: WebGLRenderer) => void;
9 | }
10 |
11 | const [useScene] = create((set) => ({
12 | model: "male",
13 | setModel: (model: string) =>
14 | set({
15 | model,
16 | }),
17 | renderer: null,
18 | setRenderer: (renderer) =>
19 | set({
20 | renderer,
21 | }),
22 | }));
23 |
24 | export { useScene };
25 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Lato:wght@300;400&display=swap");
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | box-sizing: border-box;
9 | font-family: "Lato", sans-serif;
10 | }
11 |
12 | html,
13 | body {
14 | overflow: hidden;
15 | width: 100vw;
16 | height: 100vh;
17 | background-image: radial-gradient(#2b2b2b, #000000);
18 | }
19 |
20 | .container {
21 | display: flex;
22 | flex: 1;
23 | position: relative;
24 | overflow: hidden;
25 | }
26 |
27 | .container:focus,
28 | .container > div:focus,
29 | .container canvas:focus {
30 | outline: none;
31 | }
32 |
33 | .wrapper {
34 | display: flex;
35 | flex-direction: row;
36 | width: 100vw;
37 | height: 100vh;
38 | flex: 1;
39 |
40 | @media screen and (max-width: 620px) {
41 | flex-direction: column-reverse;
42 | }
43 | }
44 |
45 | .chrome-picker {
46 | background-color: rgb(255 255 255 / 8%) !important;
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/assert.ts:
--------------------------------------------------------------------------------
1 | export function assert(condition: any, msg?: string): asserts condition {
2 | if (!condition) {
3 | throw new Error(msg);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/convertPointerToCoordinate.ts:
--------------------------------------------------------------------------------
1 | export function convertPointerToCoordinate(
2 | event: PointerEvent,
3 | domElement: HTMLCanvasElement
4 | ) {
5 | const SIDEBAR_WIDTH = window.screen.width < 620 ? 0 : 290;
6 |
7 | const x =
8 | ((event.clientX - SIDEBAR_WIDTH) / domElement.parentElement.clientWidth) *
9 | 2 -
10 | 1;
11 | const y = -(event.clientY / domElement.parentElement.clientHeight) * 2 + 1;
12 |
13 | return { x, y };
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/geometry.ts:
--------------------------------------------------------------------------------
1 | import { Box3, Object3D, Vector3 } from "three";
2 |
3 | export function getCenter(model: Object3D): Vector3 {
4 | const box = new Box3();
5 | box.setFromObject(model);
6 | const center = new Vector3();
7 | box.getCenter(center);
8 | return center;
9 | }
10 |
11 | export function getModelCenter(model: Object3D, modelName: string) {
12 | const _model =
13 | modelName === "male" || modelName === "female"
14 | ? model.children[0].children[0]
15 | : model;
16 |
17 | return getCenter(_model);
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": "./src",
17 | "paths": {
18 | "@hooks/*": ["./hooks/*"],
19 | "@components/*": ["./components/*"],
20 | "@utils/*": ["./utils/*"],
21 | "@constants/*": ["./constants/*"],
22 | "@stores/*": ["./stores/*"]
23 | }
24 | },
25 | "exclude": ["node_modules"],
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
27 | }
28 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare const DEV: boolean;
2 | declare const __THREE_DEVTOOLS__: any;
3 |
--------------------------------------------------------------------------------