教程——用SVGLoader把SVG带到Three.js中来

3,975 阅读7分钟

Three.js是最流行的3D WebGL库,为无数的3D体验提供动力,如登陆页面、VR房间、游戏,甚至是整个3D编辑器如果你对开发例如用于建模或3D打印的3D编辑器或程序性几何生成器感兴趣,你可以考虑把SVG考虑在内。

在本教程中,我将向你展示如何利用SVGLoader将矢量图带入Three.js,以及如何在3D中挤出和预览它们

设置

让我们从基础知识开始。我们将安装所需的依赖项,配置Vite构建工具,并设置Three.js的场景。

安装

首先,用Vite的 "vanilla "模板启动一个新项目,并安装Three.js。

# npm 6.x
npm init @vitejs/app svg-threejs --template vanilla

# npm 7+, extra double-dash is needed:
npm init @vitejs/app svg-threejs -- --template vanilla

cd svg-threejs
npm install three
npm run dev

通过这几行,开发环境就全部设置好了。

HTML和CSS文件

接下来,我们要对默认的HTML和CSS文件做一些修改。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js SVG extruder</title>
  </head>
  <body>
    <div id="app"></div>
    <div class="controls">
      <input type="range" min="1" max="50" id="input" />
    </div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

在HTML中,添加一个type=range 输入字段,以控制SVG的挤压程度。然后,在CSS中,根据你的需要定位和样式。在下面的例子中,我定位了滑块并确定了顶部元素的大小,以便Three.js画布能够覆盖整个窗口。

html,
body,
#app {
  height: 100%;
  margin: 0;
  overflow: hidden;
}
.controls {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
}

完成这些后,你就可以转到JavaScript,开始构建Three.js场景。

构建Three.js场景

从Vite创建的main.js 文件开始,我们访问DOM元素,监听input 事件,以便将来处理挤压变化,并将创建Three.js场景的工作委托给另一个模块--scene.js

import "./style.css";
import { setupScene } from "./scene";

const defaultExtrusion = 1;
const container = document.querySelector("#app");
const extrusionInput = document.querySelector("#input");
const scene = setupScene(container);

extrusionInput.addEventListener("input", () => {
  // Handle extrusion change
});
extrusionInput.value = defaultExtrusion;

scene.js 文件中的重任都集中在创建 Three.js 场景上。

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

const setupScene = (container) => {
  const scene = new THREE.Scene();
  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  const camera = new THREE.PerspectiveCamera(
    50,
    window.innerWidth / window.innerHeight,
    0.01,
    1e7
  );
  const ambientLight = new THREE.AmbientLight("#888888");
  const pointLight = new THREE.PointLight("#ffffff", 2, 800);
  const controls = new OrbitControls(camera, renderer.domElement);
  const animate = () => {
    renderer.render(scene, camera);
    controls.update();

    requestAnimationFrame(animate);
  };

  renderer.setSize(window.innerWidth, window.innerHeight);
  scene.add(ambientLight, pointLight);
  camera.position.z = 50;
  camera.position.x = 50;
  camera.position.y = 50;
  controls.enablePan = false;

  container.append(renderer.domElement);
  window.addEventListener("resize", () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });
  animate();

  return scene;
};

export { setupScene };

快速回顾

现在,我假设你已经对Three.js有了一些了解。如果没有,网络上有一些很好的指南,包括在这个博客上。说了这么多,下面是对事情的大致概述。

首先,创建每个Three.js场景的基本部分:scenerenderercamera 。注意THREE.WebGLRenderer 的选项--打开抗锯齿和背景透明度--这对使应用程序看起来很好很重要。

然后,还有灯光和THREE.OrbitControls 。这些都是必要的,可以正确地照亮我们将使用的材料,并允许轻松控制三维视图,分别。

最后是render 循环,额外的设置,如相机位置、渲染器视口大小和窗口大小调整处理程序。

该函数返回THREE.Scene 实例,以便于从主模块访问。

使用SVGLoader

场景设置好了,是时候加载一些SVG文件了!为此,我们将转向SVGLoader。为此,我们将移动到另一个模块:svg.js

import * as THREE from "three";
import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader";

