Tom
Tom

Reputation: 3421

How to project a rectangle onto a Mesh/Terrain object, for use as a select marquee? (in three.js)

I have a terrain which was generated using the THREE.Terrain library. I'd like to be able to click and drag out a marquee and select objects that are on the surface of the Terrain Mesh.

Currently I am detecting the start and end of the drag, and drawing out a rectangle in the global XZ plane, but I'd prefer it to be flush with the surface.

Currently it looks like this;

enter image description here

However what I am aiming for is something more like this;

enter image description here

I am wondering if I have missed some obvious way of doing that with the Core three.js features.

There is always the brute force method of casting rays at intervals around the perimeter of the rectangle, and creating a series of line segments to approximate the projected rectangle, but I was wondering if there was a native method.

(I only just started looking at three.js this week, so I might have missed something obvious... though I've spent the last day experimenting, and haven't had much luck)

Update

Based on @prisoner849's suggestion, I mashed up his code with the Terrain demo and that seems to be working pretty well.

    varying vec2 vPos;
    
    void main() {
      vec2 Ro = size * .5;
      vec2 Uo = abs( vPos - center.xz ) - Ro;
      
      vec3 c = mix(vec3(1.), vec3(1.,0.,0.), float(enabled && (abs(max(Uo.x,Uo.y)) < lineHalfWidth)  ));
      
      gl_FragColor = vec4(c, float(enabled && (abs(max(Uo.x,Uo.y)) < lineHalfWidth)  ));
    }
    
  `;

The code needs a massive clean up, and the marquees need to be rotated to match the camera perspective, and it would be nice to have ctrl-click to add to selection set, etc, etc.

But in principle the fragment shader worked well...

enter image description here

Upvotes: 6

Views: 3897

Answers (2)

wolf72
wolf72

Reputation: 19

If anyone else is having troubles understanding the shader here is my attempt at an explanation.

size contains the width and height of the rectangle Ro is half the width, half the height

Uo is a measure of how close to the edge of the rectangle you are. If vPos is on one of the edges the expression will be 0 for either X or Y. Take the example of a point on the left hand side - abs(vPos.x - centre.x) will be equal to Ro, so when you subtract Ro the result is 0.

c - mix is a function that does linear interpolation between two values - You give it a start, end and a value to use to do the interpolation. Here we are interpolating between WHITE and RED and we are using our expression of how close we are to the edge of the rectangle (float(abs(max(Uo.x,Uo.y)) < lineHalfWidth) to do the interpolation.

gl_FragColor — we need a vec4 for color so we create one from c by sticking a 1 on the end of all the values of c.

Upvotes: -1

prisoner849
prisoner849

Reputation: 17596

Right after I posted my comment, I had a thought exactly like Don McCurdy (cheers, Don :) ). Quick search on https://www.shadertoy.com gave me that shader https://www.shadertoy.com/view/XlsBRB (look at the awesome comments from FabriceNeyret2 there). So I just adapted that fragment shader for this very rough concept.

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

var controls = new THREE.OrbitControls(camera, renderer.domElement);

var geom = new THREE.PlaneGeometry(20, 20, 10, 10);
geom.vertices.forEach(v => {
  v.z = THREE.Math.randFloat(-1, 1);
});
geom.rotateX(-Math.PI * .5);
geom.computeFaceNormals();
geom.computeVertexNormals();

var uniforms = {
  center: {
    value: new THREE.Vector3()
  },
  size: {
    value: new THREE.Vector2(1, 1)
  },
  lineHalfWidth: {
    value: 0.1
  }
}

var matShader = new THREE.ShaderMaterial({
  uniforms: uniforms,
  vertexShader: vertShader,
  fragmentShader: fragShader
});

var matWire = new THREE.MeshBasicMaterial({
  color: "gray",
  wireframe: true
});

var obj = THREE.SceneUtils.createMultiMaterialObject(geom, [matShader, matWire]);

scene.add(obj);

var gui = new dat.GUI();
gui.add(uniforms.size.value, "x", .5, 5.0).name("size.x");
gui.add(uniforms.size.value, "y", .5, 5.0).name("size.y");
gui.add(uniforms.lineHalfWidth, "value", .05, 2.0).name("line half-width");

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects = [];
var point = new THREE.Vector3();

window.addEventListener("mousemove", function(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  intersects = raycaster.intersectObject(obj, true);
  if (intersects.length === 0) return;
  obj.worldToLocal(point.copy(intersects[0].point));
  uniforms.center.value.copy(point);

}, false);


render();

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
body {
  overflow: hidden;
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/utils/SceneUtils.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/libs/dat.gui.min.js"></script>
<script>
  var vertShader = `
    varying vec2 vPos;
    void main() {
      vPos = position.xz;
      gl_Position = projectionMatrix *
                    modelViewMatrix *
                    vec4(position,1.0);
    }
  `;

  var fragShader = `
    uniform vec3 center;
    uniform vec2 size;
    uniform float lineHalfWidth;
    
    varying vec2 vPos;
    
    void main() {
      vec2 Ro = size * .5;
      vec2 Uo = abs( vPos - center.xz ) - Ro;
      
      vec3 c = mix(vec3(1.), vec3(1.,0.,0.), float(abs(max(Uo.x,Uo.y)) < lineHalfWidth));
      
      gl_FragColor = vec4(c, 1.  );
    }
    
  `;
</script>

Upvotes: 10

Related Questions