From 07e27fffa38a47b3636345d410f9ff5db7075e40 Mon Sep 17 00:00:00 2001 From: Kristo Karp Date: Tue, 29 Jul 2025 16:10:18 +0300 Subject: [PATCH 1/2] feat: add mouse events to avatar --- src/components/Avatar/Avatar.component.tsx | 50 ++++++++++- src/components/Avatar/Avatar.stories.tsx | 6 +- src/components/Avatar/Examples.stories.tsx | 85 ++++++++++++++++++- .../AnimationModel.component.tsx | 12 ++- .../FloatingModel/FloatingModel.component.tsx | 25 +++++- .../HalfBodyModel/HalfBodyModel.component.tsx | 10 ++- .../Models/Model/Model.component.tsx | 64 +++++++++++++- .../MultipleAnimationModel.component.tsx | 22 ++++- .../Models/PoseModel/PoseModel.component.tsx | 10 ++- .../StaticModel/StaticModel.component.tsx | 10 ++- src/services/Models.service.tsx | 7 +- src/types/index.ts | 18 +++- 12 files changed, 297 insertions(+), 22 deletions(-) diff --git a/src/components/Avatar/Avatar.component.tsx b/src/components/Avatar/Avatar.component.tsx index 92eeee39..31574fe1 100644 --- a/src/components/Avatar/Avatar.component.tsx +++ b/src/components/Avatar/Avatar.component.tsx @@ -24,7 +24,7 @@ import { MaterialConfiguration, SpawnState } from 'src/types'; -import { AnimationAction, Vector3 } from 'three'; +import { AnimationAction, Vector3, Mesh, MeshStandardMaterial } from 'three'; import Lights from 'src/components/Lights/Lights.component'; import { spawnState } from 'src/state/spawnAtom'; @@ -179,6 +179,22 @@ export interface AvatarProps extends LightingProps, EnvironmentProps, Omit void; + /** + * Callback for when a mesh hover starts. + */ + onMeshHoverStart?: (mesh: Mesh) => void; + /** + * Callback for when a mesh hover ends. + */ + onMeshHoverEnd?: (mesh: Mesh) => void; + /** + * Callback for processing materials. + */ + materialCallback?: (material: MeshStandardMaterial) => void; } /** @@ -229,7 +245,11 @@ const Avatar: FC = ({ materialConfig, controlsMinDistance, controlsMaxDistance, - canvasConfig + canvasConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const setSpawnState = useSetAtom(spawnState); @@ -254,6 +274,10 @@ const Avatar: FC = ({ bloom={effects?.bloom} onAnimationEnd={onAnimationEnd} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); } @@ -270,6 +294,10 @@ const Avatar: FC = ({ headMovement={headMovement} bloom={effects?.bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); } @@ -285,6 +313,10 @@ const Avatar: FC = ({ headMovement={headMovement} bloom={effects?.bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); } @@ -299,6 +331,10 @@ const Avatar: FC = ({ onLoaded={onLoaded} bloom={effects?.bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); } @@ -311,6 +347,10 @@ const Avatar: FC = ({ emotion={emotion} bloom={effects?.bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); }, [ @@ -327,7 +367,11 @@ const Avatar: FC = ({ materialConfig, onAnimationEnd, idleRotation, - headMovement + headMovement, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback ]); useEffect(() => triggerCallback(onLoading), [modelSrc, animationSrc, onLoading]); diff --git a/src/components/Avatar/Avatar.stories.tsx b/src/components/Avatar/Avatar.stories.tsx index 637ff1ab..e4188c8b 100644 --- a/src/components/Avatar/Avatar.stories.tsx +++ b/src/components/Avatar/Avatar.stories.tsx @@ -241,6 +241,10 @@ export default { cameraInitialDistance: { control: { type: 'range', min: 0, max: 2.5, step: 0.01 } }, onLoaded: { control: false }, environment: { options: Object.keys(environmentPresets), control: { type: 'select' } }, - fov: { control: { type: 'range', min: 30, max: 100, step: 1 } } + fov: { control: { type: 'range', min: 30, max: 100, step: 1 } }, + onMeshClick: { control: false }, + onMeshHoverStart: { control: false }, + onMeshHoverEnd: { control: false }, + materialCallback: { control: false } } }; diff --git a/src/components/Avatar/Examples.stories.tsx b/src/components/Avatar/Examples.stories.tsx index 5fc8e361..1f6acc79 100644 --- a/src/components/Avatar/Examples.stories.tsx +++ b/src/components/Avatar/Examples.stories.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { StoryFn } from '@storybook/react'; import { Sparkles as SparklesDrei } from '@react-three/drei'; import type { Meta } from '@storybook/react'; import { Avatar as AvatarWrapper, CAMERA } from 'src/components/Avatar'; import { getStoryAssetPath } from 'src/services'; import { ignoreArgTypesOnExamples, emotions, modelPresets, animationPresets } from 'src/services/Stories.service'; -import { Vector3 } from 'three'; +import { Mesh, MeshStandardMaterial, Vector3 } from 'three'; import { AvatarProps } from './Avatar.component'; import { Static } from './Avatar.stories'; import { BloomConfiguration } from '../../types'; @@ -154,3 +154,84 @@ IdleBlinking.argTypes = { modelSrc: { options: Object.values({ seven: modelPresets.seven }), control: { type: 'select' } }, animationSrc: { options: Object.values(animationPresets), control: { type: 'select' } } }; + +const highlightMaterial = new MeshStandardMaterial({ color: '#ff0000', transparent: false }); + +export const MouseActions: StoryFn = (args) => { + const [originalMaterials] = useState(new Map()); + + const handleModelClick = (mesh: Mesh) => { + console.log('Clicked mesh:', mesh); + }; + + const dimClothMaterials = (material: MeshStandardMaterial) => { + const materialWhitelist = ['Wolf3D_Outfit_Top', 'Wolf3D_Outfit_Bottom', 'Wolf3D_Hair', 'Wolf3D_Outfit_Footwear']; + + if (!materialWhitelist.includes(material.name)) { + return; + } + + material.color.setHex(0x404040); + // eslint-disable-next-line no-param-reassign + material.roughness = 0.8; + // eslint-disable-next-line no-param-reassign + material.metalness = 0.1; + }; + + const onMeshHoverStart = useCallback( + (mesh: Mesh) => { + const meshWhiteList = ['Wolf3D_Outfit_Top', 'Wolf3D_Outfit_Bottom', 'Wolf3D_Hair', 'Wolf3D_Outfit_Footwear']; + + if (!meshWhiteList.includes(mesh?.name)) { + return; + } + + document.body.style.cursor = 'pointer'; + + // Store original material and set hover highlight for the new mesh + if (!originalMaterials.has(mesh.name)) { + originalMaterials.set(mesh.name, mesh.material); + + // eslint-disable-next-line no-param-reassign + mesh.material = highlightMaterial; + } + }, + [originalMaterials] + ); + + const onMeshHoverEnd = useCallback( + (mesh: Mesh) => { + const originalMaterial = originalMaterials.get(mesh.name); + + if (originalMaterial) { + document.body.style.cursor = 'default'; + // eslint-disable-next-line no-param-reassign + mesh.material = originalMaterial; + originalMaterials.delete(mesh.name); + } + }, + [originalMaterials] + ); + + return ( + + ); +}; + +MouseActions.args = { + ...Static.args, + modelSrc: getStoryAssetPath('male.glb'), + cameraTarget: CAMERA.TARGET.FULL_BODY.MALE, + cameraInitialDistance: CAMERA.CONTROLS.FULL_BODY.MAX_DISTANCE, + background: { + color: 'rgba(134, 157, 169, 1)' + } +}; + +MouseActions.argTypes = {}; diff --git a/src/components/Models/AnimationModel/AnimationModel.component.tsx b/src/components/Models/AnimationModel/AnimationModel.component.tsx index 7c53e398..3b9b8f29 100644 --- a/src/components/Models/AnimationModel/AnimationModel.component.tsx +++ b/src/components/Models/AnimationModel/AnimationModel.component.tsx @@ -36,7 +36,11 @@ export const AnimationModel: FC = ({ headMovement = false, emotion, bloom, - materialConfig + materialConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const ref = useRef(null); const [animationRunning, setAnimationRunning] = useState(true); @@ -57,7 +61,7 @@ export const AnimationModel: FC = ({ assetMixerRef.current = null; }; - }, [scene]); + }, [scene, embeddedAnimations]); const animationClip = useMemo(async () => { const clip = await loadAnimationClips(animationSrc); @@ -108,6 +112,10 @@ export const AnimationModel: FC = ({ onSpawnAnimationFinish={onSpawnAnimationFinish} bloom={bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); }; diff --git a/src/components/Models/FloatingModel/FloatingModel.component.tsx b/src/components/Models/FloatingModel/FloatingModel.component.tsx index 3ce654fc..c85f920c 100644 --- a/src/components/Models/FloatingModel/FloatingModel.component.tsx +++ b/src/components/Models/FloatingModel/FloatingModel.component.tsx @@ -10,7 +10,16 @@ export interface FloatingModelProps extends BaseModelProps { scale?: number; } -export const FloatingModel: FC = ({ modelSrc, scale = 1.0, onLoaded, bloom }) => { +export const FloatingModel: FC = ({ + modelSrc, + scale = 1.0, + onLoaded, + bloom, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback +}) => { const ref = useRef(null); const { scene } = useGltfLoader(modelSrc); @@ -23,5 +32,17 @@ export const FloatingModel: FC = ({ modelSrc, scale = 1.0, o } }); - return ; + return ( + + ); }; diff --git a/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx b/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx index 754b318c..411689a0 100644 --- a/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx +++ b/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx @@ -27,7 +27,11 @@ export const HalfBodyModel: FC = ({ onLoaded, headMovement = false, bloom, - materialConfig + materialConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const ref = useRef(null); const { scene } = useGltfLoader(modelSrc); @@ -72,6 +76,10 @@ export const HalfBodyModel: FC = ({ onLoaded={onLoaded} bloom={bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); }; diff --git a/src/components/Models/Model/Model.component.tsx b/src/components/Models/Model/Model.component.tsx index 3140a895..e8bb360b 100644 --- a/src/components/Models/Model/Model.component.tsx +++ b/src/components/Models/Model/Model.component.tsx @@ -1,7 +1,7 @@ import React, { FC, Ref, useEffect, useState, useCallback, useMemo } from 'react'; import { Group, Mesh } from 'three'; import { normaliseMaterialsConfig, triggerCallback, usePersistantRotation } from 'src/services'; -import { useThree } from '@react-three/fiber'; +import { ThreeEvent, useThree } from '@react-three/fiber'; import { hasWindow } from 'src/services/Client.service'; import { BaseModelProps } from 'src/types'; import { Spawn } from '../../Spawn/Spawn'; @@ -22,7 +22,11 @@ export const Model: FC = ({ onLoaded, onSpawnAnimationFinish, bloom, - materialConfig + materialConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const { gl } = useThree(); const [isTouching, setIsTouching] = useState(false); @@ -60,7 +64,7 @@ export const Model: FC = ({ [isTouching, touchEvent, scene] ); - normaliseMaterialsConfig(scene, bloom, materialConfig); + normaliseMaterialsConfig(scene, bloom, materialConfig, materialCallback); scene.traverse((object) => { const node = object; @@ -71,6 +75,51 @@ export const Model: FC = ({ } }); + const onClick = useCallback( + (event: ThreeEvent) => { + if (!onMeshClick) { + return; + } + + const mesh = event.object as Mesh; + + if (mesh && mesh.isMesh) { + onMeshClick(mesh); + } + }, + [onMeshClick] + ); + + const handlePointerEnter = useCallback( + (event: ThreeEvent) => { + if (!onMeshHoverStart) { + return; + } + const mesh = event.object as Mesh; + + if (mesh && mesh.isMesh) { + event.stopPropagation(); + onMeshHoverStart(mesh); + } + }, + [onMeshHoverStart] + ); + + const handlePointerLeave = useCallback( + (event: ThreeEvent) => { + if (!onMeshHoverEnd) { + return; + } + const mesh = event.object as Mesh; + + if (mesh && mesh.isMesh) { + event.stopPropagation(); + onMeshHoverEnd(mesh); + } + }, + [onMeshHoverEnd] + ); + useEffect(() => triggerCallback(onLoaded), [scene, onLoaded]); useEffect(() => { @@ -101,7 +150,14 @@ export const Model: FC = ({ ); return ( - + {spawnComponent} diff --git a/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx b/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx index 1c5b756b..e3209ccc 100644 --- a/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx +++ b/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx @@ -32,7 +32,11 @@ export const MultipleAnimationModel: FC = ({ emotion, bloom, materialConfig, - onAnimationEnd + onAnimationEnd, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const armatureMixerRef = useRef(null); const assetMixerRef = useRef | null>(null); @@ -66,7 +70,7 @@ export const MultipleAnimationModel: FC = ({ disposeAssetAnimations(assetMixerRef.current, scene); assetMixerRef.current = null; }; - }, [scene]); + }, [scene, embeddedAnimations]); useEffect(() => { const mixer = armatureMixerRef.current; @@ -119,5 +123,17 @@ export const MultipleAnimationModel: FC = ({ useIdleExpression('blink', scene); useFallbackScene(scene, setModelFallback); - return ; + return ( + + ); }; diff --git a/src/components/Models/PoseModel/PoseModel.component.tsx b/src/components/Models/PoseModel/PoseModel.component.tsx index b74798f5..cc0842dd 100644 --- a/src/components/Models/PoseModel/PoseModel.component.tsx +++ b/src/components/Models/PoseModel/PoseModel.component.tsx @@ -23,7 +23,11 @@ export const PoseModel: FC = ({ setModelFallback, onLoaded, bloom, - materialConfig + materialConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const { scene } = useGltfLoader(modelSrc); const { nodes } = useGraph(scene); @@ -42,6 +46,10 @@ export const PoseModel: FC = ({ onLoaded={onLoaded} bloom={bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); }; diff --git a/src/components/Models/StaticModel/StaticModel.component.tsx b/src/components/Models/StaticModel/StaticModel.component.tsx index b410cb90..b36c9acc 100644 --- a/src/components/Models/StaticModel/StaticModel.component.tsx +++ b/src/components/Models/StaticModel/StaticModel.component.tsx @@ -21,7 +21,11 @@ export const StaticModel: FC = ({ onLoaded, emotion, bloom, - materialConfig + materialConfig, + onMeshClick, + onMeshHoverStart, + onMeshHoverEnd, + materialCallback }) => { const { scene } = useGltfLoader(modelSrc); const { nodes } = useGraph(scene); @@ -37,6 +41,10 @@ export const StaticModel: FC = ({ onLoaded={onLoaded} bloom={bloom} materialConfig={materialConfig} + onMeshClick={onMeshClick} + onMeshHoverStart={onMeshHoverStart} + onMeshHoverEnd={onMeshHoverEnd} + materialCallback={materialCallback} /> ); }; diff --git a/src/services/Models.service.tsx b/src/services/Models.service.tsx index 1c0851c2..5360df47 100644 --- a/src/services/Models.service.tsx +++ b/src/services/Models.service.tsx @@ -116,7 +116,8 @@ const disposeGltfScene = (scene: Group) => { export const normaliseMaterialsConfig = ( scene: Group, bloomConfig?: BloomConfiguration, - materialConfig?: MaterialConfiguration + materialConfig?: MaterialConfiguration, + customCallback?: (material: MeshStandardMaterial) => void ) => { traverseMaterials(scene, (material: Material) => { const mat = material as MeshStandardMaterial; @@ -132,6 +133,10 @@ export const normaliseMaterialsConfig = ( if (mat.emissiveMap) { mat.emissiveIntensity = bloomConfig?.materialIntensity ?? materialConfig?.emissiveIntensity ?? 3.3; } + + if (customCallback) { + customCallback(mat); + } }); }; diff --git a/src/types/index.ts b/src/types/index.ts index 5e0ecf9f..a2a4b1ea 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -import { Vector3 } from 'three'; +import { Vector3, Mesh, MeshStandardMaterial } from 'three'; import { PresetsType } from '@react-three/drei/helpers/environment-assets'; export type Required = { @@ -53,6 +53,22 @@ export interface BaseModelProps { setModelFallback?: (fallback: JSX.Element) => void; bloom?: BloomConfiguration; materialConfig?: MaterialConfiguration; + /** + * Callback for when a mesh is clicked. + */ + onMeshClick?: (mesh: Mesh) => void; + /** + * Callback for when a mesh hover starts. + */ + onMeshHoverStart?: (mesh: Mesh) => void; + /** + * Callback for when a mesh hover ends. + */ + onMeshHoverEnd?: (mesh: Mesh) => void; + /** + * Callback for processing materials. + */ + materialCallback?: (material: MeshStandardMaterial) => void; } export type HeadBlendShapeType = From 0731ed727f8e740ac52f86ed49edbe9a1bfb9708 Mon Sep 17 00:00:00 2001 From: Kristo Karp Date: Wed, 30 Jul 2025 11:15:09 +0300 Subject: [PATCH 2/2] fix: expose meshCallback instead of materialCallback --- src/components/Avatar/Avatar.component.tsx | 20 +++++++++---------- src/components/Avatar/Avatar.stories.tsx | 2 +- src/components/Avatar/Examples.stories.tsx | 13 ++++++------ .../AnimationModel.component.tsx | 4 ++-- .../FloatingModel/FloatingModel.component.tsx | 4 ++-- .../HalfBodyModel/HalfBodyModel.component.tsx | 4 ++-- .../Models/Model/Model.component.tsx | 10 +++++++--- .../MultipleAnimationModel.component.tsx | 4 ++-- .../Models/PoseModel/PoseModel.component.tsx | 4 ++-- .../StaticModel/StaticModel.component.tsx | 4 ++-- src/services/Models.service.tsx | 15 ++++++++------ src/types/index.ts | 6 +++--- 12 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/components/Avatar/Avatar.component.tsx b/src/components/Avatar/Avatar.component.tsx index 31574fe1..d1852a73 100644 --- a/src/components/Avatar/Avatar.component.tsx +++ b/src/components/Avatar/Avatar.component.tsx @@ -24,7 +24,7 @@ import { MaterialConfiguration, SpawnState } from 'src/types'; -import { AnimationAction, Vector3, Mesh, MeshStandardMaterial } from 'three'; +import { AnimationAction, Vector3, Mesh } from 'three'; import Lights from 'src/components/Lights/Lights.component'; import { spawnState } from 'src/state/spawnAtom'; @@ -192,9 +192,9 @@ export interface AvatarProps extends LightingProps, EnvironmentProps, Omit void; /** - * Callback for processing materials. + * Callback for processing meshes on model load. */ - materialCallback?: (material: MeshStandardMaterial) => void; + meshCallback?: (mesh: Mesh) => void; } /** @@ -249,7 +249,7 @@ const Avatar: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const setSpawnState = useSetAtom(spawnState); @@ -277,7 +277,7 @@ const Avatar: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); } @@ -297,7 +297,7 @@ const Avatar: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); } @@ -316,7 +316,7 @@ const Avatar: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); } @@ -334,7 +334,7 @@ const Avatar: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); } @@ -350,7 +350,7 @@ const Avatar: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }, [ @@ -371,7 +371,7 @@ const Avatar: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback ]); useEffect(() => triggerCallback(onLoading), [modelSrc, animationSrc, onLoading]); diff --git a/src/components/Avatar/Avatar.stories.tsx b/src/components/Avatar/Avatar.stories.tsx index e4188c8b..6fdd76a8 100644 --- a/src/components/Avatar/Avatar.stories.tsx +++ b/src/components/Avatar/Avatar.stories.tsx @@ -245,6 +245,6 @@ export default { onMeshClick: { control: false }, onMeshHoverStart: { control: false }, onMeshHoverEnd: { control: false }, - materialCallback: { control: false } + meshCallback: { control: false } } }; diff --git a/src/components/Avatar/Examples.stories.tsx b/src/components/Avatar/Examples.stories.tsx index 1f6acc79..6d7e3b4a 100644 --- a/src/components/Avatar/Examples.stories.tsx +++ b/src/components/Avatar/Examples.stories.tsx @@ -164,18 +164,19 @@ export const MouseActions: StoryFn = (args) => { console.log('Clicked mesh:', mesh); }; - const dimClothMaterials = (material: MeshStandardMaterial) => { - const materialWhitelist = ['Wolf3D_Outfit_Top', 'Wolf3D_Outfit_Bottom', 'Wolf3D_Hair', 'Wolf3D_Outfit_Footwear']; + const dimClothMaterials = (mesh: Mesh) => { + const meshWhitelist = ['Wolf3D_Outfit_Top', 'Wolf3D_Outfit_Bottom', 'Wolf3D_Hair', 'Wolf3D_Outfit_Footwear']; - if (!materialWhitelist.includes(material.name)) { + if (!meshWhitelist.includes(mesh.name)) { return; } + const material = mesh.material as MeshStandardMaterial; material.color.setHex(0x404040); - // eslint-disable-next-line no-param-reassign material.roughness = 0.8; - // eslint-disable-next-line no-param-reassign material.metalness = 0.1; + material.emissive.setHex(0xffffff); + material.emissiveIntensity = 0.1; }; const onMeshHoverStart = useCallback( @@ -217,7 +218,7 @@ export const MouseActions: StoryFn = (args) => { diff --git a/src/components/Models/AnimationModel/AnimationModel.component.tsx b/src/components/Models/AnimationModel/AnimationModel.component.tsx index 3b9b8f29..df68e6f5 100644 --- a/src/components/Models/AnimationModel/AnimationModel.component.tsx +++ b/src/components/Models/AnimationModel/AnimationModel.component.tsx @@ -40,7 +40,7 @@ export const AnimationModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const ref = useRef(null); const [animationRunning, setAnimationRunning] = useState(true); @@ -115,7 +115,7 @@ export const AnimationModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/components/Models/FloatingModel/FloatingModel.component.tsx b/src/components/Models/FloatingModel/FloatingModel.component.tsx index c85f920c..aa62517f 100644 --- a/src/components/Models/FloatingModel/FloatingModel.component.tsx +++ b/src/components/Models/FloatingModel/FloatingModel.component.tsx @@ -18,7 +18,7 @@ export const FloatingModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const ref = useRef(null); const { scene } = useGltfLoader(modelSrc); @@ -42,7 +42,7 @@ export const FloatingModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx b/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx index 411689a0..4e22a9cf 100644 --- a/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx +++ b/src/components/Models/HalfBodyModel/HalfBodyModel.component.tsx @@ -31,7 +31,7 @@ export const HalfBodyModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const ref = useRef(null); const { scene } = useGltfLoader(modelSrc); @@ -79,7 +79,7 @@ export const HalfBodyModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/components/Models/Model/Model.component.tsx b/src/components/Models/Model/Model.component.tsx index e8bb360b..42638871 100644 --- a/src/components/Models/Model/Model.component.tsx +++ b/src/components/Models/Model/Model.component.tsx @@ -1,6 +1,6 @@ import React, { FC, Ref, useEffect, useState, useCallback, useMemo } from 'react'; import { Group, Mesh } from 'three'; -import { normaliseMaterialsConfig, triggerCallback, usePersistantRotation } from 'src/services'; +import { normaliseMaterialsConfig, traverseMeshes, triggerCallback, usePersistantRotation } from 'src/services'; import { ThreeEvent, useThree } from '@react-three/fiber'; import { hasWindow } from 'src/services/Client.service'; import { BaseModelProps } from 'src/types'; @@ -26,7 +26,7 @@ export const Model: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const { gl } = useThree(); const [isTouching, setIsTouching] = useState(false); @@ -64,7 +64,7 @@ export const Model: FC = ({ [isTouching, touchEvent, scene] ); - normaliseMaterialsConfig(scene, bloom, materialConfig, materialCallback); + normaliseMaterialsConfig(scene, bloom, materialConfig); scene.traverse((object) => { const node = object; @@ -75,6 +75,10 @@ export const Model: FC = ({ } }); + if (meshCallback) { + traverseMeshes(scene, meshCallback); + } + const onClick = useCallback( (event: ThreeEvent) => { if (!onMeshClick) { diff --git a/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx b/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx index e3209ccc..ece3ad96 100644 --- a/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx +++ b/src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component.tsx @@ -36,7 +36,7 @@ export const MultipleAnimationModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const armatureMixerRef = useRef(null); const assetMixerRef = useRef | null>(null); @@ -133,7 +133,7 @@ export const MultipleAnimationModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/components/Models/PoseModel/PoseModel.component.tsx b/src/components/Models/PoseModel/PoseModel.component.tsx index cc0842dd..02f83ec8 100644 --- a/src/components/Models/PoseModel/PoseModel.component.tsx +++ b/src/components/Models/PoseModel/PoseModel.component.tsx @@ -27,7 +27,7 @@ export const PoseModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const { scene } = useGltfLoader(modelSrc); const { nodes } = useGraph(scene); @@ -49,7 +49,7 @@ export const PoseModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/components/Models/StaticModel/StaticModel.component.tsx b/src/components/Models/StaticModel/StaticModel.component.tsx index b36c9acc..364e4e8a 100644 --- a/src/components/Models/StaticModel/StaticModel.component.tsx +++ b/src/components/Models/StaticModel/StaticModel.component.tsx @@ -25,7 +25,7 @@ export const StaticModel: FC = ({ onMeshClick, onMeshHoverStart, onMeshHoverEnd, - materialCallback + meshCallback }) => { const { scene } = useGltfLoader(modelSrc); const { nodes } = useGraph(scene); @@ -44,7 +44,7 @@ export const StaticModel: FC = ({ onMeshClick={onMeshClick} onMeshHoverStart={onMeshHoverStart} onMeshHoverEnd={onMeshHoverEnd} - materialCallback={materialCallback} + meshCallback={meshCallback} /> ); }; diff --git a/src/services/Models.service.tsx b/src/services/Models.service.tsx index 5360df47..b0045765 100644 --- a/src/services/Models.service.tsx +++ b/src/services/Models.service.tsx @@ -85,6 +85,14 @@ function traverseMaterials(object: Object3D, callback: (material: Material) => v }); } +export function traverseMeshes(object: Object3D, callback: (material: Mesh) => void) { + object.traverse((node) => { + const mesh = node as Mesh; + if (!mesh.geometry || !mesh.isMesh) return; + callback(mesh); + }); +} + const disposeGltfScene = (scene: Group) => { scene.traverse((node) => { if (node instanceof SkinnedMesh && node.skeleton) { @@ -116,8 +124,7 @@ const disposeGltfScene = (scene: Group) => { export const normaliseMaterialsConfig = ( scene: Group, bloomConfig?: BloomConfiguration, - materialConfig?: MaterialConfiguration, - customCallback?: (material: MeshStandardMaterial) => void + materialConfig?: MaterialConfiguration ) => { traverseMaterials(scene, (material: Material) => { const mat = material as MeshStandardMaterial; @@ -133,10 +140,6 @@ export const normaliseMaterialsConfig = ( if (mat.emissiveMap) { mat.emissiveIntensity = bloomConfig?.materialIntensity ?? materialConfig?.emissiveIntensity ?? 3.3; } - - if (customCallback) { - customCallback(mat); - } }); }; diff --git a/src/types/index.ts b/src/types/index.ts index a2a4b1ea..435c9731 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -import { Vector3, Mesh, MeshStandardMaterial } from 'three'; +import { Vector3, Mesh } from 'three'; import { PresetsType } from '@react-three/drei/helpers/environment-assets'; export type Required = { @@ -66,9 +66,9 @@ export interface BaseModelProps { */ onMeshHoverEnd?: (mesh: Mesh) => void; /** - * Callback for processing materials. + * Callback for processing meshes on model load. */ - materialCallback?: (material: MeshStandardMaterial) => void; + meshCallback?: (material: Mesh) => void; } export type HeadBlendShapeType =