如何将纯Three.js项目实现为Next.js中的React组件并解决渲染异常
问题修复方案
以下是你代码的核心问题和修复后的完整可运行代码:
核心问题列表
- 缺失Next.js客户端组件标识:Next.js默认启用服务端渲染,涉及浏览器原生API(canvas、window等)的组件必须显式声明为客户端组件
- 模块级全局变量污染:scene、renderer等实例定义在组件外部,多次挂载组件会出现变量覆盖、资源泄漏
- 纹理异步加载未触发重绘:Three.js纹理加载是异步操作,加载完成前已经渲染第一帧,后续无自动更新导致黑屏
- 缺少组件卸载清理逻辑:未销毁渲染实例、取消动画帧请求,会引发内存泄漏和渲染异常
- 未适配高DPI屏幕:未设置渲染器像素比,高分辨率设备下会出现显示异常
修复后完整代码
'use client' // Next.js 13+ App Router必须加,标记为客户端组件,Pages Router可省略 import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import React, { useEffect, useRef } from 'react'; import px from './cubemap/px.jpg'; import nx from './cubemap/nx.jpg'; import py from './cubemap/py.jpg'; import ny from './cubemap/ny.jpg'; import pz from './cubemap/pz.jpg'; import nz from './cubemap/nz.jpg'; const images = [px, nx, py, ny, pz, nz]; // 材质创建函数,加入加载完成回调 function createMaterialArray(onLoadComplete) { const loader = new THREE.TextureLoader(); const loadedCount = { value: 0 }; const materialArray = images.map((image) => { const texture = loader.load(image, () => { loadedCount.value++; // 所有纹理加载完成后触发回调重绘 if (loadedCount.value === 6 && onLoadComplete) onLoadComplete(); }); return new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide, }); }); return materialArray; } export default function Universe() { const containerRef = useRef(null); // 用useRef存储实例,避免变量泄漏 const sceneRef = useRef(null); const cameraRef = useRef(null); const rendererRef = useRef(null); const controlsRef = useRef(null); const animationIdRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // 所有实例移到useEffect内部,避免全局污染 const scene = new THREE.Scene(); sceneRef.current = scene; const camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 45, 30000, ); cameraRef.current = camera; const renderer = new THREE.WebGLRenderer({ antialias: true }); // 适配高DPI屏幕 renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); containerRef.current.appendChild(renderer.domElement); rendererRef.current = renderer; // 创建材质,纹理全部加载完成后自动触发重绘 const materialArray = createMaterialArray(() => { renderer.render(scene, camera); }); const skyboxGeo = new THREE.BoxGeometry(10000, 10000, 10000); const skybox = new THREE.Mesh(skyboxGeo, materialArray); scene.add(skybox); const controls = new OrbitControls(camera, renderer.domElement); controls.enabled = true; controls.minDistance = 100; controls.maxDistance = 1500; controls.autoRotate = true; controls.autoRotateSpeed = 1.0; controlsRef.current = controls; camera.position.set(1200, -250, 200); // 动画循环 function animate() { controls.update(); renderer.render(scene, camera); animationIdRef.current = requestAnimationFrame(animate); } animate(); // 窗口 resize 适配 function handleResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('resize', handleResize); // 卸载清理逻辑,避免内存泄漏 return () => { window.removeEventListener('resize', handleResize); if (animationIdRef.current) cancelAnimationFrame(animationIdRef.current); if (renderer.domElement) containerRef.current?.removeChild(renderer.domElement); renderer.dispose(); controls.dispose(); materialArray.forEach(mat => { mat.map.dispose(); mat.dispose(); }); skyboxGeo.dispose(); }; }, []); return <div ref={containerRef} style={{ width: '100vw', height: '100vh', margin: 0, padding: 0, overflow: 'hidden' }}></div>; }
额外注意事项
- 如果你把立方体贴图放在Next.js的public目录下,引用路径不需要加
./,直接写/cubemap/px.jpg即可 - 可以先替换纯色材质测试基础渲染逻辑是否正常,排除图片路径错误的问题
内容的提问来源于stack exchange,提问作者ambrecht-web
相关产品推荐
相关产品推荐