const fillMaterial = new THREE.MeshBasicMaterial({ color: "#F3FBFB" });
const stokeMaterial = new THREE.LineBasicMaterial({
  color: "#00A5E6",
});
const renderSVG = (extrusion, svg) => {
  const loader = new SVGLoader();
  const svgData = loader.parse(svg);

  // ...
};

export { renderSVG };

在这里,你可以看到将用于挤出的几何体的材料,这样,填充和描边都可以更好地可视化源SVG形状和它们在三维空间中的挤出。

接下来是我们的焦点--renderSVG() 函数,它将使用SVGLoader 来加载并随后挤出SVG形状。

但在这之前,我们先来看看SVGLoader的API。

SVGLoader API

SVGLoader是Three.jsLoader 类的一个实例,继承并扩展了它的方法和属性,最主要的是load()loadAsync()parse()

这三个方法负责SVGLoader的大部分功能。所有这些方法都会产生一个ShapePath 实例的数组,只是方式不同。

// ...
const loader = new SVGLoader();
const svgUrl = "..."; //SVG URL
const svg = "..."; // SVG data

loader.load(svgUrl, (data) => {
  const shapePaths = data.paths;
  // ...
});
// or
loader.loadAsync(svgUrl).then((data) => {
  const shapePaths = data.paths;
  // ...
});
// or
const data = loader.parse(svg);
const shapePaths = data.paths;

要点在于,在使用SVGLoader时,你总是会使用这些方法中的至少一个,这取决于你想如何访问SVG数据。关于更详细的信息,你可以参考官方文档

一旦你有了ShapePath,你需要把它们转换成一个Shape的数组。要做到这一点,你应该使用SVGLoader.createShapes() 静态方法,像这样。

shapePaths.forEach((path) => {
  const shapes = SVGLoader.createShapes(path);
  // ...
});

从这里开始,剩下的就是要从可用的形状中生成ExtrudeGeometry

挤出几何图形

为了挤出我们的SVG-originatedShapes,我们需要更新renderSVG() 函数。

// ...
const renderSVG = (extrusion, svg) => {
  const loader = new SVGLoader();
  const svgData = loader.parse(svg);
  const svgGroup = new THREE.Group();
  const updateMap = [];

  svgGroup.scale.y *= -1;
  svgData.paths.forEach((path) => {
    const shapes = SVGLoader.createShapes(path);

    shapes.forEach((shape) => {
      const meshGeometry = new THREE.ExtrudeBufferGeometry(shape, {
        depth: extrusion,
        bevelEnabled: false,
      });
      const linesGeometry = new THREE.EdgesGeometry(meshGeometry);
      const mesh = new THREE.Mesh(meshGeometry, fillMaterial);
      const lines = new THREE.LineSegments(linesGeometry, stokeMaterial);

      updateMap.push({ shape, mesh, lines });
      svgGroup.add(mesh, lines);
    });
  });

  const box = new THREE.Box3().setFromObject(svgGroup);
  const size = box.getSize(new THREE.Vector3());
  const yOffset = size.y / -2;
  const xOffset = size.x / -2;

  // Offset all of group's elements, to center them
  svgGroup.children.forEach((item) => {
    item.position.x = xOffset;
    item.position.y = yOffset;
  });
  svgGroup.rotateX(-Math.PI / 2);

  return {
    object: svgGroup,
    update(extrusion) {
      updateMap.forEach((updateDetails) => {
        const meshGeometry = new THREE.ExtrudeBufferGeometry(
          updateDetails.shape,
          {
            depth: extrusion,
            bevelEnabled: false,
          }
        );
        const linesGeometry = new THREE.EdgesGeometry(meshGeometry);

        updateDetails.mesh.geometry.dispose();
        updateDetails.lines.geometry.dispose();
        updateDetails.mesh.geometry = meshGeometry;
        updateDetails.lines.geometry = linesGeometry;
      });
    },
  };
};

让我们来分析一下这里发生了什么。

首先,你会注意到在SVG的加载过程中,我们创建了一个THREE.Group ,以容纳我们所有的挤出的形状。然后,它在Y轴上被翻转,随后,我们适当地偏移它,旋转它的位置,并将它在我们的场景中适当地居中。这确保了在使用OrbitControls ,这样在没有平移的情况下,控件主要是围绕着对象的基座运行,从而获得最佳的用户体验。

