Skip to content

Instantly share code, notes, and snippets.

@bplunkert
Created January 18, 2026 05:21
Show Gist options
  • Select an option

  • Save bplunkert/916ea52d332bb0815a8eeacb8bf3f82f to your computer and use it in GitHub Desktop.

Select an option

Save bplunkert/916ea52d332bb0815a8eeacb8bf3f82f to your computer and use it in GitHub Desktop.
3d rendering with Claude
import React, { useState, useRef, useEffect, useCallback } from 'react';
import * as THREE from 'three';
// OrbitControls implementation
class OrbitControls {
constructor(camera, domElement) {
this.camera = camera;
this.domElement = domElement;
this.target = new THREE.Vector3();
this.enableDamping = true;
this.dampingFactor = 0.05;
this.rotateSpeed = 0.5;
this.zoomSpeed = 1.0;
this.minDistance = 1;
this.maxDistance = 100;
this.spherical = new THREE.Spherical();
this.sphericalDelta = new THREE.Spherical();
this.scale = 1;
this.rotateStart = new THREE.Vector2();
this.rotateEnd = new THREE.Vector2();
this.rotateDelta = new THREE.Vector2();
this.state = -1;
this.STATE = { NONE: -1, ROTATE: 0, ZOOM: 1 };
this.offset = new THREE.Vector3();
const position = this.camera.position;
this.offset.copy(position).sub(this.target);
this.spherical.setFromVector3(this.offset);
this.bindEvents();
}
bindEvents() {
this.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
this.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
this.domElement.addEventListener('wheel', this.onWheel.bind(this));
this.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
}
onMouseDown(event) {
event.preventDefault();
if (event.button === 0) {
this.state = this.STATE.ROTATE;
this.rotateStart.set(event.clientX, event.clientY);
}
}
onMouseMove(event) {
if (this.state === this.STATE.ROTATE) {
this.rotateEnd.set(event.clientX, event.clientY);
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart).multiplyScalar(this.rotateSpeed);
this.sphericalDelta.theta -= 2 * Math.PI * this.rotateDelta.x / this.domElement.clientHeight;
this.sphericalDelta.phi -= 2 * Math.PI * this.rotateDelta.y / this.domElement.clientHeight;
this.rotateStart.copy(this.rotateEnd);
}
}
onMouseUp() {
this.state = this.STATE.NONE;
}
onWheel(event) {
event.preventDefault();
if (event.deltaY < 0) {
this.scale /= Math.pow(0.95, this.zoomSpeed);
} else {
this.scale *= Math.pow(0.95, this.zoomSpeed);
}
}
update() {
this.offset.copy(this.camera.position).sub(this.target);
this.spherical.setFromVector3(this.offset);
if (this.enableDamping) {
this.spherical.theta += this.sphericalDelta.theta * this.dampingFactor;
this.spherical.phi += this.sphericalDelta.phi * this.dampingFactor;
} else {
this.spherical.theta += this.sphericalDelta.theta;
this.spherical.phi += this.sphericalDelta.phi;
}
this.spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, this.spherical.phi));
this.spherical.radius *= this.scale;
this.spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, this.spherical.radius));
this.offset.setFromSpherical(this.spherical);
this.camera.position.copy(this.target).add(this.offset);
this.camera.lookAt(this.target);
if (this.enableDamping) {
this.sphericalDelta.theta *= (1 - this.dampingFactor);
this.sphericalDelta.phi *= (1 - this.dampingFactor);
} else {
this.sphericalDelta.set(0, 0, 0);
}
this.scale = 1;
}
dispose() {
this.domElement.removeEventListener('mousedown', this.onMouseDown);
this.domElement.removeEventListener('mousemove', this.onMouseMove);
this.domElement.removeEventListener('mouseup', this.onMouseUp);
this.domElement.removeEventListener('wheel', this.onWheel);
}
}
export default function ThreeJSGenerator() {
const [prompt, setPrompt] = useState('');
const [image, setImage] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
const [iterations, setIterations] = useState(3);
const [currentIteration, setCurrentIteration] = useState(0);
const [isGenerating, setIsGenerating] = useState(false);
const [status, setStatus] = useState('');
const [generatedCode, setGeneratedCode] = useState('');
const [feedback, setFeedback] = useState([]);
const [error, setError] = useState('');
const [comparisonView, setComparisonView] = useState(null);
const canvasRef = useRef(null);
const sceneRef = useRef(null);
const cameraRef = useRef(null);
const rendererRef = useRef(null);
const controlsRef = useRef(null);
const animationRef = useRef(null);
const objectRef = useRef(null);
// Initialize Three.js scene
const initScene = useCallback(() => {
if (!canvasRef.current) return;
// Clean up existing
if (rendererRef.current) {
rendererRef.current.dispose();
}
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
const width = canvasRef.current.clientWidth;
const height = canvasRef.current.clientHeight;
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
sceneRef.current = scene;
// Camera
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(5, 5, 5);
cameraRef.current = camera;
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setSize(width, height);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
canvasRef.current.innerHTML = '';
canvasRef.current.appendChild(renderer.domElement);
rendererRef.current = renderer;
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controlsRef.current = controls;
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 10);
directionalLight.castShadow = true;
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0x4fc3f7, 0.5);
pointLight.position.set(-5, 5, -5);
scene.add(pointLight);
// Grid helper
const gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
scene.add(gridHelper);
// Animation loop
const animate = () => {
animationRef.current = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
}, []);
useEffect(() => {
initScene();
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
if (rendererRef.current) {
rendererRef.current.dispose();
}
};
}, [initScene]);
// Execute generated Three.js code
const executeCode = useCallback((code) => {
if (!sceneRef.current) return;
// Remove old object
if (objectRef.current) {
sceneRef.current.remove(objectRef.current);
objectRef.current = null;
}
try {
// Create a function that has access to THREE
const createObject = new Function('THREE', 'scene', `
try {
${code}
return object;
} catch (e) {
console.error('Code execution error:', e);
return null;
}
`);
const object = createObject(THREE, sceneRef.current);
if (object) {
objectRef.current = object;
sceneRef.current.add(object);
}
} catch (e) {
console.error('Failed to execute code:', e);
setError(`Code execution error: ${e.message}`);
}
}, []);
// Capture screenshot at specific rotation
const captureScreenshot = useCallback((rotationY = 0) => {
return new Promise((resolve) => {
if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !objectRef.current) {
resolve(null);
return;
}
// Save original rotation
const originalRotation = objectRef.current.rotation.y;
// Set rotation
objectRef.current.rotation.y = rotationY;
// Render
rendererRef.current.render(sceneRef.current, cameraRef.current);
// Capture
const dataUrl = rendererRef.current.domElement.toDataURL('image/png');
// Restore rotation
objectRef.current.rotation.y = originalRotation;
resolve(dataUrl);
});
}, []);
// Capture multiple screenshots
const captureMultipleScreenshots = useCallback(async () => {
const screenshots = [];
const rotations = [0, Math.PI / 2, Math.PI, Math.PI * 1.5];
for (const rotation of rotations) {
const screenshot = await captureScreenshot(rotation);
if (screenshot) {
screenshots.push(screenshot);
}
await new Promise(r => setTimeout(r, 100));
}
return screenshots;
}, [captureScreenshot]);
// Call Claude API
const callClaude = async (messages) => {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: messages,
}),
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
return data.content.map(c => c.text || '').join('');
};
// Generate initial Three.js code
const generateInitialCode = async () => {
const content = [];
if (image) {
content.push({
type: 'text',
text: 'Here is the reference image I want you to recreate as a 3D model:',
});
content.push({
type: 'image',
source: {
type: 'base64',
media_type: image.type,
data: image.data,
},
});
}
content.push({
type: 'text',
text: `Generate Three.js code to create a 3D representation of: "${prompt}"
${image ? `IMPORTANT: Study the reference image carefully. Pay close attention to:
- The overall shape and silhouette
- Proportions and relative sizes of parts
- Colors and materials
- Key distinguishing features
- Pose/orientation of the object
Your goal is to create a 3D model that closely matches this reference image.` : ''}
IMPORTANT: Return ONLY the JavaScript code, no markdown, no explanation.
The code should:
1. Create a variable called 'object' that is a THREE.Group or THREE.Mesh
2. Use only built-in THREE.js geometries and materials
3. Match the reference image as closely as possible (colors, proportions, features)
4. Include any necessary child meshes in the group
5. Center the object at origin
6. Make the object a reasonable size (fits in a 5x5x5 box)
Example format:
const object = new THREE.Group();
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
object.add(mesh);
Now generate the code for: "${prompt}"`,
});
return callClaude([{ role: 'user', content }]);
};
// Get feedback by comparing to reference image
const getFeedbackWithComparison = async (screenshots, currentCode, iterationNum) => {
const content = [];
// First show the reference image if available
if (image) {
content.push({
type: 'text',
text: `## REFERENCE IMAGE (TARGET)
This is the original reference image that we're trying to recreate as a 3D model:`,
});
content.push({
type: 'image',
source: {
type: 'base64',
media_type: image.type,
data: image.data,
},
});
}
// Then show current renders
content.push({
type: 'text',
text: `## CURRENT 3D RENDER (Iteration ${iterationNum})
Here are screenshots of the current 3D model from 4 angles (front, right, back, left):`,
});
for (let i = 0; i < screenshots.length; i++) {
const base64Data = screenshots[i].split(',')[1];
content.push({
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: base64Data,
},
});
}
// Comparison prompt
content.push({
type: 'text',
text: `## TASK: Compare and Improve
Object description: "${prompt}"
Current Three.js code:
\`\`\`javascript
${currentCode}
\`\`\`
${image ? `**CRITICAL: Compare the current 3D render directly to the reference image above.**
Analyze the following aspects and identify SPECIFIC differences:
1. **Shape Accuracy**: Does the overall silhouette match? Are there missing or incorrect geometric shapes?
2. **Proportions**: Are the relative sizes of different parts correct compared to the reference?
3. **Colors**: Do the colors match the reference image? List specific color corrections needed.
4. **Details**: What features from the reference are missing or need refinement?
5. **Pose/Orientation**: Is the object oriented correctly compared to the reference?
For each difference you identify, explain HOW to fix it in the code.` : `Analyze the current render and suggest improvements to make it look more like "${prompt}".`}
Then provide IMPROVED Three.js code that addresses these specific differences.
The code must create a variable called 'object' that is a THREE.Group or THREE.Mesh.
Return the improved code in a \`\`\`javascript code block.`,
});
const response = await callClaude([{ role: 'user', content }]);
// Update comparison view for UI
if (screenshots.length > 0) {
setComparisonView({
reference: imagePreview,
current: screenshots[0],
iteration: iterationNum
});
}
return response;
};
// Extract code from response
const extractCode = (response) => {
// Try to find code block
const codeBlockMatch = response.match(/```(?:javascript|js)?\s*([\s\S]*?)```/);
if (codeBlockMatch) {
return codeBlockMatch[1].trim();
}
// If no code block, try to find code pattern
const objectMatch = response.match(/(const\s+object\s*=[\s\S]*?)(?=\n\n|$)/);
if (objectMatch) {
return objectMatch[1].trim();
}
// Return full response if it looks like code
if (response.includes('THREE.') && response.includes('object')) {
return response.trim();
}
return null;
};
// Extract comparison analysis from response
const extractAnalysis = (response) => {
// Get text before code block
const parts = response.split('```');
if (parts.length > 0) {
return parts[0].trim();
}
return response.slice(0, 800);
};
// Main generation loop
const handleGenerate = async () => {
if (!prompt.trim()) {
setError('Please enter a prompt');
return;
}
setIsGenerating(true);
setError('');
setFeedback([]);
setCurrentIteration(0);
setComparisonView(null);
try {
// Generate initial code
setStatus('Analyzing reference and generating initial 3D model...');
const initialResponse = await generateInitialCode();
let currentCode = extractCode(initialResponse) || initialResponse;
setGeneratedCode(currentCode);
executeCode(currentCode);
// Wait for render
await new Promise(r => setTimeout(r, 500));
// Iteration loop
for (let i = 0; i < iterations; i++) {
setCurrentIteration(i + 1);
setStatus(`Iteration ${i + 1}/${iterations}: Capturing screenshots...`);
// Capture screenshots
const screenshots = await captureMultipleScreenshots();
if (screenshots.length === 0) {
setFeedback(prev => [...prev, {
iteration: i + 1,
text: 'Failed to capture screenshots',
differences: []
}]);
continue;
}
setStatus(`Iteration ${i + 1}/${iterations}: Comparing to reference image...`);
// Get feedback with comparison
const feedbackResponse = await getFeedbackWithComparison(screenshots, currentCode, i + 1);
// Extract analysis
const analysisText = extractAnalysis(feedbackResponse);
// Parse out specific differences if possible
const differences = [];
const diffPatterns = [
/shape accuracy[:\s]*(.*?)(?=\n\d|\n\*\*|$)/is,
/proportions[:\s]*(.*?)(?=\n\d|\n\*\*|$)/is,
/colors?[:\s]*(.*?)(?=\n\d|\n\*\*|$)/is,
/details?[:\s]*(.*?)(?=\n\d|\n\*\*|$)/is,
];
diffPatterns.forEach(pattern => {
const match = analysisText.match(pattern);
if (match && match[1]) {
differences.push(match[1].trim().slice(0, 200));
}
});
setFeedback(prev => [...prev, {
iteration: i + 1,
text: analysisText.slice(0, 600),
differences: differences
}]);
// Extract and apply new code
const newCode = extractCode(feedbackResponse);
if (newCode && newCode !== currentCode) {
currentCode = newCode;
setGeneratedCode(currentCode);
setStatus(`Iteration ${i + 1}/${iterations}: Applying improvements...`);
executeCode(currentCode);
// Wait for render
await new Promise(r => setTimeout(r, 500));
}
}
setStatus('Generation complete!');
// Final comparison
if (image) {
const finalScreenshots = await captureMultipleScreenshots();
if (finalScreenshots.length > 0) {
setComparisonView({
reference: imagePreview,
current: finalScreenshots[0],
iteration: 'Final'
});
}
}
} catch (e) {
console.error('Generation error:', e);
setError(`Generation failed: ${e.message}`);
setStatus('');
} finally {
setIsGenerating(false);
}
};
// Handle image upload
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const base64 = event.target.result;
setImagePreview(base64);
setImage({
type: file.type,
data: base64.split(',')[1],
});
};
reader.readAsDataURL(file);
}
};
return (
<div className="min-h-screen bg-gray-900 text-white p-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-2 text-center bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Three.js AI Generator
</h1>
<p className="text-center text-gray-400 mb-6 text-sm">
Upload a reference image and watch Claude iteratively refine the 3D model to match it
</p>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Left Panel - Controls */}
<div className="space-y-4">
{/* Prompt Input */}
<div className="bg-gray-800 rounded-lg p-4">
<label className="block text-sm font-medium mb-2">Describe the object</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., A cute low-poly fox sitting down"
className="w-full bg-gray-700 rounded-lg p-3 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:outline-none"
rows={3}
/>
</div>
{/* Image Upload */}
<div className="bg-gray-800 rounded-lg p-4">
<label className="block text-sm font-medium mb-2">
Reference Image <span className="text-blue-400">(recommended)</span>
</label>
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
className="w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-blue-600 file:text-white hover:file:bg-blue-700"
/>
{imagePreview && (
<div className="mt-3">
<p className="text-xs text-gray-400 mb-1">Reference:</p>
<img src={imagePreview} alt="Reference" className="max-h-32 rounded-lg border border-gray-600" />
</div>
)}
</div>
{/* Iterations */}
<div className="bg-gray-800 rounded-lg p-4">
<label className="block text-sm font-medium mb-2">
Refinement Iterations: {iterations}
</label>
<input
type="range"
min="1"
max="5"
value={iterations}
onChange={(e) => setIterations(parseInt(e.target.value))}
className="w-full accent-blue-500"
/>
<p className="text-xs text-gray-400 mt-1">
Each iteration compares to reference and refines the model
</p>
</div>
{/* Generate Button */}
<button
onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()}
className={`w-full py-3 rounded-lg font-semibold transition-all ${
isGenerating || !prompt.trim()
? 'bg-gray-600 cursor-not-allowed'
: 'bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700'
}`}
>
{isGenerating ? 'Generating...' : 'Generate 3D Model'}
</button>
{/* Status */}
{status && (
<div className="bg-blue-900/50 rounded-lg p-4 border border-blue-700">
<div className="flex items-center gap-3">
<div className="animate-spin w-5 h-5 border-2 border-blue-400 border-t-transparent rounded-full" />
<span className="text-sm">{status}</span>
</div>
{currentIteration > 0 && (
<div className="mt-2">
<div className="bg-gray-700 rounded-full h-2">
<div
className="bg-gradient-to-r from-blue-500 to-purple-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${(currentIteration / iterations) * 100}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">Iteration {currentIteration} of {iterations}</p>
</div>
)}
</div>
)}
{/* Error */}
{error && (
<div className="bg-red-900/50 rounded-lg p-4 border border-red-700 text-red-200 text-sm">
{error}
</div>
)}
</div>
{/* Middle Panel - 3D Viewer & Comparison */}
<div className="space-y-4">
{/* 3D Canvas */}
<div className="bg-gray-800 rounded-lg overflow-hidden">
<div className="bg-gray-700 px-4 py-2 text-sm font-medium">
3D Preview (drag to rotate, scroll to zoom)
</div>
<div
ref={canvasRef}
className="w-full aspect-square"
style={{ minHeight: '350px' }}
/>
</div>
{/* Side-by-side Comparison */}
{comparisonView && comparisonView.reference && (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-sm font-medium mb-3 text-purple-400">
Comparison - Iteration {comparisonView.iteration}
</h3>
<div className="grid grid-cols-2 gap-2">
<div>
<p className="text-xs text-gray-400 mb-1">Reference</p>
<img
src={comparisonView.reference}
alt="Reference"
className="w-full rounded border border-gray-600"
/>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Current Render</p>
<img
src={comparisonView.current}
alt="Current"
className="w-full rounded border border-gray-600"
/>
</div>
</div>
</div>
)}
</div>
{/* Right Panel - Feedback & Code */}
<div className="space-y-4">
{/* Feedback History */}
{feedback.length > 0 && (
<div className="bg-gray-800 rounded-lg p-4 max-h-80 overflow-y-auto">
<h3 className="font-semibold mb-3 text-purple-400">Comparison Feedback</h3>
{feedback.map((f, i) => (
<div key={i} className="mb-4 pb-3 border-b border-gray-700 last:border-0">
<div className="flex items-center gap-2 mb-2">
<span className="bg-blue-600 text-xs px-2 py-0.5 rounded">
Iteration {f.iteration}
</span>
</div>
{f.differences && f.differences.length > 0 && (
<div className="mb-2">
<p className="text-xs text-yellow-400 mb-1">Key Differences Found:</p>
<ul className="text-xs text-gray-300 space-y-1">
{f.differences.map((d, j) => (
<li key={j} className="flex items-start gap-1">
<span className="text-yellow-500">•</span>
<span>{d}</span>
</li>
))}
</ul>
</div>
)}
<div className="text-xs text-gray-400 whitespace-pre-wrap">
{f.text.slice(0, 400)}...
</div>
</div>
))}
</div>
)}
{/* Generated Code */}
{generatedCode && (
<div className="bg-gray-800 rounded-lg overflow-hidden">
<div className="bg-gray-700 px-4 py-2 text-sm font-medium flex justify-between items-center">
<span>Generated Code</span>
<button
onClick={() => navigator.clipboard.writeText(generatedCode)}
className="text-xs bg-gray-600 px-2 py-1 rounded hover:bg-gray-500"
>
Copy
</button>
</div>
<pre className="p-4 text-xs text-green-400 overflow-x-auto max-h-48 overflow-y-auto">
{generatedCode}
</pre>
</div>
)}
</div>
</div>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment