From fff4725028a16c713ea281eb3abb2d5432affd9b Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 02:21:47 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E3=82=AD=E3=83=A3=E3=83=A9?= =?UTF-8?q?=E7=B7=A8=E9=9B=86=E7=94=BB=E9=9D=A2=E3=81=AE=E3=82=B9=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelConfig.tsx | 2 +- app/components/ModelLoad.tsx | 52 ++++++++++++++++++---------------- app/routes/_auth.edit.tsx | 8 ++---- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/app/components/ModelConfig.tsx b/app/components/ModelConfig.tsx index b85d2cf..5988bf9 100644 --- a/app/components/ModelConfig.tsx +++ b/app/components/ModelConfig.tsx @@ -55,7 +55,7 @@ export function ModelConfig({ ]; return ( -
+
{/* モデル選択ボタン */}
- {/* 3Dモデルを表示するためのCanvasエリア */} -
- - {/* 環境を設定*/} - - {/* 環境光を追加(全体的に均一な光を当てる) */} - {/* */} - {/* GLBモデルの読み込みと表示 */} - - {/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */} - - -
-
+ + {/* 環境を設定*/} + + {/* GLBモデルの読み込みと表示 */} + + {/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */} + + ); } diff --git a/app/routes/_auth.edit.tsx b/app/routes/_auth.edit.tsx index 81abe7b..b31aa32 100644 --- a/app/routes/_auth.edit.tsx +++ b/app/routes/_auth.edit.tsx @@ -15,12 +15,8 @@ export default function Page(): ReactNode { const { meshColor, updateVisibility: updateColor } = getMeshColor(); return ( -
-
- キャラ編集 +
+
Date: Tue, 5 Nov 2024 03:00:18 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20CharacterSetting=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=8B=E3=82=89=E3=82=AD=E3=83=A3=E3=83=A9=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E5=8F=82=E7=85=A7=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelLoad.tsx | 98 ++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/app/components/ModelLoad.tsx b/app/components/ModelLoad.tsx index 38c46a0..a6af660 100644 --- a/app/components/ModelLoad.tsx +++ b/app/components/ModelLoad.tsx @@ -3,52 +3,84 @@ import { Canvas } from "@react-three/fiber"; import { type ReactNode, useEffect, useRef } from "react"; import type { Group, MeshStandardMaterial } from "three"; import * as THREE from "three"; +import type { Accessory, CharacterSetting } from "~/features/profile/Profile"; -function Model({ - path, - colorMap, - meshVisibility, -}: { - path: string; - colorMap: { [key: string]: string }; - meshVisibility: { [key: string]: boolean }; -}): ReactNode { - const { scene } = useGLTF(path); - // モデルのグループ(オブジェクト全体)にアクセスするための参照を作成 - const groupRef = useRef(null); +const HAIR_MESH_NAMES = [ + "hairear", + "hairback", + "hairfront", + "hairtail", + "hairsid", + "hair", +] as const; +type HairMeshName = (typeof HAIR_MESH_NAMES)[number]; - useEffect(() => { - scene.traverse((child: THREE.Object3D) => { - if ((child as THREE.Mesh).isMesh) { - const mesh = child as THREE.Mesh; +const ACCESSORY_METH_NAMES = [ + "accessoryeyepatch", + "accessoryglasses", + "goggle", + "goggle_1", + "accessoryhalo", +] as const; +type AccessoryMeshName = (typeof ACCESSORY_METH_NAMES)[number]; - // メッシュ名をコンソールに出力 - // console.log("Mesh name:", mesh.name); +const ACCESSORY_MAP: Record = { + none: [], + eyepatch: ["accessoryeyepatch"], + glasses: ["accessoryglasses"], + goggle: ["goggle", "goggle_1"], + halo: ["accessoryhalo"], +}; - // 部位名がmeshVisibilityのキーに一致する場合に表示非表示を設定 - if ( - Object.hasOwn(meshVisibility, mesh.name) && - !meshVisibility[mesh.name] - ) { - mesh.visible = false; - return; - } +function useCharacterSetting(setting: CharacterSetting) { + const modelPath = `/models/web_${setting.character}.glb`; + const { scene } = useGLTF(modelPath); + useEffect(() => { + scene.traverse((child) => { + if (!(child as THREE.Mesh).isMesh) return; + const mesh = child as THREE.Mesh; + + // アクセサリーのmeshは基本非表示 + if (ACCESSORY_METH_NAMES.includes(mesh.name as AccessoryMeshName)) { + mesh.visible = false; + } + // アクセサリーのmeshのうち、設定されたアクセサリーに対応するものだけ表示 + const visibleAccessoryMeshNames = ACCESSORY_MAP[setting.accessory]; + if (visibleAccessoryMeshNames.includes(mesh.name as AccessoryMeshName)) { mesh.visible = true; + } + // 髪のmeshの場合は色を設定 + if (HAIR_MESH_NAMES.includes(mesh.name as HairMeshName)) { const material = mesh.material as MeshStandardMaterial; - - // 部位名がcolorMapのキーに一致する場合に色を設定 - if (Object.hasOwn(colorMap, mesh.name)) { - material.color.set(colorMap[mesh.name]); - } + material.color.set(setting.hair); } }); - }, [scene, colorMap, meshVisibility]); + }, [scene, setting]); + + return scene; +} + +function Model({ + path, + colorMap, + meshVisibility, +}: { + path: string; + colorMap: { [key: string]: string }; + meshVisibility: { [key: string]: boolean }; +}): ReactNode { + const scene = useCharacterSetting({ + character: "jiraichan", + costume: 0, + accessory: "glasses", + hair: "#333333", + }); return ( // グループとしてシーンをレンダリング - + {/* モデルのプリミティブ(生のオブジェクト)を表示 */} From c3857da8c0d7e578ff032ac809cd367742672520 Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 03:11:57 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20API=E3=81=8B=E3=82=89=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=97=E3=81=9F=E3=82=AD=E3=83=A3=E3=83=A9=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E6=83=85=E5=A0=B1=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelLoad.tsx | 40 ++++++++---------------------------- app/routes/_auth.edit.tsx | 11 +++++----- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/app/components/ModelLoad.tsx b/app/components/ModelLoad.tsx index a6af660..ecb5ccf 100644 --- a/app/components/ModelLoad.tsx +++ b/app/components/ModelLoad.tsx @@ -62,21 +62,12 @@ function useCharacterSetting(setting: CharacterSetting) { return scene; } -function Model({ - path, - colorMap, - meshVisibility, -}: { - path: string; - colorMap: { [key: string]: string }; - meshVisibility: { [key: string]: boolean }; -}): ReactNode { - const scene = useCharacterSetting({ - character: "jiraichan", - costume: 0, - accessory: "glasses", - hair: "#333333", - }); +interface ModelProps { + characterSetting: CharacterSetting; +} + +function Model({ characterSetting }: ModelProps): ReactNode { + const scene = useCharacterSetting(characterSetting); return ( // グループとしてシーンをレンダリング @@ -87,18 +78,7 @@ function Model({ ); } -// 3Dモデルビューアーコンポーネント -// モデルのパスを受け取って、それを表示する -type ModelViewerProps = { - modelPath: string; // モデルのパス - colorMap: { [key: string]: string }; // 部位ごとの色マップ - meshVisibility: { [key: string]: boolean }; // 部位ごとの表示非表示 -}; -export function ModelViewer({ - modelPath, - colorMap, - meshVisibility, -}: ModelViewerProps): ReactNode { +export function ModelViewer({ characterSetting }: ModelProps): ReactNode { return ( {/* GLBモデルの読み込みと表示 */} - + {/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */} [{ title: "キャラ編集 | RicoShot" }]; export default function Page(): ReactNode { + const { myProfile } = useMyProfile(); + if (!myProfile) return null; + const [modelPath, setModelPath] = useState("/models/web_asuka.glb"); const { meshVisibility, updateVisibility } = getMeshVisibility(); const { meshColor, updateVisibility: updateColor } = getMeshColor(); @@ -17,11 +20,7 @@ export default function Page(): ReactNode { return (
- + Date: Tue, 5 Nov 2024 03:41:32 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=E3=83=97=E3=83=AD=E3=83=95?= =?UTF-8?q?=E3=82=A3=E3=83=BC=E3=83=AB=E7=B7=A8=E9=9B=86=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E6=A5=BD=E8=A6=B3=E7=9A=84UI=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=95=E3=81=9B=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/features/profile/Profile.ts | 4 -- app/features/profile/useMyProfile.ts | 98 +++++++++++++++++++--------- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/app/features/profile/Profile.ts b/app/features/profile/Profile.ts index 371f496..e54fc38 100644 --- a/app/features/profile/Profile.ts +++ b/app/features/profile/Profile.ts @@ -33,7 +33,3 @@ export interface Profile { rank: number | null; characterSetting: CharacterSetting; } - -export type UpdateProfileBody = { - profile: Partial>; -}; diff --git a/app/features/profile/useMyProfile.ts b/app/features/profile/useMyProfile.ts index 38a7721..a5c18c1 100644 --- a/app/features/profile/useMyProfile.ts +++ b/app/features/profile/useMyProfile.ts @@ -1,13 +1,8 @@ import { useCallback } from "react"; import useSWR from "swr"; import * as v from "valibot"; -import { - ACCESSORY_LIST, - type CharacterSetting, - MODEL_LIST, - type Profile, - type UpdateProfileBody, -} from "~/features/profile/Profile"; +import { ACCESSORY_LIST, MODEL_LIST } from "~/features/profile/Profile"; +import type { CharacterSetting, Profile } from "~/features/profile/Profile"; import type { Json } from "~/libs/database"; import { supabase } from "~/libs/supabase"; import { useSession } from "../../hooks/useSession"; @@ -17,7 +12,8 @@ interface UseMyProfile { error: unknown; isValidating: boolean; - updateMyProfile: (params: UpdateProfileBody) => Promise; + updateDisplayName: (displayName: string) => Promise; + updateCharacterSetting: (setting: CharacterSetting) => Promise; } export function useMyProfile(): UseMyProfile { @@ -66,37 +62,78 @@ export function useMyProfile(): UseMyProfile { }, ); - const updateMyProfile = useCallback( - async ({ profile }: UpdateProfileBody) => { + const updateDisplayName = useCallback( + async (displayName: string) => { if (!session) throw new Error("Can't update profile without session"); const userId = session.user.id; - const { data: newProfile } = await supabase - .from("profiles") - .update({ - display_name: profile.displayName, - character_setting: serializeCharacterSetting( - profile.characterSetting, - ), - }) - .eq("user_id", userId) - .select("user_id, display_name") - .single(); + const mutation = async ( + prev: Profile | null | undefined, + ): Promise => { + const { data: newProfile } = await supabase + .from("profiles") + .update({ + display_name: displayName, + }) + .eq("user_id", userId) + .select("user_id, display_name") + .single(); + + if (!newProfile) throw new Error("Failed to update profile"); + + if (!prev) return; + return { + ...prev, + id: newProfile.user_id, + displayName: newProfile.display_name, + }; + }; + + mutate(mutation, { revalidate: false }); + }, + [mutate, session], + ); - if (!newProfile) throw new Error("Failed to update profile"); + const updateCharacterSetting = useCallback( + async (setting: CharacterSetting) => { + if (!session) throw new Error("Can't update profile without session"); + const userId = session.user.id; - mutate( - (prev) => { - if (!prev) return; + const mutation = async ( + prev: Profile | null | undefined, + ): Promise => { + const { data: newProfile } = await supabase + .from("profiles") + .update({ + character_setting: serializeCharacterSetting(setting), + }) + .eq("user_id", userId) + .select("user_id, character_setting") + .single(); + + if (!newProfile) throw new Error("Failed to update profile"); + + if (!prev) return; + return { + ...prev, + id: newProfile.user_id, + characterSetting: deserializeCharacterSetting( + newProfile.character_setting, + ), + }; + }; + + mutate(mutation, { + optimisticData: (prev) => { + if (!prev) return null; return { ...prev, - id: newProfile.user_id, - displayName: newProfile.display_name, + characterSetting: setting, }; }, - { revalidate: false }, - ); + revalidate: false, + }); }, [mutate, session], ); @@ -106,7 +143,8 @@ export function useMyProfile(): UseMyProfile { error, isValidating, - updateMyProfile, + updateDisplayName, + updateCharacterSetting, }; } From be1cc3a935a6fd207d33ff02208ef59534812aee Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 03:43:09 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E6=99=82=E3=81=ABDB?= =?UTF-8?q?=E5=81=B4=E3=82=82=E6=9B=B4=E6=96=B0=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelConfig.tsx | 85 +++++++++++----------------------- app/routes/_auth.edit.tsx | 17 ++++--- 2 files changed, 38 insertions(+), 64 deletions(-) diff --git a/app/components/ModelConfig.tsx b/app/components/ModelConfig.tsx index 5988bf9..ae1d906 100644 --- a/app/components/ModelConfig.tsx +++ b/app/components/ModelConfig.tsx @@ -1,63 +1,34 @@ -import { useEffect, useState } from "react"; +import { MODEL_LIST } from "~/features/profile/Profile"; +import type { CharacterSetting, Model } from "~/features/profile/Profile"; -interface ModelListProps { - onModelSelect: (modelPath: string) => void; - updateVisibility: (key: string, value: boolean) => void; - updateColor: (key: string, value: string) => void; +interface ModelConfigProps { + characterSetting: CharacterSetting; + onModelSelect?: (model: Model) => void; } export function ModelConfig({ + characterSetting, onModelSelect, - updateVisibility, - updateColor, -}: ModelListProps) { - const [models, setModels] = useState([]); - const [containerWidth, setContainerWidth] = useState(0); - const [selectedColors, setSelectedColors] = useState>( - {}, - ); - - useEffect(() => { - const fetchModels = async () => { - const modelFiles = [ - "/models/web_asuka.glb", - "/models/web_jiraichan.glb", - "/models/web_kaiju.glb", - "/models/web_necochan.glb", - "/models/web_sushong.glb", - ]; - setModels(modelFiles); - }; - - fetchModels(); - - const updateContainerWidth = () => { - const containerRef = document.getElementById("model-list-container"); - if (containerRef) { - setContainerWidth(containerRef.offsetWidth); - } - }; - - updateContainerWidth(); - window.addEventListener("resize", updateContainerWidth); - - return () => window.removeEventListener("resize", updateContainerWidth); - }, []); - - // ボタンの幅を計算 - const buttonWidth = `${Math.floor(containerWidth / models.length) - 16}px`; - // 表示非表示のキー - const visibilityKeys = [ - "accessoryeyepatch", - "accessoryglasses", - "goggle", - "accessoryhalo", - ]; - +}: ModelConfigProps) { return (
+
+ {MODEL_LIST.map((model) => ( + + ))} +
+ {/* モデル選択ボタン */} -
@@ -72,9 +43,9 @@ export function ModelConfig({ {model.split("/").pop()?.split("_")[1].split(".")[0]} ))} -
+
*/} {/* 表示非表示ボタン */} -
+ {/*
{visibilityKeys.map((key) => (
{key} @@ -94,9 +65,9 @@ export function ModelConfig({
))} -
+
*/} {/* 色変更ボタン */} -
+ {/*
Hair Color: -
+
*/}
); } diff --git a/app/routes/_auth.edit.tsx b/app/routes/_auth.edit.tsx index 5669580..ceec488 100644 --- a/app/routes/_auth.edit.tsx +++ b/app/routes/_auth.edit.tsx @@ -5,26 +5,29 @@ import { getMeshColor } from "~/components/ModelColor"; import { ModelConfig } from "~/components/ModelConfig"; import { ModelViewer } from "~/components/ModelLoad"; import { getMeshVisibility } from "~/components/ModelVisibility"; +import type { Model } from "~/features/profile/Profile"; import { useMyProfile } from "~/features/profile/useMyProfile"; export const meta: MetaFunction = () => [{ title: "キャラ編集 | RicoShot" }]; export default function Page(): ReactNode { - const { myProfile } = useMyProfile(); + const { myProfile, updateCharacterSetting } = useMyProfile(); if (!myProfile) return null; - const [modelPath, setModelPath] = useState("/models/web_asuka.glb"); - const { meshVisibility, updateVisibility } = getMeshVisibility(); - const { meshColor, updateVisibility: updateColor } = getMeshColor(); + const handleModelSelect = (model: Model) => { + updateCharacterSetting({ + ...myProfile.characterSetting, + character: model, + }); + }; return (
From 243fd4f7dc2cbe9ef4031581260fc20224e24483 Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 03:47:51 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E3=82=A2=E3=82=AF=E3=82=BB?= =?UTF-8?q?=E3=82=B5=E3=83=AA=E3=83=BC=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88?= =?UTF-8?q?=E3=82=82=E6=B0=B8=E7=B6=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelConfig.tsx | 25 +++++++++++++++++++++++-- app/routes/_auth.edit.tsx | 13 +++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/components/ModelConfig.tsx b/app/components/ModelConfig.tsx index ae1d906..9674589 100644 --- a/app/components/ModelConfig.tsx +++ b/app/components/ModelConfig.tsx @@ -1,14 +1,20 @@ -import { MODEL_LIST } from "~/features/profile/Profile"; -import type { CharacterSetting, Model } from "~/features/profile/Profile"; +import { ACCESSORY_LIST, MODEL_LIST } from "~/features/profile/Profile"; +import type { + Accessory, + CharacterSetting, + Model, +} from "~/features/profile/Profile"; interface ModelConfigProps { characterSetting: CharacterSetting; onModelSelect?: (model: Model) => void; + onAccessorySelect?: (accessory: Accessory) => void; } export function ModelConfig({ characterSetting, onModelSelect, + onAccessorySelect, }: ModelConfigProps) { return (
@@ -27,6 +33,21 @@ export function ModelConfig({ ))}
+
+ {ACCESSORY_LIST.map((accessory) => ( + + ))} +
+ {/* モデル選択ボタン */} {/*
[{ title: "キャラ編集 | RicoShot" }]; @@ -21,6 +18,13 @@ export default function Page(): ReactNode { }); }; + const handleAccessorySelect = (accessory: Accessory) => { + updateCharacterSetting({ + ...myProfile.characterSetting, + accessory, + }); + }; + return (
@@ -28,6 +32,7 @@ export default function Page(): ReactNode {
From 5fa377115dfe13ead5c22a47aee4924cf7b19074 Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 04:00:11 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E9=AB=AA=E8=89=B2=E3=81=AE?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=82=82DB=E3=81=AB=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelConfig.tsx | 76 ++++++++++++++++++++++------------ app/routes/_auth.edit.tsx | 8 ++++ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/app/components/ModelConfig.tsx b/app/components/ModelConfig.tsx index 9674589..0c3b2e8 100644 --- a/app/components/ModelConfig.tsx +++ b/app/components/ModelConfig.tsx @@ -9,43 +9,65 @@ interface ModelConfigProps { characterSetting: CharacterSetting; onModelSelect?: (model: Model) => void; onAccessorySelect?: (accessory: Accessory) => void; + onHairColorChange?: (color: string) => void; } export function ModelConfig({ characterSetting, onModelSelect, onAccessorySelect, + onHairColorChange, }: ModelConfigProps) { return ( -
-
- {MODEL_LIST.map((model) => ( - - ))} +
+
+ モデル: +
+ {MODEL_LIST.map((model) => ( + + ))} +
-
- {ACCESSORY_LIST.map((accessory) => ( - - ))} +
+ アクセサリー: +
+ {ACCESSORY_LIST.map((accessory) => ( + + ))} +
+
+ +
+
{/* モデル選択ボタン */} diff --git a/app/routes/_auth.edit.tsx b/app/routes/_auth.edit.tsx index 293b2af..8475d5d 100644 --- a/app/routes/_auth.edit.tsx +++ b/app/routes/_auth.edit.tsx @@ -25,6 +25,13 @@ export default function Page(): ReactNode { }); }; + const handleHairColorChange = (color: string) => { + updateCharacterSetting({ + ...myProfile.characterSetting, + hair: color, + }); + }; + return (
@@ -33,6 +40,7 @@ export default function Page(): ReactNode { characterSetting={myProfile.characterSetting} onModelSelect={handleModelSelect} onAccessorySelect={handleAccessorySelect} + onHairColorChange={handleHairColorChange} />
From a41859d4ad7b93cb826fda5d8016a6e3d4642676 Mon Sep 17 00:00:00 2001 From: NISHIZAWA Shuntaro Date: Tue, 5 Nov 2024 14:00:42 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E3=83=9E=E3=83=86=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=82=92=E3=83=88=E3=82=A5=E3=83=BC=E3=83=B3?= =?UTF-8?q?=E8=AA=BF=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ModelLoad.tsx | 38 +++++++++++++++++++++--- package.json | 1 + pnpm-lock.yaml | 54 +++++++++++++++++++++++++++++++++++ public/assets/threeTone.jpg | Bin 0 -> 11040 bytes 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 public/assets/threeTone.jpg diff --git a/app/components/ModelLoad.tsx b/app/components/ModelLoad.tsx index ecb5ccf..a732e40 100644 --- a/app/components/ModelLoad.tsx +++ b/app/components/ModelLoad.tsx @@ -1,5 +1,6 @@ -import { Environment, OrbitControls, useGLTF } from "@react-three/drei"; +import { OrbitControls, useGLTF } from "@react-three/drei"; import { Canvas } from "@react-three/fiber"; +import { Bloom, EffectComposer } from "@react-three/postprocessing"; import { type ReactNode, useEffect, useRef } from "react"; import type { Group, MeshStandardMaterial } from "three"; import * as THREE from "three"; @@ -10,6 +11,7 @@ const HAIR_MESH_NAMES = [ "hairback", "hairfront", "hairtail", + "hairside", "hairsid", "hair", ] as const; @@ -36,6 +38,29 @@ function useCharacterSetting(setting: CharacterSetting) { const modelPath = `/models/web_${setting.character}.glb`; const { scene } = useGLTF(modelPath); + + useEffect(() => { + const threeTone = new THREE.TextureLoader().load("/assets/threeTone.jpg"); + threeTone.minFilter = THREE.NearestFilter; + threeTone.magFilter = THREE.NearestFilter; + + scene.traverse((child) => { + if (!(child as THREE.Mesh).isMesh) return; + const mesh = child as THREE.Mesh; + const oldMaterial = mesh.material as MeshStandardMaterial; + const newMaterial = new THREE.MeshToonMaterial({ + name: oldMaterial.name, + color: oldMaterial.color, + gradientMap: threeTone, + map: oldMaterial.map, + emissive: oldMaterial.emissive, + emissiveIntensity: oldMaterial.emissiveIntensity, + emissiveMap: oldMaterial.emissiveMap, + }); + mesh.material = newMaterial; + }); + }, [scene]); + useEffect(() => { scene.traverse((child) => { if (!(child as THREE.Mesh).isMesh) return; @@ -83,15 +108,20 @@ export function ModelViewer({ characterSetting }: ModelProps): ReactNode { - {/* 環境を設定*/} - + {/* ライトを設定 */} + + + {/* ポストプロセッシング */} + + + {/* GLBモデルの読み込みと表示 */} {/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */} diff --git a/package.json b/package.json index 8137866..80b8e91 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-slot": "1.1.0", "@react-three/drei": "9.114.6", "@react-three/fiber": "8.17.10", + "@react-three/postprocessing": "2.16.3", "@remix-run/react": "2.13.1", "@supabase/supabase-js": "2.45.6", "@types/three": "0.169.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3507e32..fbe9fc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@react-three/fiber': specifier: 8.17.10 version: 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + '@react-three/postprocessing': + specifier: 2.16.3 + version: 2.16.3(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/three@0.169.0)(react@18.3.1)(three@0.169.0) '@remix-run/react': specifier: 2.13.1 version: 2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) @@ -1221,6 +1224,13 @@ packages: react-native: optional: true + '@react-three/postprocessing@2.16.3': + resolution: {integrity: sha512-ftodXpUsy0/mzn0KqyV7MBau71dD9C5UOFnB3kHhCLNoxjKYQWZa9do0olJTSkl3owYXRfNHcLriK1Xn8wxZJw==} + peerDependencies: + '@react-three/fiber': '>=8.0' + react: '>=18.0' + three: '>= 0.138.0' + '@remix-run/dev@2.13.1': resolution: {integrity: sha512-7+06Dail6zMyRlRvgrZ4cmQjs2gUb+M24iP4jbmql+0B7VAAPwzCRU0x+BF5z8GSef13kDrH3iXv/BQ2O2yOgw==} engines: {node: '>=18.0.0'} @@ -3252,6 +3262,12 @@ packages: '@types/three': '>=0.134.0' three: '>=0.134.0' + maath@0.6.0: + resolution: {integrity: sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==} + peerDependencies: + '@types/three': '>=0.144.0' + three: '>=0.144.0' + magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -3557,6 +3573,12 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + n8ao@1.9.3: + resolution: {integrity: sha512-OZX+u8LaEfxLi6lupuyT8gIv80D6D8FIeKbBNkCyY0nE+1wmm6sQ4yeyW3a15lFMrfTcEhe0AU8QhhDejHg7sg==} + peerDependencies: + postprocessing: '>=6.30.0' + three: '>=0.137' + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3937,6 +3959,11 @@ packages: resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} engines: {node: '>=12'} + postprocessing@6.36.3: + resolution: {integrity: sha512-GtMLwoqg0xdYAgm9lbf6qlKay7HQPb8Z+xAPSzbCUspud/qBj3Y/orJSkqEQVlfxTtm7YAUSv+J39ir51/IwaQ==} + peerDependencies: + three: '>= 0.157.0 < 0.170.0' + potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} @@ -5994,6 +6021,19 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) + '@react-three/postprocessing@2.16.3(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/three@0.169.0)(react@18.3.1)(three@0.169.0)': + dependencies: + '@react-three/fiber': 8.17.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + buffer: 6.0.3 + maath: 0.6.0(@types/three@0.169.0)(three@0.169.0) + n8ao: 1.9.3(postprocessing@6.36.3(three@0.169.0))(three@0.169.0) + postprocessing: 6.36.3(three@0.169.0) + react: 18.3.1 + three: 0.169.0 + three-stdlib: 2.33.0(three@0.169.0) + transitivePeerDependencies: + - '@types/three' + '@remix-run/dev@2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@types/node@22.8.2)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.2))': dependencies: '@babel/core': 7.26.0 @@ -8287,6 +8327,11 @@ snapshots: '@types/three': 0.169.0 three: 0.169.0 + maath@0.6.0(@types/three@0.169.0)(three@0.169.0): + dependencies: + '@types/three': 0.169.0 + three: 0.169.0 + magic-string@0.27.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -8759,6 +8804,11 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + n8ao@1.9.3(postprocessing@6.36.3(three@0.169.0))(three@0.169.0): + dependencies: + postprocessing: 6.36.3(three@0.169.0) + three: 0.169.0 + nanoid@3.3.7: {} nearley@2.20.1: @@ -9122,6 +9172,10 @@ snapshots: postgres@3.4.5: {} + postprocessing@6.36.3(three@0.169.0): + dependencies: + three: 0.169.0 + potpack@1.0.2: {} prettier@2.8.8: {} diff --git a/public/assets/threeTone.jpg b/public/assets/threeTone.jpg new file mode 100644 index 0000000000000000000000000000000000000000..58278acb40c1efb7a2af4c65520e865bc55c9863 GIT binary patch literal 11040 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