还有一些重要的代码在shapes 循环里面,在这里我们要从形状中生成THREE.ExtrudeBufferGeometry 。由于我们不需要以任何复杂的方式与这些几何体进行交互,选择缓冲几何体可以在不增加成本的情况下提高性能。

我们还使用THREE.EdgesGeometry ,同时使用THREE.LineSegments ,以突出边缘。

网格被添加到组中,需要的细节被保存到我们的updateMap 。这在返回的update() 方法中使用,以根据选定的挤压正确更新几何体。为了做到这一点,我们创建新的几何体,并处理掉旧的几何体以清理内存。

把它放在一起

现在,renderSVG() 函数已经准备好了,我们现在可以回到main.js 模块并好好利用它了。

// ...
import { renderSVG } from "./svg";
import { svg } from "./example";

// ...
const { object, update } = renderSVG(defaultExtrusion, svg);

scene.add(object);

extrusionInput.addEventListener("input", () => {
  update(Number(extrusionInput.value));
});
// ...

example.js ,我将导出一个SVG字符串进行测试。在这里,它被导入并和默认的挤压一起传递给renderSVG() 。得到的对象被解构,THREE.Group 加入到场景中,update() 方法被用来处理挤出的变化。

就这样,我们已经准备好了基本的SVG挤出器!

请看CodePen上Arek Nawo(@areknawo
的Pen
Three.js SVG挤出器

改进的余地

自然,上面的应用程序是相当基本的,它可以从额外的功能中受益。首先想到的是一个 "聚焦 "功能,在改变挤压时调整OrbitControlscamera 。让我们看看吧!

增加对焦功能,使摄像机与物体相适应

我们将把这个功能放在scene.js 模块中,因为它是密切相关的。

// ...
// Inspired by https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/3
const fitCameraToObject = (camera, object, controls) => {
  const boundingBox = new THREE.Box3().setFromObject(object);
  const center = boundingBox.getCenter(new THREE.Vector3());
  const size = boundingBox.getSize(new THREE.Vector3());
  const offset = 1.25;
  const maxDim = Math.max(size.x, size.y, size.z);
  const fov = camera.fov * (Math.PI / 180);
  const cameraZ = Math.abs((maxDim / 4) * Math.tan(fov * 2)) * offset;
  const minZ = boundingBox.min.z;
  const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ;

  controls.target = center;
  controls.maxDistance = cameraToFarEdge * 2;
  controls.minDistance = cameraToFarEdge * 0.5;
  controls.saveState();
  camera.position.z = cameraZ;
  camera.far = cameraToFarEdge * 3;
  camera.updateProjectionMatrix();
};

export { fitCameraToObject, setupScene };

其步骤如下。

  1. 获取物体的边界框并计算其最大尺寸以调整摄像机
  2. 应用一个选定的偏移量(1.25),以便该物体不会填满整个屏幕
  3. 设置OrbitControls 目标,使摄像机围绕物体运行,并通过调整maxDistanceminDistance 属性,防止其过度放大和缩小

setupScene() 函数也需要调整,以方便访问摄像机和控制实例。

// ...
const setupScene = (container) => {
  // ...
  return { scene, camera, controls };
};
// ...

然后只要在HTML中的.controls 容器中添加一个#focus 按钮,并编辑main.js ,以整合所有的变化。

// ...
import { fitCameraToObject, setupScene } from "./scene";
// ...

const focusButton = document.querySelector("#focus");
const { scene, camera, controls } = setupScene(app);

// ...
focusButton.addEventListener("click", () => {
  fitCameraToObject(camera, object, controls);
});
// ...

这就是我们如何在我们的3D应用程序中添加聚焦功能的方法!

请看CodePen上Arek Nawo(@areknawo)
的Pen
Three.js SVG挤出机与聚焦

底线

正如你所看到的,Three.js是一个非常强大的库。它的SVGLoader以及无数其他的API使其功能非常全面。

只要有想法,有学习,有时间,你就可以用Three.js为网络带来前所未有的原生3D体验。The sky’s the limit!

The postBringing SVGs to Three.js with SVGLoaderappeared first onLogRocket Blog.