Sir Robert
Sir Robert

Reputation: 4976

How can I change this Three.js ConvexGeometry to a non-convex geometry?

I'm worked with Three.JS before, but not on meshes. I think I am approaching my problem the right way, but I'm not sure.

The Goal

I'm trying to make a 3D blobby object that has specific verticies. The direction of the verticies are fixed, but their radius from center varies. You can imagine it sort of like an audio equalizer, except radial and in 3D.

I'm open to scrapping this approach and taking a totally different one if there's some easier way to do this.

Current Progress

I took this example and cleaned/modified it to my needs. Here's the HTML and JavaScript:

HTML (disco-ball.html)

<!DOCTYPE html>
<html>
  <head>
    <title>Disco Ball</title>
    <script type="text/javascript" src="../libs/three.js"></script>
    <script type="text/javascript" src="../libs/stats.js"></script>
    <script type="text/javascript" src="../libs/ConvexGeometry.js"></script>
    <script type="text/javascript" src="../libs/dat.gui.js"></script>
    <style type='text/css'>
      /* set margin to 0 and overflow to hidden, to go fullscreen */
      body { margin: 0; overflow: hidden; }
    </style>
  </head>
  <body>

    <div id="Stats-output"></div>
    <div id="WebGL-output"></div>
    <script type="text/javascript" src="01-app.js"></script>
  </body>
</html>

And the JavaScript (01-app.js):

window.onload = init;

const PARAMS = {
  SHOW_SURFACE   : true,
  SHOW_POINTS    : true,
  SHOW_WIREFRAME : true,

  SHOW_STATS     : true
};

// once everything is loaded, we run our Three.js stuff.
function init() {
  var renderParams = {
    webGLRenderer : createWebGLRenderer(),
    step          : 0,
    rotationSpeed : 0.007,
    scene         : new THREE.Scene(),
    camera        : createCamera(),
  };

  // Create the actual points.
  var points      = getPoints(
    100, // Number of points (approximate)
    10,  // Unweighted radius
    // Radius weights for a few points.  This is a multiplier.
    [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2]
  );

  if (PARAMS.SHOW_STATS) {
    renderParams.stats = initStats();
  }

  if (PARAMS.SHOW_SURFACE) {
    renderParams.surface = getHullMesh(points);
    renderParams.scene.add(renderParams.surface);
  }

  if (PARAMS.SHOW_POINTS) {
    renderParams.sphereGroup = getSphereGroup(points);
    renderParams.scene.add(sphereGroup);
  }

  render(renderParams);
}

function render(params) {
  if (params.stats) {
    params.stats.update();
  }

  if (params.sphereGroup) {
    params.sphereGroup.rotation.y = params.step;
  }

  params.step += params.rotationSpeed;

  if (params.surface) {
    params.surface.rotation.y = params.step;
  }


  // render using requestAnimationFrame
  requestAnimationFrame(function () {render(params)});
  params.webGLRenderer.render(params.scene, params.camera);
}


// ******************************************************************
// Helper functions
// ******************************************************************

function getPoints (count, baseRadius, weightMap) {
  // Because this is deterministic, we can pass in a weight map to adjust
  // the radii.
  var points = distributePoints(count,baseRadius,weightMap);

  points.forEach((d,i) => {
    points[i] = new THREE.Vector3(d[0],d[1],d[2]);
  });

  return points;
}


// A deterministic function for (approximately) evenly distributing n points
// over a sphere.
function distributePoints (count, radius, weightMap) {
  // I'm not sure why I need this...
  count *= 100;

  var points = []; 

  var area = 4 * Math.PI * Math.pow(radius,2) / count;
  var dist = Math.sqrt(area);

  var Mtheta    = Math.round(Math.PI / dist);
  var distTheta = Math.PI / Mtheta
  var distPhi   = area / distTheta;

  for (var m = 0; m < Mtheta; m++) {
    let theta  = (Math.PI * (m + 0.5)) / Mtheta;
    let Mphi = Math.round((2 * Math.PI * Math.sin(theta)) / distPhi);
    for (var n = 0; n < Mphi; n++) {
      let phi = ((2 * Math.PI * n) / Mphi);
      // Use the default radius, times any multiplier passed in through the
      // weightMap.  If no multiplier is present, use 1 to leave it
      // unchanged.
      points.push(createPoint(radius * (weightMap[points.length] || 1),theta,phi));
    }   
  }

  return points;
}

function createPoint (radius, theta, phi) {
  var x = radius * Math.sin(theta) * Math.cos(phi);
  var y = radius * Math.sin(theta) * Math.sin(phi);
  var z = radius * Math.cos(theta);

  return [Math.round(x), Math.round(y), Math.round(z)];
}

function createWebGLRenderer () {
  // create a render and set the size
  var webGLRenderer = new THREE.WebGLRenderer();
  webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
  webGLRenderer.setSize(window.innerWidth, window.innerHeight);
  webGLRenderer.shadowMapEnabled = true;
  // add the output of the renderer to the html element
  document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
  return webGLRenderer;
}

function createCamera () {
  // create a camera, which defines where we're looking at.
  var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

  // position and point the camera to the center of the scene
  camera.position.x = -30;
  camera.position.y =  40;
  camera.position.z =  50;

  camera.lookAt(new THREE.Vector3(0, 0, 0));
  return camera;
}

