Reputation: 3421
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;
However what I am aiming for is something more like this;
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...
Upvotes: 6
Views: 3897
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
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