如果你还在用传统方式写 3D 应用,一行行敲 Three.js 代码,那么 Codex + tldraw 的组合可能会让你重新思考开发流程。最近,一个简单的技术演示在开发者社区引发关注:在 tldraw 画布上画个草图,用自然语言描述需求,Codex 就能生成完整的 3D 地球应用。这不仅仅是"又一个 AI 代码生成demo",而是展示了 AI 如何改变前端 3D 开发的交互范式。
传统 3D 开发需要掌握复杂的图形学概念和 API,而 Codex 理解自然语言指令后,直接生成可运行的代码。更关键的是,tldraw 作为交互媒介,让草图成为 AI 理解需求的视觉上下文。这种"草图+语言"的混合输入方式,比纯文本提示词更精准,特别适合空间和视觉类应用的快速原型开发。
本文将带你完整实现这个流程:从环境搭建到草图绘制,从自然语言指令到 3D 地球应用生成,最后深入分析这种开发模式的实际价值和使用边界。无论你是前端开发者想了解 AI 辅助编程,还是对 3D 可视化感兴趣,都能获得可直接复用的实践方案。
1. 为什么 Codex + tldraw 值得关注:重新定义 3D 应用原型开发
在讨论具体实现前,我们需要理解这个组合解决的真正痛点。3D 应用开发历来门槛较高,开发者需要同时掌握三维数学、图形 API、着色器编程等多领域知识。即使使用 Three.js 这类封装较好的库,创建一个简单的 3D 场景仍然需要大量样板代码。
Codex 的价值在于它理解编程上下文的能力。当你说"创建一个旋转的地球,有云层效果和星空背景",它能生成结构完整的 Three.js 代码,包括场景初始化、相机设置、光照配置、纹理加载和动画循环。这不仅仅是代码补全,而是从需求到实现的跨越。
tldraw 的作用则提供了视觉上下文。单纯靠文字描述 3D 场景容易产生歧义——"地球稍微倾斜"到底倾斜多少度?在画布上简单画个倾斜的椭圆,AI 就能理解你的视觉意图。这种多模态输入大大降低了沟通成本。
实际开发中,这种模式最适合两类场景:
- 快速原型验证:产品经理或设计师用草图表达想法,开发者用自然语言补充细节,立即看到可交互的 3D 原型
- 教育演示制作:教师需要创建物理现象、地理概念的 3D 演示,无需编码背景也能生成专业可视化
但需要注意,当前技术更适合原型阶段。复杂的光照模型、性能优化、跨浏览器兼容等工程问题,仍需要开发者介入处理。
2. 核心工具链解析:Codex、tldraw 与 Three.js 的协同原理
2.1 Codex:不只是代码生成器
Codex 基于 GPT 模型训练,专门针对编程任务优化。与通用聊天机器人不同,Codex 理解编程语言的语法结构、API 使用模式和项目上下文。当处理 3D 开发任务时,它能准确选择 Three.js 的合适组件,比如知道地球模型应该用SphereGeometry而不是BoxGeometry。
更重要的是,Codex 具备一定的推理能力。当你说"添加围绕地球旋转的月亮",它能理解需要创建另一个球体,设置合适的轨道参数和父子关系。这种逻辑推理超越了简单的模板填充。
2.2 tldraw:从草图到结构化数据
tldraw 的核心价值在于将自由形式的草图转化为机器可理解的结构化数据。当你画一个地球草图时,tldraw 不仅记录像素点,还识别出基本几何属性:位置、大小、旋转角度、颜色填充等。
这些数据通过 JSON 格式传递给 Codex,成为生成代码的视觉约束。比如草图地球的倾斜角度直接转化为 Three.js 中模型的rotation.x值,实现了"所见即所得"的代码生成。
2.3 Three.js:3D 渲染的基石
Three.js 提供了完整的 WebGL 封装,让浏览器中的 3D 渲染变得可行。Codex 生成的代码通常包含以下核心组件:
- 场景图管理:物体间的层级关系和变换
- 材质系统:定义物体外观,包括颜色、纹理、反光特性
- 光照模型:环境光、方向光、点光源的配置
- 动画循环:使用
requestAnimationFrame实现平滑动画
理解这三个工具的分工协作,是后续实现的基础。
3. 环境准备与工具配置
3.1 开发环境要求
确保你的系统满足以下条件:
- Node.js 16.0 或更高版本
- npm 或 yarn 包管理器
- 现代浏览器(Chrome 90+、Firefox 88+、Safari 14+)
- 稳定的网络连接(用于调用 Codex API)
3.2 项目初始化
创建新项目并安装依赖:
# 创建项目目录 mkdir codex-tldraw-3d cd codex-tldraw-3d # 初始化 package.json npm init -y # 安装核心依赖 npm install tldraw@2.0.0 three@0.150.0 npm install --save-dev vite@4.0.0 @types/three@0.150.03.3 Codex API 配置
要使用 Codex,你需要获取 OpenAI API 密钥:
- 访问 OpenAI 平台(https://platform.openai.com)
- 注册账号并完成验证
- 在 API Keys 页面生成新密钥
- 在项目中创建配置文件:
// config.js export const OPENAI_API_KEY = '你的API密钥'; export const OPENAI_API_URL = 'https://api.openai.com/v1/completions';重要安全提醒:永远不要将 API 密钥提交到版本控制系统。使用环境变量或配置文件,并在.gitignore中排除敏感信息。
3.4 项目结构规划
建立清晰的项目结构有助于后续开发:
codex-tldraw-3d/ ├── index.html # 主页面 ├── src/ │ ├── main.js # 应用入口 │ ├── tldraw-wrapper.js # tldraw 封装 │ ├── codex-client.js # Codex API 客户端 │ ├── three-renderer.js # Three.js 渲染器 │ └── styles.css # 样式文件 ├── config.js # 配置文件 └── package.json4. 核心实现流程拆解
4.1 tldraw 画布集成
首先创建基础的 tldraw 画布界面:
<!-- index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Codex + tldraw 3D 应用生成器</title> <link rel="stylesheet" href="./src/styles.css"> </head> <body> <div class="container"> <div class="control-panel"> <h1>3D 地球应用生成器</h1> <div class="input-group"> <textarea id="promptInput" placeholder="描述你想要的 3D 地球效果..."></textarea> <button id="generateBtn">生成 3D 应用</button> </div> </div> <div class="canvas-container"> <div id="tldraw-canvas"></div> </div> <div class="preview-container"> <div id="three-preview"></div> </div> </div> <script type="module" src="./src/main.js"></script> </body> </html>// src/tldraw-wrapper.js import { createTLDraw } from 'tldraw'; export class TLDrawWrapper { constructor(containerId) { this.container = document.getElementById(containerId); this.app = null; this.init(); } init() { this.app = createTLDraw({ container: this.container, options: { tools: ['select', 'draw', 'erase', 'rectangle', 'ellipse'], grid: true, snapToGrid: true } }); } // 获取画布数据作为视觉上下文 getCanvasData() { if (!this.app) return null; const shapes = this.app.getShapes(); const viewport = this.app.getViewport(); return { shapes: shapes.map(shape => ({ type: shape.type, rotation: shape.rotation, point: shape.point, size: shape.size, style: shape.style })), viewport: { zoom: viewport.zoom, point: viewport.point } }; } // 清空画布 clearCanvas() { if (this.app) { this.app.clear(); } } }4.2 Codex 客户端实现
创建与 OpenAI API 交互的客户端:
// src/codex-client.js import { OPENAI_API_KEY, OPENAI_API_URL } from '../config.js'; export class CodexClient { constructor() { this.apiKey = OPENAI_API_KEY; this.baseURL = OPENAI_API_URL; } async generateThreeJSCode(prompt, canvasData = null) { // 构建完整的提示词,包含画布上下文 const fullPrompt = this.buildPrompt(prompt, canvasData); const response = await fetch(this.baseURL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, body: JSON.stringify({ model: 'code-davinci-002', prompt: fullPrompt, max_tokens: 1500, temperature: 0.7, stop: ['// 结束', '/* 结束 */'] }) }); if (!response.ok) { throw new Error(`API 请求失败: ${response.status}`); } const data = await response.json(); return data.choices[0].text.trim(); } buildPrompt(userPrompt, canvasData) { let prompt = `生成一个 Three.js 3D 地球应用,要求:${userPrompt}\n\n`; if (canvasData && canvasData.shapes.length > 0) { prompt += "画布上下文信息:\n"; canvasData.shapes.forEach((shape, index) => { prompt += `形状 ${index + 1}: 类型=${shape.type}, 旋转=${shape.rotation}, 位置=${JSON.stringify(shape.point)}\n`; }); } prompt += "\n生成完整的 HTML 文件,包含以下部分:\n"; prompt += "1. Three.js 场景初始化\n"; prompt += "2. 相机和渲染器设置\n"; prompt += "3. 地球几何体和材质\n"; prompt += "4. 光照设置\n"; prompt += "5. 动画循环\n"; prompt += "6. 响应式布局\n\n"; prompt += "代码:\n`; return prompt; } }4.3 Three.js 渲染器封装
创建用于预览生成结果的渲染器:
// src/three-renderer.js import * as THREE from 'three'; export class ThreeRenderer { constructor(containerId) { this.container = document.getElementById(containerId); this.scene = null; this.camera = null; this.renderer = null; this.earth = null; this.animationId = null; this.init(); } init() { // 创建场景 this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x000033); // 创建相机 this.camera = new THREE.PerspectiveCamera( 75, this.container.clientWidth / this.container.clientHeight, 0.1, 1000 ); this.camera.position.z = 5; // 创建渲染器 this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize( this.container.clientWidth, this.container.clientHeight ); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.container.appendChild(this.renderer.domElement); // 添加基础光照 this.setupBasicLighting(); // 处理窗口大小变化 window.addEventListener('resize', () => this.onWindowResize()); } setupBasicLighting() { // 环境光 const ambientLight = new THREE.AmbientLight(0x404040, 0.6); this.scene.add(ambientLight); // 方向光 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(5, 3, 5); directionalLight.castShadow = true; this.scene.add(directionalLight); } // 执行生成的代码 executeGeneratedCode(code) { try { // 清空现有场景 this.clearScene(); // 创建执行环境 const context = { THREE: THREE, scene: this.scene, camera: this.camera, renderer: this.renderer, container: this.container, earth: null }; // 包装代码为函数 const wrappedCode = ` (function(THREE, scene, camera, renderer, container) { ${code} }) `; const codeFunction = eval(wrappedCode); codeFunction( context.THREE, context.scene, context.camera, context.renderer, context.container ); // 启动动画循环 this.startAnimation(); } catch (error) { console.error('代码执行错误:', error); throw new Error(`生成的代码存在错误: ${error.message}`); } } clearScene() { // 停止当前动画 if (this.animationId) { cancelAnimationFrame(this.animationId); } // 移除所有物体 while (this.scene.children.length > 0) { const child = this.scene.children[0]; if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(material => material.dispose()); } else { child.material.dispose(); } } this.scene.remove(child); } // 重新添加基础光照 this.setupBasicLighting(); } startAnimation() { const animate = () => { this.animationId = requestAnimationFrame(animate); this.renderer.render(this.scene, this.camera); }; animate(); } onWindowResize() { this.camera.aspect = this.container.clientWidth / this.container.clientHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize( this.container.clientWidth, this.container.clientHeight ); } // 销毁资源 dispose() { this.clearScene(); if (this.renderer) { this.renderer.dispose(); } } }4.4 主应用集成
将各个模块整合到主应用中:
// src/main.js import { TLDrawWrapper } from './tldraw-wrapper.js'; import { CodexClient } from './codex-client.js'; import { ThreeRenderer } from './three-renderer.js'; class App { constructor() { this.tldraw = null; this.codexClient = null; this.threeRenderer = null; this.isGenerating = false; this.init(); } init() { // 初始化组件 this.tldraw = new TLDrawWrapper('tldraw-canvas'); this.codexClient = new CodexClient(); this.threeRenderer = new ThreeRenderer('three-preview'); // 绑定事件 this.bindEvents(); console.log('应用初始化完成'); } bindEvents() { const generateBtn = document.getElementById('generateBtn'); const promptInput = document.getElementById('promptInput'); generateBtn.addEventListener('click', () => this.handleGenerate()); promptInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && e.ctrlKey) { this.handleGenerate(); } }); } async handleGenerate() { if (this.isGenerating) return; const promptInput = document.getElementById('promptInput'); const prompt = promptInput.value.trim(); if (!prompt) { alert('请输入描述信息'); return; } this.isGenerating = true; this.updateUIState('generating'); try { // 获取画布数据 const canvasData = this.tldraw.getCanvasData(); // 调用 Codex 生成代码 const generatedCode = await this.codexClient.generateThreeJSCode(prompt, canvasData); // 执行生成的代码 await this.threeRenderer.executeGeneratedCode(generatedCode); this.updateUIState('success'); } catch (error) { console.error('生成失败:', error); this.updateUIState('error', error.message); } finally { this.isGenerating = false; } } updateUIState(state, errorMessage = '') { const generateBtn = document.getElementById('generateBtn'); switch (state) { case 'generating': generateBtn.textContent = '生成中...'; generateBtn.disabled = true; break; case 'success': generateBtn.textContent = '生成成功!'; generateBtn.disabled = false; setTimeout(() => { generateBtn.textContent = '生成 3D 应用'; }, 2000); break; case 'error': generateBtn.textContent = '生成失败'; generateBtn.disabled = false; alert(`生成错误: ${errorMessage}`); setTimeout(() => { generateBtn.textContent = '生成 3D 应用'; }, 2000); break; } } } // 启动应用 new App();5. 完整示例:从草图到 3D 地球应用
5.1 基础地球生成示例
让我们测试一个完整的流程。首先在 tldraw 画布上画一个简单的圆形代表地球,然后在文本框中输入:
创建一个美丽的 3D 地球,要有云层效果,在太空中缓慢旋转,添加星空背景Codex 可能会生成类似以下的代码:
// 生成的 Three.js 代码示例 const scene = new THREE.Scene(); // 创建星空背景 const starGeometry = new THREE.BufferGeometry(); const starPositions = []; for (let i = 0; i < 10000; i++) { const x = (Math.random() - 0.5) * 2000; const y = (Math.random() - 0.5) * 2000; const z = (Math.random() - 0.5) * 2000; starPositions.push(x, y, z); } starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3)); const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1 }); const stars = new THREE.Points(starGeometry, starMaterial); scene.add(stars); // 创建地球 const earthGeometry = new THREE.SphereGeometry(2, 32, 32); const earthTexture = new THREE.TextureLoader().load('https://example.com/earth.jpg'); const earthMaterial = new THREE.MeshPhongMaterial({ map: earthTexture }); const earth = new THREE.Mesh(earthGeometry, earthMaterial); scene.add(earth); // 创建云层 const cloudGeometry = new THREE.SphereGeometry(2.05, 32, 32); const cloudTexture = new THREE.TextureLoader().load('https://example.com/clouds.jpg'); const cloudMaterial = new THREE.MeshPhongMaterial({ map: cloudTexture, transparent: true, opacity: 0.4 }); const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial); scene.add(clouds); // 动画循环 function animate() { requestAnimationFrame(animate); earth.rotation.y += 0.001; clouds.rotation.y += 0.0015; renderer.render(scene, camera); } animate();5.2 高级特性:交互式地球
尝试更复杂的提示词:
创建一个交互式地球,可以用鼠标拖动旋转,滚轮缩放,点击国家显示名称。添加昼夜交替效果和天气动画。生成的代码会增加交互控制和高级特效:
// 交互控制部分 const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; // 昼夜交替效果 const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); sunLight.position.set(5, 3, 5); scene.add(sunLight); let timeOfDay = 0; function updateDayNightCycle() { timeOfDay += 0.001; const sunX = Math.cos(timeOfDay) * 10; const sunY = Math.sin(timeOfDay) * 5; const sunZ = Math.sin(timeOfDay) * 10; sunLight.position.set(sunX, sunY, sunZ); // 根据太阳位置调整环境光强度 const ambientIntensity = Math.max(0.1, Math.abs(Math.sin(timeOfDay)) * 0.4); scene.background = new THREE.Color(ambientIntensity * 0.1, ambientIntensity * 0.1, ambientIntensity * 0.3); }6. 运行验证与效果测试
6.1 启动开发服务器
使用 Vite 启动开发服务器:
npm run dev访问 http://localhost:3000 查看应用界面。
6.2 功能测试流程
按照以下步骤验证完整功能:
- 画布绘制测试:在 tldraw 画布上绘制各种形状,验证数据获取是否正确
- 基础生成测试:输入简单提示词(如"创建地球"),检查是否能生成基本 3D 场景
- 复杂需求测试:输入详细描述,验证生成代码的完整性和复杂性
- 交互功能测试:测试鼠标控制、动画效果等交互特性
- 性能测试:检查帧率是否稳定,内存使用是否合理
6.3 预期输出验证
成功运行时应该看到:
- tldraw 画布响应绘制操作
- 生成按钮点击后显示"生成中"状态
- 3D 预览区域显示旋转的地球模型
- 控制台无错误日志
- 页面帧率保持在 60fps 左右
7. 常见问题与排查指南
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 画布无法绘制 | tldraw 初始化失败 | 检查浏览器控制台错误 | 确认 tldraw 版本兼容性,检查容器元素是否存在 |
| API 调用失败 | 网络问题或密钥错误 | 查看网络请求状态 | 验证 API 密钥,检查网络连接,确认额度充足 |
| 3D 场景黑屏 | Three.js 初始化问题 | 检查 WebGL 支持 | 更新显卡驱动,尝试不同浏览器 |
| 生成代码语法错误 | Codex 输出格式问题 | 查看生成代码内容 | 调整提示词明确性,添加代码格式要求 |
| 动画卡顿 | 性能问题 | 监控帧率和内存使用 | 优化几何体复杂度,减少实时阴影计算 |
| 纹理加载失败 | 图片路径错误 | 检查网络请求 | 使用本地纹理或可靠 CDN 资源 |
7.1 详细排查步骤
问题:生成的代码无法执行
排查流程:
- 打开浏览器开发者工具(F12)
- 查看 Console 标签页的错误信息
- 检查错误发生的具体行数
- 查看生成的代码是否存在语法错误
- 验证 Three.js 对象和方法是否存在
问题:画布数据未正确传递
排查流程:
- 在
getCanvasData()方法中添加日志输出 - 确认画布数据格式符合预期
- 检查数据序列化过程是否丢失信息
- 验证提示词构建逻辑是否正确使用画布数据
8. 最佳实践与工程建议
8.1 提示词工程优化
有效的提示词应该包含:
- 明确的目标:具体描述想要的 3D 效果
- 技术约束:指定使用的库和版本
- 性能要求:说明目标平台和性能预期
- 代码风格:要求清晰的注释和结构
好的提示词示例:
使用 Three.js 0.150.0 创建一个性能优化的地球模型。 要求: - 使用 SphereGeometry 和纹理贴图 - 添加平滑的旋转动画 - 支持响应式布局 - 代码要有详细注释 - 避免使用已弃用的 API8.2 性能优化策略
生成的 3D 应用应该考虑性能:
- 几何体优化:使用合适的细分程度,避免过多顶点
- 纹理压缩:使用适当分辨率的纹理,考虑压缩格式
- 动画效率:使用
requestAnimationFrame,避免频繁对象创建 - 内存管理:及时销毁不再使用的几何体和材质
8.3 错误处理与回退机制
生产环境需要考虑健壮性:
// 安全的代码执行封装 async function safeExecuteCode(code, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await executeGeneratedCode(code); } catch (error) { console.warn(`执行失败 (尝试 ${attempt}/${maxRetries}):`, error); if (attempt === maxRetries) { // 最后一次尝试失败,使用回退方案 return await loadFallbackScene(); } // 简单的代码修复尝试 code = attemptCodeFix(code, error); } } }8.4 安全注意事项
- API 密钥保护:永远不要在前端代码中硬编码密钥
- 代码沙箱:考虑使用 Web Workers 或沙箱执行生成代码
- 输入验证:对用户输入进行严格的验证和转义
- 资源限制:限制生成代码的复杂度和执行时间
9. 实际应用场景与扩展方向
9.1 教育领域的应用
这种技术特别适合创建交互式教学材料:
- 地理教育:动态展示地球构造、板块运动
- 天文教学:模拟太阳系运行、星座变化
- 物理实验:可视化力学原理、波动现象
9.2 产品设计与原型开发
设计师可以快速创建 3D 产品原型:
- 建筑可视化:生成建筑模型和环境效果
- 工业设计:创建产品 3D 展示和交互演示
- 游戏原型:快速验证游戏场景和机制
9.3 技术扩展可能性
基于这个基础,可以进一步扩展:
- 多模型支持:集成更多 3D 格式和渲染引擎
- 实时协作:多人同时编辑画布和查看结果
- 模板系统:预定义常用组件和效果模板
- 版本管理:保存和对比不同版本的生成结果
这个项目展示了 AI 辅助编程在 3D 开发领域的巨大潜力。虽然当前技术还有局限,但已经能够显著降低 3D 应用开发的门槛。随着 AI 模型的不断进化,这种"描述即生成"的开发模式可能会成为前端开发的新标准。
建议在实际项目中从小范围开始试用,逐步积累使用经验。重点关注提示词优化、错误处理和性能调优,让 AI 真正成为提升开发效率的助力而非负担。