AgentSkillsCN

debugging-3d-mobile

调试 React Native 3D 问题——iOS 模拟器难题、Android 差异、性能剖析、内存泄漏、空白屏幕、触摸失灵,以及常见的开发陷阱。

SKILL.md
--- frontmatter
name: debugging-3d-mobile
description: Debug React Native 3D issues - iOS simulator problems, Android differences, performance profiling, memory leaks, blank screens, touch not working, and common pitfalls.

Debugging 3D in React Native

This skill covers diagnosing and fixing common 3D rendering issues in React Native apps.

Critical Knowledge: iOS Simulator Limitations

The iOS Simulator has incomplete and unreliable OpenGL-ES support.

Symptoms in Simulator

  • EXC_BAD_ACCESS crashes
  • Blank canvas
  • Touch events not firing
  • Textures not loading
  • Shaders failing silently
  • Performance drastically worse than device

The Rule

code
If it works on physical device but fails in simulator → Simulator problem
If it fails on both → Your code problem

ALWAYS test 3D on physical device before extensive debugging.

Quick Device Test Setup

bash
# Connect physical iOS device, then:
npx expo run:ios --device

# Or for Android:
npx expo run:android --device

Diagnostic Flowchart

code
Is the Canvas rendering anything?
├─ NO (blank/black screen)
│   ├─ Check: Is Canvas in a View with explicit dimensions?
│   ├─ Check: Is camera positioned to see objects?
│   ├─ Check: Is there any light source?
│   ├─ Check: Console errors? (esp. GL errors)
│   └─ Try: Add a simple <mesh> to verify Canvas works
│
└─ YES (something renders)
    │
    ├─ Objects look wrong?
    │   ├─ Check: Camera near/far planes
    │   ├─ Check: Object scale vs camera distance
    │   └─ Check: Material needs light?
    │
    ├─ Touch not working?
    │   ├─ Check: Is OrbitControls capturing events?
    │   ├─ Check: Does object have raycastable geometry?
    │   ├─ Check: Is there a blocking View/component?
    │   └─ Try: Add onPointerMissed to Canvas
    │
    └─ Performance issues?
        ├─ Check: Are you creating objects every render?
        ├─ Check: How many draw calls?
        └─ Check: Texture sizes

Issue: Blank/Black Canvas

Cause 1: No Dimensions

tsx
// ❌ WRONG - Canvas has no size
<Canvas>
  <mesh>...</mesh>
</Canvas>

// ✅ CORRECT - Wrap in View with dimensions
<View style={{ flex: 1 }}>
  <Canvas>
    <mesh>...</mesh>
  </Canvas>
</View>

// ✅ ALSO CORRECT - Explicit dimensions
<View style={{ width: 300, height: 400 }}>
  <Canvas>
    <mesh>...</mesh>
  </Canvas>
</View>

Cause 2: Camera Inside or Behind Objects

tsx
// Default camera is at [0, 0, 0] looking at [0, 0, -1]
// If your objects are at [0, 0, 0], camera is INSIDE them

// ✅ Move camera back
<Canvas camera={{ position: [0, 0, 5] }}>

Cause 3: No Lights for PBR Materials

tsx
// ❌ meshStandardMaterial requires lights
<mesh>
  <boxGeometry />
  <meshStandardMaterial color="red" />  {/* Will be black without light */}
</mesh>

// ✅ Add lights
<>
  <ambientLight intensity={0.5} />
  <pointLight position={[10, 10, 10]} />
  <mesh>
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>
</>

// ✅ Or use meshBasicMaterial (no lighting needed)
<mesh>
  <boxGeometry />
  <meshBasicMaterial color="red" />
</mesh>

Cause 4: expo-gl Not Loaded

tsx
// Check if GL context is available
import { GLView } from 'expo-gl';

function DebugGL() {
  return (
    <GLView
      style={{ flex: 1 }}
      onContextCreate={(gl) => {
        console.log('GL Context created:', gl);
        console.log('GL Version:', gl.getParameter(gl.VERSION));
      }}
    />
  );
}

