posts

웨이퍼맵 3D 완전 구현 가이드

Oct 1, 2025 updated Oct 1, 2025 3dtypescript

개요

이 문서는 웨이퍼맵 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)

🔧 문제 해결 가이드

일반적인 문제와 해결책

  1. Shot이 너무 많아서 느려요

    • InstancedMesh 사용 확인
    • LOD 시스템 적용
    • Frustum Culling 활성화
  2. 메모리 사용량이 너무 높아요

    • 텍스처 크기 줄이기
    • 불필요한 Shot 필터링
    • 지오메트리 재사용
  3. 색상이 제대로 표시되지 않아요

    • 색상 범위 정규화 확인
    • 색상 매핑 알고리즘 검증
    • 머티리얼 설정 확인
  4. 클릭 선택이 작동하지 않아요

    • Raycaster 설정 확인
    • InstancedMesh 인스턴스 ID 처리
    • 바운딩 박스 업데이트

이 가이드를 통해 웨이퍼맵 3D 시스템의 핵심 4단계 프로세스를 이해하고, 100-1000개 Shot을 효율적으로 렌더링할 수 있는 최적화된 시스템을 구현할 수 있습니다.