3

I am trying to extrude this path using Three.js ('M10,10h100v100'). You'll note there isn’t any 'z' command in the SVG file, and it is just meant to be a polyline, going right 100 and then down 100.

When I try to extrude it using Three.js and transformSVGPath(), it closes the shape, as if there were a 'z' command to return to the origin.

How do I make it not do this?

body {
  overflow: hidden;
  margin: 0;
}
<script type="module">
  import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';

  import {
    OrbitControls
  } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
  import {
    GLTFLoader
  } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
  import {
    RGBELoader
  } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/RGBELoader.js';
  import {
    RoughnessMipmapper
  } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/utils/RoughnessMipmapper.js';

  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
  camera.position.set(0, 0, 500);
  var renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  var walls;
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);


  function transformSVGPath(pathStr) {

    const DEGS_TO_RADS = Math.PI / 180,
          UNIT_SIZE = 100;
    const DIGIT_0 = 48,
          DIGIT_9 = 57,
          COMMA = 44,
          SPACE = 32,
          PERIOD = 46,
          MINUS = 45;

    var path = new THREE.ShapePath();
    var idx = 1,
      len = pathStr.length,
      activeCmd,
      x = 0,
      y = 0,
      nx = 0,
      ny = 0,
      firstX = null,
      firstY = null,
      x1 = 0,
      x2 = 0,
      y1 = 0,
      y2 = 0,
      rx = 0,
      ry = 0,
      xar = 0,
      laf = 0,
      sf = 0,
      cx, cy;

    function eatNum() {
      var sidx, c, isFloat = false,
          s;
      // Eat delimiters
      while (idx < len) {
        c = pathStr.charCodeAt(idx);
        if (c !== COMMA && c !== SPACE)
          break;
        idx++;
      }
      if (c === MINUS)
        sidx = idx++;
      else
        sidx = idx;
      // Eat number
      while (idx < len) {
        c = pathStr.charCodeAt(idx);
        if (DIGIT_0 <= c && c <= DIGIT_9) {
          idx++;
          continue;
        } else if (c === PERIOD) {
          idx++;
          isFloat = true;
          continue;
        }
        s = pathStr.substring(sidx, idx);
        return isFloat ? parseFloat(s) : parseInt(s);
      }
      s = pathStr.substring(sidx);
      return isFloat ? parseFloat(s) : parseInt(s);
    }

    function nextIsNum() {
      var c;
      // Do permanently eat any delimiters...
      while (idx < len) {
        c = pathStr.charCodeAt(idx);
        if (c !== COMMA && c !== SPACE)
          break;
        idx++;
      }
      c = pathStr.charCodeAt(idx);
      return (c === MINUS || (DIGIT_0 <= c && c <= DIGIT_9));
    }

    var canRepeat;
    activeCmd = pathStr[0];
    while (idx <= len) {
      canRepeat = true;
      switch (activeCmd) {
        // 'moveTo' commands, become lineto's if repeated
        case 'M':
          x = eatNum();
          y = eatNum();
          path.moveTo(x, y);
          activeCmd = 'L';
          firstX = x;
          firstY = y;
          break;
        case 'm':
          x += eatNum();
          y += eatNum();
          path.moveTo(x, y);
          activeCmd = 'l';
          firstX = x;
          firstY = y;
          break;
        case 'Z':
        case 'z':
          canRepeat = false;
          if (x !== firstX || y !== firstY)
            path.lineTo(firstX, firstY);
          break;
          // - lines!
        case 'L':
        case 'H':
        case 'V':
          nx = (activeCmd === 'V') ? x : eatNum();
          ny = (activeCmd === 'H') ? y : eatNum();
          path.lineTo(nx, ny);
          x = nx;
          y = ny;
          break;
        case 'l':
        case 'h':
        case 'v':
          nx = (activeCmd === 'v') ? x : (x + eatNum());
          ny = (activeCmd === 'h') ? y : (y + eatNum());
          path.lineTo(nx, ny);
          x = nx;
          y = ny;
          break;
          // - cubic bezier
        case 'C':
          x1 = eatNum();
          y1 = eatNum();
        case 'S':
          if (activeCmd === 'S') {
            x1 = 2 * x - x2;
            y1 = 2 * y - y2;
          }
          x2 = eatNum();
          y2 = eatNum();
          nx = eatNum();
          ny = eatNum();
          path.bezierCurveTo(x1, y1, x2, y2, nx, ny);
          x = nx;
          y = ny;
          break;
        case 'c':
          x1 = x + eatNum();
          y1 = y + eatNum();
        case 's':
          if (activeCmd === 's') {
            x1 = 2 * x - x2;
            y1 = 2 * y - y2;
          }
          x2 = x + eatNum();
          y2 = y + eatNum();
          nx = x + eatNum();
          ny = y + eatNum();
          path.bezierCurveTo(x1, y1, x2, y2, nx, ny);
          x = nx;
          y = ny;
          break;
          // - quadratic bezier
        case 'Q':
          x1 = eatNum();
          y1 = eatNum();
        case 'T':
          if (activeCmd === 'T') {
            x1 = 2 * x - x1;
            y1 = 2 * y - y1;
          }
          nx = eatNum();
          ny = eatNum();
          path.quadraticCurveTo(x1, y1, nx, ny);
          x = nx;
          y = ny;
          break;
        case 'q':
          x1 = x + eatNum();
          y1 = y + eatNum();
        case 't':
          if (activeCmd === 't') {
            x1 = 2 * x - x1;
            y1 = 2 * y - y1;
          }
          nx = x + eatNum();
          ny = y + eatNum();
          path.quadraticCurveTo(x1, y1, nx, ny);
          x = nx;
          y = ny;
          break;
          // - elliptical arc
        case 'A':
          rx = eatNum();
          ry = eatNum();
          xar = eatNum() * DEGS_TO_RADS;
          laf = eatNum();
          sf = eatNum();
          nx = eatNum();
          ny = eatNum();
          if (rx !== ry) {
            console.warn("Forcing elliptical arc to be a circular one :(",
              rx, ry);
          }
          // SVG implementation notes does all the math for us! woo!
          // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
          // Step 1, using x1 as x1'
          x1 = Math.cos(xar) * (x - nx) / 2 + Math.sin(xar) * (y - ny) / 2;
          y1 = -Math.sin(xar) * (x - nx) / 2 + Math.cos(xar) * (y - ny) / 2;
          // Step 2, using x2 as cx'
          var norm = Math.sqrt(
            (rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1) /
            (rx * rx * y1 * y1 + ry * ry * x1 * x1));
          if (laf === sf)
            norm = -norm;
          x2 = norm * rx * y1 / ry;
          y2 = norm * -ry * x1 / rx;
          // Step 3
          cx = Math.cos(xar) * x2 - Math.sin(xar) * y2 + (x + nx) / 2;
          cy = Math.sin(xar) * x2 + Math.cos(xar) * y2 + (y + ny) / 2;
          var u = new THREE.Vector2(1, 0),
            v = new THREE.Vector2((x1 - x2) / rx,
              (y1 - y2) / ry);
          var startAng = Math.acos(u.dot(v) / u.length() / v.length());
          if (u.x * v.y - u.y * v.x < 0)
            startAng = -startAng;
          // We can reuse 'v' from start angle as our 'u' for delta angle
          u.x = (-x1 - x2) / rx;
          u.y = (-y1 - y2) / ry;
          var deltaAng = Math.acos(v.dot(u) / v.length() / u.length());
          // This normalization ends up making our curves fail to triangulate...
          if (v.x * u.y - v.y * u.x < 0)
            deltaAng = -deltaAng;
          if (!sf && deltaAng > 0)
            deltaAng -= Math.PI * 2;
          if (sf && deltaAng < 0)
            deltaAng += Math.PI * 2;
          path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf);
          x = nx;
          y = ny;
          break;
        default:
          throw new Error("weird path command: " + activeCmd);
      }
      // Just reissue the command
      if (canRepeat && nextIsNum())
        continue;
      activeCmd = pathStr[idx++];
    }
    return path;
  }

  var shapepath = transformSVGPath('M10,10h100v100');

  var shapeArr = shapepath.toShapes(true);

  const wallSideMaterial = new THREE.LineBasicMaterial({
    color: 'rgb(40, 40, 40)',
    side: THREE.DoubleSide
  });
  const blankMaterial = new THREE.MeshBasicMaterial({
    transparent: true,
    opacity: 0,
    colorWrite: false
  });
  const wallMaterials = [blankMaterial, wallSideMaterial];

  shapeArr.forEach((shape) => {
    //const geometry = new THREE.CubeGeometry(200, 200, 200);
    const geometry3d = new THREE.ExtrudeGeometry(shape, {
      depth: 50,
      bevelEnabled: false
    });
    const material = new THREE.MeshNormalMaterial();
    walls = new THREE.Mesh(geometry3d, material);

    walls.scale.y = -1;
    scene.add(walls);
  });

  function animate() {

    requestAnimationFrame(animate);
    render();
  }

  function render() {

    walls.rotation.x += 0.01;
    walls.rotation.y += 0.02;

    renderer.render(scene, camera);
  }

  animate();
</script>

Please see this JSfiddle:

https://jsfiddle.net/nzkjry2o/

5
  • It's actually the expected behavior. I assume you're aiming at something like an "L" shaped to be extruded? If so you would need to define an outline on some kind of stroke width. Maybe this post can help "outline a 3d object in three.js". An open non-filled polyline is invisible if there is no kind of stroke applied – quite likely you need to convert this stroke to a closed polygon to be visible and calculable in 3d projections. Commented Nov 12 at 23:28
  • Seems like three.js can actually handle zero-stroke/surface paths. But you still need to close it manually like so M0 0 h100 v100 h0 v-100 h-100 to make it an "extrudable" polygon shape. See codepen. However, open polylines don't make much sense in 3D space – therefore three.js tries to auto-close a polyline to a polygon. Commented Nov 12 at 23:53
  • Thanks for this. The use case is a floorplan, with these open paths being some of the walls. They are just lines around an area which may or may not enclose the space. I will try to work with your suggestion, and I very much appreciate it! Commented Nov 13 at 2:59
  • It is hardly minimal: "Use as little code as possible that still produces the same problem" Commented Nov 13 at 9:59
  • Fell free to condense @Peter. I was trying to show the entire process from the SVG string to threejs, and I do not know how else to do it. Commented Nov 13 at 12:53

