웨이퍼맵 3D 완전 구현 가이드
개요
이 문서는 웨이퍼맵 3D 시스템의 핵심 4단계 프로세스와 100-1000개 Shot 렌더링 최적화를 위한 완전한 구현 가이드입니다.
🎯 핵심 4단계 프로세스
1단계: 데이터 입력 (Raw Data Input)
데이터 구조
interface WaferData {
wafer: {
id: string;
diameter: number; // 웨이퍼 지름 (mm)
thickness: number; // 웨이퍼 두께 (mm)
center: { x: number; y: number; z: number };
};
shots: Shot[];
focusSpots: FocusSpot[];
}
interface Shot {
id: number;
x: number; // 웨이퍼 좌표계 X (mm)
y: number; // 웨이퍼 좌표계 Y (mm)
z?: number; // 웨이퍼 좌표계 Z (mm)
value: number; // 측정값
isValid: boolean; // 유효성 여부
}
데이터 검증 로직
class WaferDataValidator {
validate(data: WaferData): ValidationResult {
const errors: string[] = [];
// 웨이퍼 크기 검증
if (data.wafer.diameter <= 0 || data.wafer.diameter > 450) {
errors.push('웨이퍼 지름이 유효하지 않습니다');
}
// Shot 위치 검증 (웨이퍼 원 안에 있는지)
const radius = data.wafer.diameter / 2;
data.shots.forEach(shot => {
const distance = Math.sqrt(shot.x ** 2 + shot.y ** 2);
if (distance > radius) {
errors.push(`Shot ${shot.id}가 웨이퍼 영역을 벗어났습니다`);
}
});
// 측정값 범위 검증
const values = data.shots.filter(s => s.isValid).map(s => s.value);
if (values.length === 0) {
errors.push('유효한 측정값이 없습니다');
}
return {
isValid: errors.length === 0,
errors,
validShotCount: data.shots.filter(s => s.isValid).length
};
}
}
2단계: 컴포넌트 데이터 전달 (Component Data Transmission)
React 컴포넌트 구조
// 메인 웨이퍼맵 컴포넌트
interface WaferMap3DProps {
data: WaferData;
mode: 'shot' | 'focusSpot';
onShotSelect?: (shot: Shot) => void;
onError?: (error: Error) => void;
}
const WaferMap3D: React.FC<WaferMap3DProps> = ({ data, mode, onShotSelect, onError }) => {
const [processedData, setProcessedData] = useState<ProcessedWaferData | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const processData = async () => {
try {
setIsLoading(true);
const processor = new WaferDataProcessor();
const processed = await processor.process(data);
setProcessedData(processed);
} catch (error) {
onError?.(error as Error);
} finally {
setIsLoading(false);
}
};
processData();
}, [data]);
if (isLoading) return <LoadingSpinner />;
if (!processedData) return <ErrorDisplay />;
return (
<Canvas camera={{ position: [0, 0, 100], fov: 75 }}>
<Scene3D>
<WaferGeometry wafer={processedData.wafer} />
{mode === 'shot' && (
<ShotRenderer
shots={processedData.shots}
onSelect={onShotSelect}
/>
)}
{mode === 'focusSpot' && (
<FocusSpotRenderer spots={processedData.focusSpots} />
)}
</Scene3D>
</Canvas>
);
};
데이터 전달 최적화
// 메모이제이션을 통한 불필요한 재렌더링 방지
const ShotRenderer = React.memo<ShotRendererProps>(({ shots, onSelect }) => {
const memoizedShots = useMemo(() => {
return shots.filter(shot => shot.isValid);
}, [shots]);
return <ShotInstancedMesh shots={memoizedShots} onSelect={onSelect} />;
});
// 청크 단위 데이터 처리 (대용량 데이터 대응)
class ChunkedDataProcessor {
private chunkSize = 100;
async processInChunks<T>(data: T[], processor: (chunk: T[]) => Promise<any[]>) {
const results = [];
for (let i = 0; i < data.length; i += this.chunkSize) {
const chunk = data.slice(i, i + this.chunkSize);
const chunkResult = await processor(chunk);
results.push(...chunkResult);
// 브라우저가 다른 작업을 할 수 있도록 양보
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
}
3단계: 데이터 파싱 (Data Parsing)
좌표계 변환
class CoordinateTransformer {
// 웨이퍼 좌표계 → Three.js 월드 좌표계
waferToWorld(waferCoord: { x: number; y: number; z: number }): Vector3 {
// 웨이퍼 좌표계: 중심 (0,0), 단위 mm
// Three.js 좌표계: Y축 위쪽, 단위 Three.js unit
const scale = 0.1; // 1mm = 0.1 Three.js unit (스케일 조정)
return new Vector3(
waferCoord.x * scale, // X축 그대로
waferCoord.z * scale, // Z → Y (높이)
-waferCoord.y * scale // Y → -Z (깊이, 뒤집기)
);
}
// Three.js 월드 좌표계 → 웨이퍼 좌표계 (역변환)
worldToWafer(worldCoord: Vector3): { x: number; y: number; z: number } {
const scale = 10; // 0.1의 역수
return {
x: worldCoord.x * scale,
y: -worldCoord.z * scale,
z: worldCoord.y * scale
};
}
}
색상 매핑 시스템
class ColorMapper {
private colorScale: Color[];
private valueRange: { min: number; max: number };
constructor(values: number[], colorSteps: number = 25) {
this.valueRange = {
min: Math.min(...values),
max: Math.max(...values)
};
this.colorScale = this.generateColorScale(colorSteps);
}
private generateColorScale(steps: number): Color[] {
const colors: Color[] = [];
for (let i = 0; i < steps; i++) {
const t = i / (steps - 1);
// 파란색 → 초록색 → 노란색 → 빨간색 그라데이션
let r, g, b;
if (t < 0.33) {
// 파란색 → 초록색
const localT = t / 0.33;
r = 0;
g = localT;
b = 1 - localT;
} else if (t < 0.66) {
// 초록색 → 노란색
const localT = (t - 0.33) / 0.33;
r = localT;
g = 1;
b = 0;
} else {
// 노란색 → 빨간색
const localT = (t - 0.66) / 0.34;
r = 1;
g = 1 - localT;
b = 0;
}
colors.push(new Color(r, g, b));
}
return colors;
}
mapValueToColor(value: number): Color {
if (this.valueRange.max === this.valueRange.min) {
return this.colorScale[0];
}
const normalizedValue = (value - this.valueRange.min) /
(this.valueRange.max - this.valueRange.min);
const index = Math.floor(normalizedValue * (this.colorScale.length - 1));
const clampedIndex = Math.max(0, Math.min(this.colorScale.length - 1, index));
return this.colorScale[clampedIndex];
}
}
데이터 정규화
class DataNormalizer {
// Shot 크기 정규화 (웨이퍼 크기에 따라 조정)
normalizeShotSize(waferDiameter: number, shotCount: number): number {
// 기본 공식: 웨이퍼가 클수록, Shot이 많을수록 작게
const baseSize = 2.0; // 기본 크기 (mm)
const diameterFactor = 300 / waferDiameter; // 300mm 기준
const densityFactor = Math.sqrt(100 / shotCount); // 100개 기준
return baseSize * diameterFactor * densityFactor;
}
// 높이 정규화 (Z축 값 조정)
normalizeHeight(value: number, valueRange: { min: number; max: number }): number {
const maxHeight = 5.0; // 최대 높이 (mm)
const normalizedValue = (value - valueRange.min) / (valueRange.max - valueRange.min);
return normalizedValue * maxHeight;
}
}
4단계: 3D 렌더링 (3D Rendering)
InstancedMesh 기반 Shot 렌더링 (100-1000개 최적화)
class OptimizedShotRenderer {
private instancedMesh: InstancedMesh;
private colorAttribute: InstancedBufferAttribute;
private maxInstances: number;
constructor(maxInstances: number = 1000) {
this.maxInstances = maxInstances;
this.initializeInstancedMesh();
}
private initializeInstancedMesh() {
// 기본 Shot 지오메트리 (사각형)
const geometry = new PlaneGeometry(1, 1);
// 기본 머티리얼
const material = new MeshBasicMaterial({
side: DoubleSide,
transparent: true,
opacity: 0.8
});
// InstancedMesh 생성
this.instancedMesh = new InstancedMesh(
geometry,
material,
this.maxInstances
);
// 색상 속성 추가
this.colorAttribute = new InstancedBufferAttribute(
new Float32Array(this.maxInstances * 3),
3
);
this.instancedMesh.geometry.setAttribute('instanceColor', this.colorAttribute);
// 초기에는 모든 인스턴스 숨김
this.instancedMesh.count = 0;
}
updateShots(shots: ProcessedShot[]) {
const shotCount = Math.min(shots.length, this.maxInstances);
// 성능 최적화: 배치 업데이트
const matrices: Matrix4[] = [];
const colors: Color[] = [];
for (let i = 0; i < shotCount; i++) {
const shot = shots[i];
// 변환 행렬 생성
const matrix = new Matrix4();
matrix.setPosition(shot.position);
matrix.scale(new Vector3(shot.size, shot.size, 1));
matrices.push(matrix);
// 색상 설정
colors.push(shot.color);
}
// 배치 업데이트 적용
this.batchUpdateInstances(matrices, colors);
// 렌더링할 인스턴스 수 설정
this.instancedMesh.count = shotCount;
}
private batchUpdateInstances(matrices: Matrix4[], colors: Color[]) {
// 행렬 배치 업데이트
for (let i = 0; i < matrices.length; i++) {
this.instancedMesh.setMatrixAt(i, matrices[i]);
}
// 색상 배치 업데이트
for (let i = 0; i < colors.length; i++) {
this.colorAttribute.setXYZ(i, colors[i].r, colors[i].g, colors[i].b);
}
// GPU 업데이트 플래그 설정
this.instancedMesh.instanceMatrix.needsUpdate = true;
this.colorAttribute.needsUpdate = true;
}
}
성능 최적화 전략
1. LOD (Level of Detail) 시스템
class LODManager {
private lodLevels = [
{ distance: 50, shotSize: 1.0, showLabels: true },
{ distance: 100, shotSize: 0.7, showLabels: false },
{ distance: 200, shotSize: 0.5, showLabels: false },
{ distance: 500, shotSize: 0.3, showLabels: false }
];
updateLOD(camera: Camera, shots: ProcessedShot[]) {
const distance = camera.position.length();
const lodLevel = this.getLODLevel(distance);
// Shot 크기 조정
shots.forEach(shot => {
shot.size *= lodLevel.shotSize;
});
// 라벨 표시/숨김
this.toggleLabels(lodLevel.showLabels);
}
private getLODLevel(distance: number) {
for (const level of this.lodLevels) {
if (distance <= level.distance) {
return level;
}
}
return this.lodLevels[this.lodLevels.length - 1];
}
}
2. Frustum Culling
class FrustumCuller {
cullShots(shots: ProcessedShot[], camera: Camera): ProcessedShot[] {
const frustum = new Frustum();
const matrix = new Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(matrix);
return shots.filter(shot => {
const sphere = new Sphere(shot.position, shot.size);
return frustum.intersectsSphere(sphere);
});
}
}
3. 메모리 관리
class MemoryManager {
private memoryThreshold = 100 * 1024 * 1024; // 100MB
checkMemoryUsage(): boolean {
if ('memory' in performance) {
const memory = (performance as any).memory;
return memory.usedJSHeapSize < this.memoryThreshold;
}
return true;
}
optimizeForMemory(shots: ProcessedShot[]): ProcessedShot[] {
if (this.checkMemoryUsage()) {
return shots;
}
// 메모리 부족 시 Shot 수 줄이기
const maxShots = Math.floor(shots.length * 0.7);
return shots.slice(0, maxShots);
}
}
🎨 실제 구현 예제
React Three Fiber 통합
const WaferMap3DScene: React.FC<{ data: WaferData }> = ({ data }) => {
const shotRendererRef = useRef<OptimizedShotRenderer>();
const [processedData, setProcessedData] = useState<ProcessedWaferData>();
useEffect(() => {
const processor = new WaferDataProcessor();
const processed = processor.process(data);
setProcessedData(processed);
}, [data]);
useFrame((state) => {
if (shotRendererRef.current && processedData) {
// LOD 업데이트
const lodManager = new LODManager();
lodManager.updateLOD(state.camera, processedData.shots);
// Frustum Culling
const culler = new FrustumCuller();
const visibleShots = culler.cullShots(processedData.shots, state.camera);
// Shot 렌더링 업데이트
shotRendererRef.current.updateShots(visibleShots);
}
});
return (
<>
<ambientLight intensity={0.6} />
<directionalLight position={[10, 10, 5]} intensity={0.8} />
{processedData && (
<>
<WaferGeometry wafer={processedData.wafer} />
<primitive
object={shotRendererRef.current?.instancedMesh}
ref={shotRendererRef}
/>
</>
)}
</>
);
};
📊 성능 벤치마크
예상 성능 지표
| Shot 개수 | 렌더링 방식 | FPS | Draw Calls | 메모리 사용량 |
|---|---|---|---|---|
| 100개 | Individual Mesh | 30-40 | 100+ | 50MB |
| 100개 | InstancedMesh | 60 | 1 | 20MB |
| 500개 | Individual Mesh | 10-15 | 500+ | 200MB |
| 500개 | InstancedMesh | 55-60 | 1 | 35MB |
| 1000개 | Individual Mesh | 5-10 | 1000+ | 400MB |
| 1000개 | InstancedMesh | 50-55 | 1 | 50MB |
최적화 체크리스트
- InstancedMesh 사용으로 Draw Call 최소화
- LOD 시스템으로 거리별 상세도 조정
- Frustum Culling으로 화면 밖 객체 제외
- 메모리 사용량 모니터링 및 제한
- 색상 매핑 최적화 (미리 계산된 팔레트 사용)
- 텍스처 아틀라스 사용 (라벨 텍스트)
- 지오메트리 재사용 (동일한 Shape)
🔧 문제 해결 가이드
일반적인 문제와 해결책
Shot이 너무 많아서 느려요
- InstancedMesh 사용 확인
- LOD 시스템 적용
- Frustum Culling 활성화
메모리 사용량이 너무 높아요
- 텍스처 크기 줄이기
- 불필요한 Shot 필터링
- 지오메트리 재사용
색상이 제대로 표시되지 않아요
- 색상 범위 정규화 확인
- 색상 매핑 알고리즘 검증
- 머티리얼 설정 확인
클릭 선택이 작동하지 않아요
- Raycaster 설정 확인
- InstancedMesh 인스턴스 ID 처리
- 바운딩 박스 업데이트
이 가이드를 통해 웨이퍼맵 3D 시스템의 핵심 4단계 프로세스를 이해하고, 100-1000개 Shot을 효율적으로 렌더링할 수 있는 최적화된 시스템을 구현할 수 있습니다.