You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 17:27:03