Skip to content

Commit

Permalink
feat: コスチュームのバリアントを変えられるように (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
shun-shobon authored Nov 5, 2024
2 parents d51b7bc + 3de7e55 commit 7740bc4
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 28 deletions.
20 changes: 20 additions & 0 deletions app/components/ModelConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ const COLORS = [
interface ModelConfigProps {
characterSetting: CharacterSetting;
onModelSelect?: (model: Model) => void;
onCostumeSelect?: (costume: number) => void;
onAccessorySelect?: (accessory: Accessory) => void;
onHairColorChange?: (color: string) => void;
}

export function ModelConfig({
characterSetting,
onModelSelect,
onCostumeSelect,
onAccessorySelect,
onHairColorChange,
}: ModelConfigProps) {
Expand All @@ -52,6 +54,24 @@ export function ModelConfig({
</div>
</div>

<div className="flex gap-4">
<span className="flex-shrink-0">モデル:</span>
<div className="flex flex-grow flex-wrap justify-between gap-2">
{[0, 1, 2].map((idx) => (
<label key={idx}>
<input
type="radio"
name="costume"
value={idx}
checked={idx === characterSetting.costume}
onChange={() => onCostumeSelect?.(idx)}
/>
<span>{idx + 1}</span>
</label>
))}
</div>
</div>

<div className="flex gap-4">
<span className="flex-shrink-0">アクセサリー:</span>
<div className="flex flex-grow flex-wrap justify-between gap-2">
Expand Down
90 changes: 62 additions & 28 deletions app/components/ModelLoad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ import { type ReactNode, Suspense, useEffect, useMemo, useRef } from "react";
import type { MeshStandardMaterial } from "three";
import * as THREE from "three";
import { OutlineEffect } from "three/addons/effects/OutlineEffect.js";
import type { Accessory, CharacterSetting } from "~/features/profile/Profile";

const HAIR_MESH_NAMES = [
"hairear",
"hairback",
"hairfront",
"hairtail",
"hairside",
"hairsid",
"hair",
] as const;
type HairMeshName = (typeof HAIR_MESH_NAMES)[number];
import type {
Accessory,
CharacterSetting,
Model,
} from "~/features/profile/Profile";

const ACCESSORY_METH_NAMES = [
"accessoryeyepatch",
Expand All @@ -35,11 +28,26 @@ const ACCESSORY_MAP: Record<Accessory, AccessoryMeshName[]> = {
halo: ["accessoryhalo"],
};

const COSTUME_MAP: Record<Model, string[]> = {
jiraichan: ["#FFA0C0", "#FFD700", "#FFA07A"],
necochan: ["#00DCFF", "#FF66CC", "#99FF00"],
asuka: ["#0D5793", "#F2C800", "#F57D9D"],
kaiju: ["#00FF33", "#009966", "#FF6600"],
sushong: ["#ff0000", "#ffcc00", "#0099ff"],
};

const COSTUME_MATERIAL_NAMES = [
"clothes_primary",
"parker_primary",
"gothloli_primary",
];

function useCharacterSetting(setting: CharacterSetting) {
const modelPath = `/models/web_${setting.character}.glb`;

const { scene } = useGLTF(modelPath);
const { scene, materials } = useGLTF(modelPath);

// トゥーンシェーディング用のテクスチャを読み込み
const threeTone = useMemo(() => {
const threeTone = new THREE.TextureLoader().load("/assets/threeTone.jpg");
threeTone.minFilter = THREE.NearestFilter;
Expand All @@ -48,11 +56,12 @@ function useCharacterSetting(setting: CharacterSetting) {
return threeTone;
}, []);

useEffect(() => {
scene.traverse((child) => {
if (!(child as THREE.Mesh).isMesh) return;
const mesh = child as THREE.Mesh;
const oldMaterial = mesh.material as MeshStandardMaterial;
// トゥーンシェーディング用のマテリアルを作成
const toonMaterials = useMemo(() => {
const toonMaterials: Record<string, THREE.MeshToonMaterial> = {};

for (const key of Object.keys(materials)) {
const oldMaterial = materials[key] as MeshStandardMaterial;
const newMaterial = new THREE.MeshToonMaterial({
name: oldMaterial.name,
color: oldMaterial.color,
Expand All @@ -65,10 +74,41 @@ function useCharacterSetting(setting: CharacterSetting) {
opacity: oldMaterial.opacity,
transparent: oldMaterial.transparent,
});
mesh.material = newMaterial;

toonMaterials[key] = newMaterial;
}

return toonMaterials;
}, [threeTone, materials]);

// マテリアルをトゥーンシェーディング用に差し替え
useEffect(() => {
scene.traverse((child) => {
if (!(child as THREE.Mesh).isMesh) return;
const mesh = child as THREE.Mesh;
mesh.material =
toonMaterials[(mesh.material as MeshStandardMaterial).name];
});
}, [scene, threeTone]);
}, [scene, toonMaterials]);

// 髪のマテリアルの色を反映
useEffect(() => {
toonMaterials.hair.color.set(setting.hair);
}, [toonMaterials, setting.hair]);

// 衣装のマテリアルの色を反映
useEffect(() => {
for (const name of COSTUME_MATERIAL_NAMES) {
const costumeMaterial = toonMaterials[name];
if (costumeMaterial) {
costumeMaterial.color.set(
COSTUME_MAP[setting.character][setting.costume],
);
}
}
}, [toonMaterials, setting.character, setting.costume]);

// アクセサリーの表示を切り替え
useEffect(() => {
scene.traverse((child) => {
if (!(child as THREE.Mesh).isMesh) return;
Expand All @@ -83,12 +123,6 @@ function useCharacterSetting(setting: CharacterSetting) {
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;
material.color.set(setting.hair);
}
});
}, [scene, setting]);

Expand All @@ -99,7 +133,7 @@ interface ModelProps {
characterSetting: CharacterSetting;
}

function Model({ characterSetting }: ModelProps): ReactNode {
function Character({ characterSetting }: ModelProps): ReactNode {
const scene = useCharacterSetting(characterSetting);

return (
Expand Down Expand Up @@ -144,7 +178,7 @@ export function ModelViewer({ characterSetting }: ModelProps): ReactNode {
}
>
{/* GLBモデルの読み込みと表示 */}
<Model characterSetting={characterSetting} />
<Character characterSetting={characterSetting} />
</Suspense>
{/* カメラコントロールの追加(ユーザーが自由にカメラを操作できるようにする) */}
<OrbitControls
Expand Down
8 changes: 8 additions & 0 deletions app/routes/_auth.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export default function Page(): ReactNode {
});
};

const handleCostumeSelect = (costume: number) => {
updateCharacterSetting({
...myProfile.characterSetting,
costume,
});
};

const handleAccessorySelect = (accessory: Accessory) => {
updateCharacterSetting({
...myProfile.characterSetting,
Expand All @@ -39,6 +46,7 @@ export default function Page(): ReactNode {
<ModelConfig
characterSetting={myProfile.characterSetting}
onModelSelect={handleModelSelect}
onCostumeSelect={handleCostumeSelect}
onAccessorySelect={handleAccessorySelect}
onHairColorChange={handleHairColorChange}
/>
Expand Down

0 comments on commit 7740bc4

Please sign in to comment.