react-native-three-fiber-globe
Production-ready 3D globe implementation with React Three Fiber for React Native. Use when implementing interactive 3D globes with GLB models, country/continent selection, smooth camera animations, and gesture controls in Expo apps. Optimized for iOS 60fps performance.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: react-native-three-fiber-globe
description: Production-ready 3D globe implementation with React Three Fiber for React Native. Use when implementing interactive 3D globes with GLB models, country/continent selection, smooth camera animations, and gesture controls in Expo apps. Optimized for iOS 60fps performance.
---
# React Native Three Fiber Globe Implementation
Complete guide for implementing interactive 3D globes using @react-three/fiber with pre-built GLB models in React Native Expo apps.
## Technology Stack
**Core dependencies:**
```json
{
"three": "^0.166.0",
"@react-three/fiber": "^8.17.10",
"@react-three/drei": "^9.120.3",
"expo-gl": "~17.0.0",
"react-native-gesture-handler": "^2.16.0",
"react-native-reanimated": "^4.0.0"
}
```
**CRITICAL: Always use `/native` imports:**
```typescript
// ✅ Correct
import { Canvas, useFrame, useThree } from '@react-three/fiber/native';
import { useGLTF } from '@react-three/drei/native';
// ❌ Wrong (these are web-only)
import { Canvas } from '@react-three/fiber';
import { useGLTF } from '@react-three/drei';
```
## Core Implementation Pattern
### Loading GLB Models
```typescript
import { useGLTF } from '@react-three/drei/native';
import * as THREE from 'three';
function GlobeModel({ modelPath }: { modelPath: any }) {
const gltf = useGLTF(modelPath);
const groupRef = useRef<THREE.Group>(null);
return (
<group ref={groupRef}>
<primitive object={gltf.scene} scale={1.5} />
</group>
);
}
// Preload for faster initial render
useGLTF.preload(require('../assets/globe.glb'));
```
### Accessing Blender Custom Properties
```typescript
// GLB files from Blender include userData
useEffect(() => {
if (!gltf.scene) return;
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
// Access custom properties from Blender
const countryName = child.userData.country_name;
const continent = child.userData.continent;
console.log(`Country: ${countryName}, Continent: ${continent}`);
}
});
}, [gltf]);
```
## Raycasting for Country Selection
### Hit Detection
```typescript
import { useThree } from '@react-three/fiber/native';
import * as THREE from 'three';
function CountrySelector() {
const { camera, scene, size } = useThree();
const raycaster = useRef(new THREE.Raycaster());
const handleTap = useCallback((touchX: number, touchY: number) => {
// Normalize screen coordinates to -1 to 1
const pointer = new THREE.Vector2(
(touchX / size.width) * 2 - 1,
-(touchY / size.height) * 2 + 1
);
raycaster.current.setFromCamera(pointer, camera);
// Intersect with all meshes recursively
const intersects = raycaster.current.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const object = intersects[0].object;
if (object.userData.country_name) {
const country = object.userData.country_name;
const continent = object.userData.continent;
onCountrySelect(country, continent);
}
}
}, [camera, scene, size, onCountrySelect]);
return null;
}
```
## Gesture Handling with Reanimated
### Pan + Pinch + Tap Gestures
```typescript
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue, withDecay, withSpring } from 'react-native-reanimated';
function InteractiveGlobe() {
const rotationX = useSharedValue(0);
const rotationY = useSharedValue(0);
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
// Pan gesture for rotation
const panGesture = Gesture.Pan()
.onStart(() => {
lastRotationX.value = rotationX.value;
lastRotationY.value = rotationY.value;
})
.onUpdate((e) => {
const sensitivity = 0.005;
rotationY.value = lastRotationY.value + e.translationX * sensitivity;
rotationX.value = lastRotationX.value + e.translationY * sensitivity;
// Clamp X rotation to prevent flipping
rotationX.value = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, rotationX.value));
})
.onEnd((e) => {
// Apply momentum with decay
rotationX.value = withDecay({
velocity: e.velocityX * 0.001,
deceleration: 0.998,
});
rotationY.value = withDecay({
velocity: e.velocityY * 0.001,
deceleration: 0.998,
});
});
// Pinch gesture for zoom
const pinchGesture = Gesture.Pinch()
.onUpdate((e) => {
scale.value = Math.max(0.5, Math.min(5, savedScale.value * e.scale));
})
.onEnd(() => {
savedScale.value = scale.value;
// Spring back to limits if exceeded
if (scale.value < 1) {
scale.value = withSpring(1, { damping: 15 });
savedScale.value = 1;
}
});
// Tap gesture for selection
const tapGesture = Gesture.Tap()
.onEnd((e) => {
runOnJS(handleCountryTap)(e.x, e.y);
});
const composed = Gesture.Simultaneous(panGesture, pinchGesture, tapGesture);
return (
<GestureDetector gesture={composed}>
{/* Canvas here */}
</GestureDetector>
);
}
```
## Camera Animations
### Smooth Camera Transitions (iOS-Native Timing)
```typescript
import { useFrame, useThree } from '@react-three/fiber/native';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
// iOS-native animation timing
const ANIMATION_CONFIG = {
duration: 1500, // 1.5 seconds
easing: 'cubic-bezier(0.65, 0, 0.35, 1)', // Cubic ease-in-out
};
function GlobeControls() {
const { camera } = useThree();
const targetPosition = useRef(new THREE.Vector3(0, 0, 5));
const targetLookAt = useRef(new THREE.Vector3(0, 0, 0));
const isAnimating = useRef(false);
// Animate camera to focus on country
const focusOnCountry = useCallback((country: string) => {
const position = calculateCountryPosition(country);
targetPosition.current.set(...position);
isAnimating.current = true;
setTimeout(() => {
isAnimating.current = false;
}, 1700);
}, []);
// Smooth interpolation every frame
useFrame((state, delta) => {
if (isAnimating.current) {
// Lerp camera position
camera.position.lerp(targetPosition.current, 0.05);
// Always look at globe center
const lookAt = new THREE.Vector3();
camera.getWorldDirection(lookAt);
lookAt.lerp(targetLookAt.current, 0.05);
camera.lookAt(lookAt);
}
});
return null;
}
```
## Fade Effects for Non-Selected Regions
### Coordinated Opacity + Emissive Changes
```typescript
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
function GlobeModel({ selectedCountry, selectedContinent }) {
const groupRef = useRef<THREE.Group>(null);
useEffect(() => {
if (!groupRef.current) return;
groupRef.current.traverse((child) => {
if (child instanceof THREE.Mesh) {
const countryName = child.userData.country_name;
const continent = child.userData.continent;
// Ensure material supports transparency
if (child.material) {
child.material.transparent = true;
if (selectedCountry) {
// Fade all except selected country
if (countryName === selectedCountry) {
child.material.opacity = 1.0;
child.material.emissive = new THREE.Color(0x4466ff);
child.material.emissiveIntensity = 0.3;
} else {
child.material.opacity = 0.15; // Significant fade
child.material.emissive = new THREE.Color(0x000000);
child.material.emissiveIntensity = 0;
}
} else if (selectedContinent) {
// Fade all except selected continent
if (continent === selectedContinent) {
child.material.opacity = 1.0;
} else {
child.material.opacity = 0.2;
}
} else {
// Reset all
child.material.opacity = 1.0;
child.material.emissive = new THREE.Color(0x000000);
child.material.emissiveIntensity = 0;
}
}
}
});
}, [selectedCountry, selectedContinent]);
return (
<group ref={groupRef}>
<primitive object={gltf.scene} scale={1.5} />
</group>
);
}
```
## Lighting Setup
### Optimized for Mobile Performance
```typescript
<Canvas>
{/* Ambient light for base illumination */}
<ambientLight intensity={0.5} />
{/* Directional light for depth */}
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow={false} // Disable shadows for performance
/>
{/* Subtle fill light */}
<pointLight
position={[-10, -10, -5]}
intensity={0.3}
/>
{/* Globe model */}
<GlobeModel />
</Canvas>
```
## Performance Optimization
### Critical Optimizations for 60fps
**1. Pause Rendering When Inactive**
```typescript
import { useFocusEffect } from '@react-navigation/native';
const GlobeScreen = () => {
const [isActive, setIsActive] = useState(false);
useFocusEffect(
useCallback(() => {
setIsActive(true);
return () => setIsActive(false);
}, [])
);
if (!isActive) return <View style={{ flex: 1 }} />;
return <InteractiveGlobe />;
};
```
**2. Memoize Expensive Calculations**
```typescript
const countryPaths = useMemo(() => {
return countries.map(country => ({
id: country.id,
position: calculatePosition(country),
}));
}, [countries]);
```
**3. Dispose Resources on Unmount**
```typescript
useEffect(() => {
return () => {
// Cleanup Three.js resources
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach(mat => mat.dispose());
} else {
child.material.dispose();
}
}
});
};
}, [gltf]);
```
**4. Monitor FPS (Dev Only)**
```typescript
const PerformanceMonitor = () => {
const lastTime = useRef(Date.now());
const frames = useRef(0);
useFrame(() => {
if (__DEV__) {
frames.current++;
const now = Date.now();
if (now - lastTime.current >= 1000) {
console.log(`FPS: ${frames.current}`);
frames.current = 0;
lastTime.current = now;
}
}
});
return null;
};
```
## Common Issues
### Issue: "Canvas not rendering"
**Solution**: Verify correct imports from `/native` paths. Check expo-gl is installed.
### Issue: "Touch gestures not working"
**Solution**: Wrap in GestureHandlerRootView. Ensure react-native-gesture-handler is configured.
### Issue: "Low FPS on device"
**Solution**: Test on PHYSICAL device (simulator misleading). Check polygon count <150k. Disable shadows.
### Issue: "Raycasting misses countries"
**Solution**: Verify recursive intersect (third param `true`). Check camera updated before raycasting.
### Issue: "App crashes on unmount"
**Solution**: Add proper cleanup to dispose Three.js geometries, materials, textures.
## Performance Targets
**Expected metrics:**
- Frame rate: 60fps sustained on iPhone 12+
- Load time: 1-2 seconds for <1MB GLB
- Memory: 30-50MB during active use
- Battery: 5-10% drain per hour
- File size: GLB <10MB with Draco compression
**Polygon budget:**
- Globe base: 4k-8k triangles
- Countries: 100k-150k triangles total
- **Critical**: Stay under 200k triangles for 60fps
## Testing Checklist
- [ ] Test on physical iOS device (simulator unreliable)
- [ ] Verify 60fps with PerformanceMonitor
- [ ] Test gestures (pan, pinch, tap) simultaneously
- [ ] Check country selection accuracy at different zoom levels
- [ ] Verify animations match iOS timing (1500ms zoom)
- [ ] Test memory stability over 5+ minutes
- [ ] Measure battery drain over 30 minutes
- [ ] Verify safe areas respected (notch, home indicator)
- [ ] Test touch targets ≥44x44pt equivalent
## Integration with Brand Guidelines
**Animation timing from brand guidelines:**
- Country selection zoom: 1500ms cubic ease-in-out
- Fade effects: 800ms ease-out with 200ms stagger
- Spring physics: damping 15, stiffness 140
- Always test on physical device
**Visual style:**
- Background: Dark (#1a1a1a) for depth
- Atmosphere: Subtle blue glow (opacity 0.2-0.3)
- Selection highlight: Brand accent color with emissive
- Typography: SF Pro (iOS default) for labels
## Key Principles
1. **Physical device testing mandatory** - Simulator gives false performance
2. **Always use /native imports** - Web imports will fail at runtime
3. **Coordinate animations** - Rotation + zoom + fade start together
4. **Dispose resources** - Memory leaks will crash app
5. **Touch targets matter** - Raycasting must feel precise
6. **60fps is non-negotiable** - Acceptable 45fps on iPhone 11 only
7. **Test gestures extensively** - Pan + pinch conflicts common
---
**Use this skill when:** Implementing 3D globes with GLB models in React Native using @react-three/fiber. For 2.5D projections with GeoJSON, use react-native-skia-globe skill instead.