Issue: Touch Events Not Working

Debug Step 1: Verify Canvas Receives Events

tsx
<Canvas
  onPointerDown={() => console.log('Canvas: pointerdown')}
  onPointerUp={() => console.log('Canvas: pointerup')}
  onPointerMissed={() => console.log('Canvas: pointer missed (background)')}
>

Debug Step 2: Verify Object Receives Events

tsx
<mesh
  onPointerDown={(e) => {
    console.log('Mesh hit!', e.object.name);
    console.log('Hit point:', e.point);
    console.log('Distance:', e.distance);
  }}
>

Debug Step 3: Check for Blockers

tsx
// Common blocker: OrbitControls with no enable toggle
<OrbitControls />  // May capture ALL events

// Solution: Disable when interacting
<OrbitControls enabled={!isInteracting} />

Debug Step 4: Visualize Raycasting

tsx
function RaycastDebugger() {
  const { raycaster, camera, pointer, scene } = useThree();

  useFrame(() => {
    raycaster.setFromCamera(pointer, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);

    if (intersects.length > 0) {
      console.log('Would hit:', intersects.map(i => i.object.name || i.object.type));
    }
  });

  return null;
}

Debug Step 5: Check View Hierarchy

tsx
// ❌ TouchableOpacity overlay blocks Canvas
<View style={{ flex: 1 }}>
  <Canvas>...</Canvas>
  <TouchableOpacity style={StyleSheet.absoluteFill}>  {/* BLOCKS CANVAS */}
    ...
  </TouchableOpacity>
</View>

// ✅ Use pointerEvents="none" for overlays that shouldn't block
<View style={{ flex: 1 }}>
  <Canvas>...</Canvas>
  <View style={StyleSheet.absoluteFill} pointerEvents="none">
    {/* UI overlay */}
  </View>
</View>

Issue: Performance Problems

Diagnostic: Frame Rate

tsx
import { useFrame } from '@react-three/fiber/native';

function FPSCounter() {
  const frameCount = useRef(0);
  const lastTime = useRef(performance.now());

  useFrame(() => {
    frameCount.current++;
    const now = performance.now();
    if (now - lastTime.current >= 1000) {
      console.log(`FPS: ${frameCount.current}`);
      frameCount.current = 0;
      lastTime.current = now;
    }
  });

  return null;
}

Diagnostic: Draw Calls

tsx
function DrawCallCounter() {
  const { gl } = useThree();

  useFrame(() => {
    console.log('Draw calls:', gl.info.render.calls);
    console.log('Triangles:', gl.info.render.triangles);
    console.log('Geometries:', gl.info.memory.geometries);
    console.log('Textures:', gl.info.memory.textures);
  });

  return null;
}

Common Performance Killers

1. Creating Objects Every Render

tsx
// ❌ BAD - Creates new geometry every render
function BadMesh() {
  return (
    <mesh position={[0, 0, 0]}>
      <boxGeometry args={[1, 1, 1]} />  {/* NEW EVERY RENDER */}
      <meshStandardMaterial color="red" />  {/* NEW EVERY RENDER */}
    </mesh>
  );
}

// ✅ GOOD - Memoize expensive objects
function GoodMesh() {
  const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
  const material = useMemo(() => new THREE.MeshStandardMaterial({ color: 'red' }), []);

  return <mesh geometry={geometry} material={material} />;
}

2. Too Many Individual Objects

tsx
// ❌ BAD - 1000 separate meshes = 1000 draw calls
{nodes.map(node => (
  <mesh key={node.id} position={node.position}>
    <sphereGeometry />
    <meshStandardMaterial />
  </mesh>
))}

// ✅ GOOD - Use instancing
<instancedMesh args={[undefined, undefined, nodes.length]}>
  <sphereGeometry />
  <meshStandardMaterial />