function getSphereGroup (points) {
  sphereGroup = new THREE.Object3D();
  var material = new THREE.MeshBasicMaterial({color: 0xFF0000, transparent: false});

  points.forEach(function (point) {
    var spGeom = new THREE.SphereGeometry(0.2);
    var spMesh = new THREE.Mesh(spGeom, material);
    spMesh.position.copy(point);
    sphereGroup.add(spMesh);
  });

  return sphereGroup;
}

function getHullMesh (points) {
  // use the same points to create a convexgeometry
  var surfaceGeometry = new THREE.ConvexGeometry(points);
  var surface = createMesh(surfaceGeometry);
  return surface;
}

function createMesh(geom) {
  // assign two materials
  var meshMaterial = new THREE.MeshBasicMaterial({color: 0x666666, transparent: true, opacity: 0.25});
  meshMaterial.side = THREE.DoubleSide;

  var wireFrameMat = new THREE.MeshBasicMaterial({color: 0x0000ff});
  wireFrameMat.wireframe = PARAMS.SHOW_WIREFRAME;

  // create a multimaterial
  var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [meshMaterial, wireFrameMat]);

  return mesh;
}

function initStats() {
  var stats = new Stats();
  stats.setMode(0); // 0: fps, 1: ms

  // Align top-left
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.left = '0px';
  stats.domElement.style.top = '0px';

  document.getElementById("Stats-output").appendChild(stats.domElement);

  return stats;
}

What I'm Missing

  1. You can see that there are two points on the "ball" for which I've doubled the radius (big spikes). Of course, since I'm using a ConvexGeometry, the shape is convex... so a number of the points are hidden. What kind of ... non-convex geometry can I use to make those points no longer be hidden?
  2. I would like to subdivide the mesh a bit so it's not simply vertex-to-vertex, but a bit smoother. How can I do that (the spikes less spikey and more blobby)?
  3. I'd like to modify the mesh so different points spike different amounts every few seconds (I have some data arrays that describe how much). How do I modify the geometry after its been made? Ideally with some kind of tweening, but I can do without of that's extremely hard =)

Thanks!

Upvotes: 1

Views: 1535

Answers (1)

Blindman67
Blindman67

Reputation: 54089

Smooth and animate a mesh.

Three provides a huge range of options. These are just suggestions, your best bet is to read the Three documentation start point and find what suits you.

A mesh is just a set of 3D points and an array of indexes describing each triangle. Once you have built the mesh you only need to update the verts and let Three update the shader attributes, and the mesh normals

Your questions

Q1. Use Three.Geometry for the mesh.

Q2. As you are building the mesh you can use the curve helpers eg Three.CubicBezierCurve3 or Three.QuadraticBezierCurve3 or maybe your best option Three.SplineCurve

Another option is to use a modifier and create the simple mesh and then let Three subdivide the mesh for you. eg three example webgl modifier subdivision

Though not the fastest solution, if the vert count is low it will do this each frame without any loss of frame rate.

Q3. Using Three.Geometry you can can set the mesh morph targets, an array of vertices.

Another option is to use a modifier, eg three example webgl modifier subdivision

Or you can modify the vertices directly each frame.

for ( var i = 0, l = geometry.vertices.length; i < l; i ++ ) {
    geometry.vertices[ i ].x = ?;
    geometry.vertices[ i ].y = ?;
    geometry.vertices[ i ].z = ?;
}
mesh.geometry.verticesNeedUpdate = true;

How you do it?

There are a zillion other ways to do this. Which is the best will depend on the load and amount of complexity you want to create. Spend some time and read the doc's, and experiment.

What I would do! maybe?

I am not too sure what you are trying to achieve but the following is a way of getting some life into the animation rather than the overdone curves that seem so ubiquitous these days.

So if the vert count is not too high I would use a Three.BufferGeometry and modify the verts each frame. Rather than use curves I would weight subdivision verts to follow a polynomial curve f(x) = x^2/(x^2 + (1-x)^2) where x is the normalized distance between two control verts (note don't use x=0.5 rather subdivide the mesh in > 2 times)

EG the two control points and two smoothing verts

   // two control points
   const p1 = {x,y,z};
   const p2 = {x,y,z};

   // two weighted points
   // dx,dy,dz are deltas
   // w is the weighted position s-curve
   // wa, and wd are acceleration and drag coefficients. Try to keep their sum < 1

   const pw1 = {x, y, z, dx, dy, dz, w : 1/3, wa : 0.1,wd : 0.7};
   const pw2 = {x, y, z, dx, dy, dz, w : 2/3, wa : 0.1,wd : 0.7};
   // Compute w
   pw1.w = Math.pow(pw1.w,2) / ( Math.pow(pw1.w,2) + Math.pow(1 - pw1.w,2));
   pw2.w = Math.pow(pw2.w,2) / ( Math.pow(pw2.w,2) + Math.pow(1 - pw2.w,2));

Then for each weighted point you can find the new delta and update the position

   // do for x,y,z
   x = (p2.x - p1.x);  // these points are updated every frame

   // get the new pw1 vert target position
   x = p1.x + x * w;

   // get new delta
   pw1.dx += (x - pw1.x) * pw1.wa; // set delta
   pw1.dx *= pw1.wd;

   // set new position
   pw1.x += pw1.dx;

Do for all weighted points then set geometry.vertices

The wa,wd coefficients will change the behaviour of the smoothing, you will have to play with these values to suit your own taste. Must be 0 <= (wa,wd) < 1 and the sum should be wa + wd < 1. High sumed values will result in oscillations, too high and the oscillations will be uncontrolled.

Upvotes: 0

Related Questions