如何更新Three.js纹理实现可移动文本标识并支持3D对象直接操作
3D定制工具纹理交互需求
我正在使用Three.js开发一款3D定制工具,目前我借助Fabric.js添加文本内容,并将Fabric.js的画布作为纹理传入Three.js,相关实现代码如下:
var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); camera.position.set(0, 0, 10); var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var light = new THREE.DirectionalLight(0xffffff, 0.5); light.position.setScalar(10); scene.add(light); scene.add(new THREE.AmbientLight(0xffffff, 1)); var canvasTexture = new THREE.CanvasTexture(cnvs); canvasTexture.wrapS = THREE.RepeatWrapping; canvasTexture.wrapT = THREE.RepeatWrapping; canvasTexture.repeat.set(2, 2); var geometry = new THREE.PlaneGeometry(10, 10, 20, 20); geometry.vertices.forEach(v => { v.z = Math.cos(v.x) * Math.sin(-v.y * 0.5) * 0.5; }); geometry.computeFaceNormals(); geometry.computeVertexNormals(); var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ map: canvasTexture, metalness: 0.25, roughness: 0.25 })); scene.add(mesh); var canvas = new fabric.Canvas('cnvs', { backgroundColor: 'white' }); canvas.on("after:render", function() { mesh.material.map.needsUpdate = true; }); var rect = new fabric.Rect({ width: 50, height: 50, left: 50, top: 128, stroke: '#aaf', strokeWidth: 5, fill: '#faa', selectable: false, originX: 'center', originY: 'center' }); canvas.add(rect); function animateRandom() { var randomX = THREE.Math.randInt(50, 206); var randomY = THREE.Math.randInt(50, 206); rect.animate({ left: randomX, top: randomY }, { duration: 1000, onChange: canvas.renderAll.bind(canvas), easing: fabric.util.ease.easeOutExpo }) } var text = new fabric.IText('Three.js\n+\nFaBric.js', { fontSize: 40, textAlign: 'center', fontWeight: 'bold', left: 128, top: 128, angle: 30, originX: 'center', originY: 'center', shadow: 'blue -5px 6px 5px', styles: { 0: { 0: { fontSize: 60, fontFamily: 'Impact', fontWeight: 'normal', fill: 'orange' } }, 1: { 0: { fill: "blue" } }, 2: { 0: { textBackgroundColor: 'red' }, 2: { fill: 'fuchsia', stroke: 'orange', strokeWidth: 1 } } } }); text.setSelectionStyles({ fontStyle: 'italic', fill: '', stroke: 'red', strokeWidth: 2 }, 1, 5); canvas.add(text); canvas.setActiveObject(text); animateRandom(); setInterval(animateRandom, 1000); var clock = new THREE.Clock(); var time = 0; render(); function render() { requestAnimationFrame(render); time += clock.getDelta(); mesh.lookAt(Math.cos(time * 0.314) * 20, Math.sin(time * 0.157) * 10, 40); renderer.render(scene, camera); }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/90/three.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.1/dat.gui.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.2.1/fabric.min.js"></script> <div id="container"> <canvas id="cnvs" height="256" width="256"></canvas> </div>
实现效果如下图所示:
现在我希望隐藏Fabric.js的画布,支持用户直接在3D对象上调整文本、标识的位置,请问该如何实现这一需求?
实现方案
1. 隐藏Fabric画布
直接给Fabric绑定的canvas元素添加CSS样式即可,隐藏不会影响画布在内存中渲染生成纹理:
#cnvs { display: none; }
2. 实现3D坐标到画布坐标的映射
引入Three.js的Raycaster射线拾取能力,将用户在3D渲染画布上的操作坐标转换为Fabric画布的对应坐标:
- 监听鼠标在Three.js渲染画布上的操作事件,将鼠标坐标转换为WebGL裁剪空间坐标
- 用射线检测获取鼠标和3D网格的交点,拿到交点的UV坐标(取值范围0~1)
- 注意Three.js的UV坐标Y轴方向和Fabric画布Y轴方向相反,转换时需要做翻转:
fabricY = (1 - uv.y) * 画布高度
3. 实现拖拽调整逻辑
- 鼠标按下时,通过转换后的坐标查找Fabric中被点击的元素,设为激活状态
- 鼠标拖拽过程中,实时转换当前坐标更新激活元素的位置,触发Fabric重绘
- 你已经配置了
after:render监听自动更新纹理的needsUpdate,修改后纹理会自动同步到3D对象上
核心实现代码如下:
const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let activeObject = null; const renderDom = renderer.domElement; renderDom.addEventListener('mousedown', onMouseDown); renderDom.addEventListener('mousemove', onMouseMove); renderDom.addEventListener('mouseup', onMouseUp); renderDom.addEventListener('mouseleave', onMouseUp); function getFabricCoordFromEvent(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObject(mesh); if (intersects.length) { const uv = intersects[0].uv; return { x: uv.x * canvas.width, y: (1 - uv.y) * canvas.height } } return null; } function onMouseDown(event) { const coord = getFabricCoordFromEvent(event); if (!coord) return; const point = new fabric.Point(coord.x, coord.y); activeObject = canvas.findTarget(point); if (activeObject) canvas.setActiveObject(activeObject); } function onMouseMove(event) { if (!activeObject) return; const coord = getFabricCoordFromEvent(event); if (!coord) return; activeObject.set({ left: coord.x, top: coord.y }); canvas.renderAll(); } function onMouseUp() { activeObject = null; canvas.discardActiveObject(); }
如果需要支持直接修改文本内容,只需要在检测到点击的是IText元素时,弹出输入框获取新内容,同步更新元素的text属性即可。
内容的提问来源于stack exchange,提问作者naoval luthfi
相关产品推荐
相关产品推荐

