2025-12-02 06:33:12 +00:00
|
|
|
import { memo, Suspense, useMemo, useRef, useEffect } from "react";
|
2025-11-15 03:33:44 +00:00
|
|
|
import { useGLTF, useTexture } from "@react-three/drei";
|
2025-12-03 13:57:16 +00:00
|
|
|
import { FALLBACK_TEXTURE_URL, textureToUrl, shapeToUrl } from "../loaders";
|
2025-11-15 07:43:31 +00:00
|
|
|
import { filterGeometryByVertexGroups, getHullBoneIndices } from "../meshUtils";
|
2025-11-15 07:59:35 +00:00
|
|
|
import {
|
|
|
|
|
createAlphaAsRoughnessMaterial,
|
|
|
|
|
setupAlphaAsRoughnessTexture,
|
|
|
|
|
} from "../shaderMaterials";
|
2025-11-15 07:43:31 +00:00
|
|
|
import { MeshStandardMaterial } from "three";
|
2025-11-15 09:42:32 +00:00
|
|
|
import { setupColor } from "../textureUtils";
|
2025-11-26 07:44:37 +00:00
|
|
|
import { useDebug } from "./SettingsProvider";
|
2025-11-24 05:47:49 +00:00
|
|
|
import { useShapeInfo } from "./ShapeInfoProvider";
|
|
|
|
|
import { FloatingLabel } from "./FloatingLabel";
|
2025-12-02 06:33:12 +00:00
|
|
|
import { useIflTexture } from "./useIflTexture";
|
2025-11-15 03:33:44 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load a .glb file that was converted from a .dts, used for static shapes.
|
|
|
|
|
*/
|
|
|
|
|
export function useStaticShape(shapeName: string) {
|
|
|
|
|
const url = shapeToUrl(shapeName);
|
|
|
|
|
return useGLTF(url);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 06:33:12 +00:00
|
|
|
/**
|
|
|
|
|
* Animated IFL (Image File List) material component. Creates a sprite sheet
|
|
|
|
|
* from all frames and animates via texture offset.
|
|
|
|
|
*/
|
|
|
|
|
export function IflTexture({
|
2025-11-15 07:43:31 +00:00
|
|
|
material,
|
2025-11-15 09:42:32 +00:00
|
|
|
shapeName,
|
2025-11-15 07:43:31 +00:00
|
|
|
}: {
|
2025-12-02 06:33:12 +00:00
|
|
|
material: MeshStandardMaterial;
|
2025-11-15 09:42:32 +00:00
|
|
|
shapeName?: string;
|
2025-11-15 07:43:31 +00:00
|
|
|
}) {
|
2025-12-02 06:33:12 +00:00
|
|
|
const resourcePath = material.userData.resource_path;
|
|
|
|
|
// Convert resource_path (e.g., "skins/blue00") to IFL path
|
|
|
|
|
const iflPath = `textures/${resourcePath}.ifl`;
|
|
|
|
|
|
|
|
|
|
const texture = useIflTexture(iflPath);
|
|
|
|
|
|
|
|
|
|
const isOrganic = shapeName && /borg|xorg|porg|dorg/i.test(shapeName);
|
|
|
|
|
|
|
|
|
|
const customMaterial = useMemo(() => {
|
|
|
|
|
if (!isOrganic) {
|
|
|
|
|
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
|
|
|
|
shaderMaterial.map = texture;
|
|
|
|
|
return shaderMaterial;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clonedMaterial = material.clone();
|
|
|
|
|
clonedMaterial.map = texture;
|
|
|
|
|
clonedMaterial.transparent = true;
|
|
|
|
|
clonedMaterial.alphaTest = 0.9;
|
|
|
|
|
clonedMaterial.side = 2; // DoubleSide
|
|
|
|
|
return clonedMaterial;
|
|
|
|
|
}, [material, texture, isOrganic]);
|
|
|
|
|
|
|
|
|
|
return <primitive object={customMaterial} attach="material" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function StaticTexture({ material, shapeName }) {
|
|
|
|
|
const resourcePath = material.userData.resource_path;
|
|
|
|
|
|
2025-12-01 08:17:27 +00:00
|
|
|
const url = useMemo(() => {
|
|
|
|
|
if (!resourcePath) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`Material index out of range on shape "${shapeName}" - rendering fallback.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-02 06:33:12 +00:00
|
|
|
return resourcePath
|
2025-12-01 08:17:27 +00:00
|
|
|
? // Use custom `resource_path` added by forked io_dts3d Blender add-on
|
2025-12-03 13:57:16 +00:00
|
|
|
textureToUrl(resourcePath)
|
2025-12-02 06:33:12 +00:00
|
|
|
: FALLBACK_TEXTURE_URL;
|
|
|
|
|
}, [material, resourcePath, shapeName]);
|
2025-12-01 08:17:27 +00:00
|
|
|
|
2025-11-26 01:36:41 +00:00
|
|
|
const isOrganic = shapeName && /borg|xorg|porg|dorg/i.test(shapeName);
|
2025-11-15 09:42:32 +00:00
|
|
|
|
|
|
|
|
const texture = useTexture(url, (texture) => {
|
|
|
|
|
if (!isOrganic) {
|
|
|
|
|
setupAlphaAsRoughnessTexture(texture);
|
|
|
|
|
}
|
|
|
|
|
return setupColor(texture);
|
|
|
|
|
});
|
2025-11-15 07:59:35 +00:00
|
|
|
|
2025-11-24 05:47:49 +00:00
|
|
|
const customMaterial = useMemo(() => {
|
|
|
|
|
// Only use alpha-as-roughness material for borg shapes
|
|
|
|
|
if (!isOrganic) {
|
|
|
|
|
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
|
|
|
|
shaderMaterial.map = texture;
|
|
|
|
|
return shaderMaterial;
|
|
|
|
|
}
|
2025-11-15 07:43:31 +00:00
|
|
|
|
2025-11-24 05:47:49 +00:00
|
|
|
// For non-borg shapes, use the original GLTF material with updated texture
|
|
|
|
|
const clonedMaterial = material.clone();
|
|
|
|
|
clonedMaterial.map = texture;
|
|
|
|
|
clonedMaterial.transparent = true;
|
|
|
|
|
clonedMaterial.alphaTest = 0.9;
|
2025-12-01 00:41:41 +00:00
|
|
|
clonedMaterial.side = 2; // DoubleSide
|
2025-11-24 05:47:49 +00:00
|
|
|
return clonedMaterial;
|
|
|
|
|
}, [material, texture, isOrganic]);
|
2025-11-15 07:43:31 +00:00
|
|
|
|
2025-11-24 05:47:49 +00:00
|
|
|
return <primitive object={customMaterial} attach="material" />;
|
2025-11-15 03:33:44 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 06:33:12 +00:00
|
|
|
export function ShapeTexture({
|
|
|
|
|
material,
|
|
|
|
|
shapeName,
|
|
|
|
|
}: {
|
|
|
|
|
material?: MeshStandardMaterial;
|
|
|
|
|
shapeName?: string;
|
|
|
|
|
}) {
|
|
|
|
|
const flagNames = new Set(material.userData.flag_names ?? []);
|
|
|
|
|
const isIflMaterial = flagNames.has("IflMaterial");
|
|
|
|
|
const resourcePath = material.userData.resource_path;
|
|
|
|
|
|
|
|
|
|
// Use IflTexture for animated materials
|
|
|
|
|
if (isIflMaterial && resourcePath) {
|
|
|
|
|
return <IflTexture material={material} shapeName={shapeName} />;
|
|
|
|
|
} else {
|
|
|
|
|
return <StaticTexture material={material} shapeName={shapeName} />;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 01:42:59 +00:00
|
|
|
export function ShapePlaceholder({
|
|
|
|
|
color,
|
|
|
|
|
label,
|
|
|
|
|
}: {
|
|
|
|
|
color: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
}) {
|
2025-11-15 03:33:44 +00:00
|
|
|
return (
|
|
|
|
|
<mesh>
|
|
|
|
|
<boxGeometry args={[10, 10, 10]} />
|
|
|
|
|
<meshStandardMaterial color={color} wireframe />
|
2025-12-01 01:42:59 +00:00
|
|
|
{label ? <FloatingLabel color={color}>{label}</FloatingLabel> : null}
|
2025-11-15 03:33:44 +00:00
|
|
|
</mesh>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-11-24 05:47:49 +00:00
|
|
|
|
2025-12-01 01:42:59 +00:00
|
|
|
export function DebugPlaceholder({
|
|
|
|
|
color,
|
|
|
|
|
label,
|
|
|
|
|
}: {
|
|
|
|
|
color: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
}) {
|
2025-11-27 01:19:17 +00:00
|
|
|
const { debugMode } = useDebug();
|
2025-12-01 01:42:59 +00:00
|
|
|
return debugMode ? <ShapePlaceholder color={color} label={label} /> : null;
|
2025-11-27 01:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 01:36:41 +00:00
|
|
|
export const ShapeModel = memo(function ShapeModel() {
|
2025-11-24 05:47:49 +00:00
|
|
|
const { shapeName } = useShapeInfo();
|
2025-11-26 07:44:37 +00:00
|
|
|
const { debugMode } = useDebug();
|
2025-11-24 05:47:49 +00:00
|
|
|
const { nodes } = useStaticShape(shapeName);
|
|
|
|
|
|
|
|
|
|
const hullBoneIndices = useMemo(() => {
|
|
|
|
|
const skeletonsFound = Object.values(nodes).filter(
|
2025-11-29 17:08:20 +00:00
|
|
|
(node: any) => node.skeleton,
|
2025-11-24 05:47:49 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (skeletonsFound.length > 0) {
|
|
|
|
|
const skeleton = (skeletonsFound[0] as any).skeleton;
|
|
|
|
|
return getHullBoneIndices(skeleton);
|
|
|
|
|
}
|
|
|
|
|
return new Set<number>();
|
|
|
|
|
}, [nodes]);
|
|
|
|
|
|
|
|
|
|
const processedNodes = useMemo(() => {
|
|
|
|
|
return Object.entries(nodes)
|
|
|
|
|
.filter(
|
|
|
|
|
([name, node]: [string, any]) =>
|
|
|
|
|
node.material &&
|
|
|
|
|
node.material.name !== "Unassigned" &&
|
2025-11-29 17:08:20 +00:00
|
|
|
!node.name.match(/^Hulk/i),
|
2025-11-24 05:47:49 +00:00
|
|
|
)
|
|
|
|
|
.map(([name, node]: [string, any]) => {
|
|
|
|
|
const geometry = filterGeometryByVertexGroups(
|
|
|
|
|
node.geometry,
|
2025-11-29 17:08:20 +00:00
|
|
|
hullBoneIndices,
|
2025-11-24 05:47:49 +00:00
|
|
|
);
|
|
|
|
|
return { node, geometry };
|
|
|
|
|
});
|
|
|
|
|
}, [nodes, hullBoneIndices]);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-26 00:56:54 +00:00
|
|
|
<group rotation={[0, Math.PI / 2, 0]}>
|
2025-11-24 05:47:49 +00:00
|
|
|
{processedNodes.map(({ node, geometry }) => (
|
|
|
|
|
<mesh key={node.id} geometry={geometry} castShadow receiveShadow>
|
|
|
|
|
{node.material ? (
|
|
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
// Allow the mesh to render while the texture is still loading;
|
|
|
|
|
// show a wireframe placeholder.
|
|
|
|
|
<meshStandardMaterial color="gray" wireframe />
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{Array.isArray(node.material) ? (
|
|
|
|
|
node.material.map((mat, index) => (
|
|
|
|
|
<ShapeTexture
|
|
|
|
|
key={index}
|
|
|
|
|
material={mat as MeshStandardMaterial}
|
|
|
|
|
shapeName={shapeName}
|
|
|
|
|
/>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<ShapeTexture
|
|
|
|
|
material={node.material as MeshStandardMaterial}
|
|
|
|
|
shapeName={shapeName}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Suspense>
|
|
|
|
|
) : null}
|
|
|
|
|
</mesh>
|
|
|
|
|
))}
|
|
|
|
|
{debugMode ? <FloatingLabel>{shapeName}</FloatingLabel> : null}
|
2025-11-26 00:56:54 +00:00
|
|
|
</group>
|
2025-11-24 05:47:49 +00:00
|
|
|
);
|
2025-11-26 01:36:41 +00:00
|
|
|
});
|