1 Answer 1

0

ExtrudeGeometry always works on objects of the THREE.Shape type. A Shape itself is something filled like a figure; it's not a line, but a surface.

https://github.com/mrdoob/three.js/blob/a95185f43d42fab9678585725d99a08de4937c6f/src/geometries/ExtrudeGeometry.js#L37C1-L38C1

So if you pass an SVG path to ExtrudeGeometry (e.g., via transformSVGPath()), Three.ja assumes it's a shape to fill, not a line, and automatically closes the path, even if there's no Z.

For example, in SVGLoader, there's an autoClose = true mechanism that additionally closes some paths. But even if you don't use it, Shape + ExtrudeGeometry always treat the contour as closed, because that's how 3D surface generation works.

You could try TubeGeometry:

*{
  margin: 0;
}
<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/[email protected]/build/three.module.js",
      "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
    }
  }
</script>

<script type="module">

import * as THREE from "three";
import {OrbitControls} from "three/addons/controls/OrbitControls.js";

const s = new THREE.Scene();
s.background = new THREE.Color(0xeeeeee);

const c = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 1000);
c.position.set(200, 200, 200);
c.lookAt(100, 50, 0);

const r = new THREE.WebGLRenderer({antialias:true});
r.setSize(window.innerWidth, window.innerHeight);
r.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(r.domElement);

const o = new OrbitControls(c, r.domElement);
o.enableDamping = true;
o.dampingFactor = 0.05;

const l = new THREE.DirectionalLight(0xffffff, 1);
l.position.set(1,1,1);
s.add(l);

const p = [
  new THREE.Vector3(10,10,0),
  new THREE.Vector3(110,10,0),
  new THREE.Vector3(110,110,0)
];

const q = new THREE.CatmullRomCurve3(p);

const g = new THREE.TubeGeometry(q, 50, 4, 8, false);
const m = new THREE.MeshStandardMaterial({color: 0x66aaff});
const mesh = new THREE.Mesh(g, m);
s.add(mesh);

function a() {
  requestAnimationFrame(a);
  o.update();
  r.render(s, c);
}
a();

</script>

or extrudePath (but without bevel):

*{margin:0;}
<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/[email protected]/build/three.module.js",
      "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
    }
  }
</script>

<script type="module">

import * as THREE from "three";
import {OrbitControls} from "three/addons/controls/OrbitControls.js";

const s = new THREE.Scene();
s.background = new THREE.Color(0xeeeeee);

const c = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 1000);
c.position.set(200, 200, 200);
c.lookAt(100, 50, 0);

const r = new THREE.WebGLRenderer({antialias:true});
r.setSize(window.innerWidth, window.innerHeight);
r.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(r.domElement);

const o = new OrbitControls(c, r.domElement);
o.enableDamping = true;
o.dampingFactor = 0.05;

const l = new THREE.DirectionalLight(0xffffff, 1);
l.position.set(1,1,1);
s.add(l);

const p = [
  new THREE.Vector3(10,10,0),
  new THREE.Vector3(110,10,0),
  new THREE.Vector3(110,110,0)
];

const q = new THREE.CatmullRomCurve3(p);

const w = 10, h = 1;
const sh = new THREE.Shape();
sh.moveTo(-w/2,-h/2);
sh.lineTo(w/2,-h/2);
sh.lineTo(w/2,h/2);
sh.lineTo(-w/2,h/2);
sh.closePath();

const eS = { steps:50, bevelEnabled:false, extrudePath:q };
const g = new THREE.ExtrudeGeometry(sh, eS);
const m = new THREE.MeshStandardMaterial({color:0xff8844});
const mesh = new THREE.Mesh(g, m);
s.add(mesh);

function a() {
  requestAnimationFrame(a);
  o.update();
  r.render(s, c);
}
a();

</script>

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.