</instancedMesh>

3. High-Poly Geometries

tsx
// ❌ BAD - Unnecessary detail for mobile
<sphereGeometry args={[1, 64, 64]} />  // 8192 triangles

// ✅ GOOD - Appropriate for mobile
<sphereGeometry args={[1, 16, 16]} />  // 512 triangles

Issue: Memory Leaks

Symptoms

  • App slows down over time
  • Eventually crashes with memory warning
  • Performance degrades after navigating away and back

Common Causes

1. Not Disposing Geometries/Materials

tsx
function ProperCleanup() {
  const geometryRef = useRef<THREE.BufferGeometry>();
  const materialRef = useRef<THREE.Material>();

  useEffect(() => {
    geometryRef.current = new THREE.BoxGeometry(1, 1, 1);
    materialRef.current = new THREE.MeshStandardMaterial();

    return () => {
      // CRITICAL: Dispose on unmount
      geometryRef.current?.dispose();
      materialRef.current?.dispose();
    };
  }, []);
}

2. Event Listeners Not Removed

tsx
function ProperEventCleanup() {
  const { gl } = useThree();

  useEffect(() => {
    const handleResize = () => { /* ... */ };
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
}

Memory Diagnostic

tsx
function MemoryMonitor() {
  const { gl } = useThree();

  useEffect(() => {
    const interval = setInterval(() => {
      console.log('=== Memory ===');
      console.log('Geometries:', gl.info.memory.geometries);
      console.log('Textures:', gl.info.memory.textures);
    }, 5000);

    return () => clearInterval(interval);
  }, [gl]);

  return null;
}

Platform Differences

BehavioriOSAndroid
WebGL VersionES 2.0/3.0ES 2.0/3.0
Max Texture SizeDevice-dependentDevice-dependent
Touch Latency~16ms~16ms
Simulator 3DUnreliableMore reliable
Shader Precisionhighp availablemediump default

Android-Specific Issues

tsx
// Android may need explicit pixel ratio
<Canvas dpr={[1, 2]}>  {/* Limit pixel ratio for performance */}

iOS-Specific Issues

tsx
// iOS may need explicit onLayout for sizing
<View
  style={{ flex: 1 }}
  onLayout={(e) => {
    const { width, height } = e.nativeEvent.layout;
    console.log('Canvas container size:', width, height);
  }}
>
  <Canvas>...</Canvas>
</View>

Complete Debug Component

Add this to your scene during development:

tsx
function DebugPanel() {
  const { gl, scene, camera } = useThree();
  const [stats, setStats] = useState({});

  useFrame((state, delta) => {
    setStats({
      fps: Math.round(1 / delta),
      drawCalls: gl.info.render.calls,
      triangles: gl.info.render.triangles,
      geometries: gl.info.memory.geometries,
      textures: gl.info.memory.textures,
      objects: scene.children.length,
      cameraPos: camera.position.toArray().map(n => n.toFixed(2)),
    });
  });

  return (
    <Html position={[0, 3, 0]}>
      <View style={styles.debugPanel}>
        <Text>FPS: {stats.fps}</Text>
        <Text>Draw Calls: {stats.drawCalls}</Text>
        <Text>Triangles: {stats.triangles}</Text>
        <Text>Geometries: {stats.geometries}</Text>
        <Text>Textures: {stats.textures}</Text>
        <Text>Camera: [{stats.cameraPos?.join(', ')}]</Text>
      </View>
    </Html>
  );
}

Quick Fixes Checklist

ProblemQuick Fix
Blank screenAdd dimensions to parent View
Black objectsAdd ambient/point light
Touch not workingDisable OrbitControls or add stopPropagation
Low FPSUse instancing, reduce geometry complexity
Memory growthDispose geometries/materials on unmount
Simulator crashTest on physical device
Objects invisibleCheck camera position and near/far planes
Lines too thinUse TubeGeometry or Line2 (WebGL 1px limit)