Draw a Line with a simple single colour fading gradient

I am trying to find a solution for drawing a simple white line which fades out at a certain point. It is to be used as a vr controller laser pointer with a reticle circle.

const laserLine = new Line( new BufferGeometry(), new LineBasicMaterial( {
   linewidth: 1
} ) );

laserLine.geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, -2, 0, 0, -10 ], 3 ) );
laserLine.name = 'line';

Current code looks like this so I am using a buffer geometry already. I am actually using a rawshadermaterial with a simple trimmed down fragment and vertex shader also.

You can use THREE.Raycaster() and THREE.ShaderMaterial(). But I’m sure, there can be several ways to achieve the desired result :slight_smile:

Raycaster_Laser2

https://jsfiddle.net/prisoner849/swvj5syL/

  var lineVertexShader = `
  	varying vec3 vPos;
    void main() 
    {
      vPos = position;
      vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
      gl_Position = projectionMatrix * modelViewPosition;
    }
  `;

  var lineFragmentShader = `
    uniform vec3 origin;
    uniform vec3 color;
  	varying vec3 vPos;
    float limitDistance = 7.0;
    void main() {
    	float distance = clamp(length(vPos - origin), 0., limitDistance);
      float opacity = 1. - distance / limitDistance;
      gl_FragColor = vec4(color, opacity);
    }

  `;

. . .

var objs = [];

var plane = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), new THREE.MeshBasicMaterial({
  color: "gray"
}));
scene.add(plane);
objs.push(plane);

for (let i = 0; i < 10; i++) {
  var mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({
    color: Math.random() * 0x777777 + 0x777777,
    wireframe: true
  }));
  mesh.position.set(
    THREE.Math.randInt(-4, 4),
    THREE.Math.randInt(-4, 4),
    0.5
  );
  scene.add(mesh);
  objs.push(mesh);
}

var group = new THREE.Mesh(new THREE.SphereGeometry(0.1, 4, 2), new THREE.MeshBasicMaterial({
  color: "red",
  wireframe: true
}));
group.position.set(0, 0, 5);
var emitter = new THREE.Mesh(new THREE.SphereGeometry(0.1, 4, 2), new THREE.MeshBasicMaterial({
  color: "white",
  wireframe: true
}));
emitter.position.set(0, 0, 5);
group.add(emitter);
scene.add(group);

var controls = new THREE.OrbitControls(emitter, renderer.domElement);
controls.zoomEnabled = false;

window.addEventListener("mousemove", mouseMove, false);

var lineGeom = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3()]);
var rayLine = new THREE.Line(lineGeom, new THREE.ShaderMaterial({
  uniforms: {
    color: {
      value: new THREE.Color(0x00ff00)
    },
    origin: {
      value: new THREE.Vector3()
    }
  },
  vertexShader: lineVertexShader,
  fragmentShader: lineFragmentShader,
  transparent: true
}));
scene.add(rayLine);

var marker = new THREE.Mesh(new THREE.SphereGeometry(0.2, 4, 2), new THREE.MeshBasicMaterial({
  color: 0x00ff00
}));
marker.visible = false;
scene.add(marker);
var raycaster = new THREE.Raycaster(),
  intersects = [],
  distanceDefault = 1000;
var emitPosWorld = new THREE.Vector3(), infiniteDist = new THREE.Vector3(),
  direction = new THREE.Vector3();

function mouseMove() {
  emitter.getWorldPosition(emitPosWorld);
  direction.subVectors(group.position, emitPosWorld).normalize();
  raycaster.set(emitPosWorld, direction);
  intersects = raycaster.intersectObjects(objs);
  lineGeom.attributes.position.setXYZ(0, emitPosWorld.x, emitPosWorld.y, emitPosWorld.z);
  if (intersects.length === 0) {
    marker.visible = false;
    infiniteDist.copy(emitPosWorld).addScaledVector(direction, distanceDefault);
    lineGeom.attributes.position.setXYZ(1, infiniteDist.x, infiniteDist.y, infiniteDist.z);
  } else {
    marker.visible = true;
    marker.position.copy(intersects[0].point);
    lineGeom.attributes.position.setXYZ(1, intersects[0].point.x, intersects[0].point.y, intersects[0].point.z);
  }
  lineGeom.attributes.position.needsUpdate = true;
  rayLine.material.uniforms.origin.value.copy(emitPosWorld);
}
1 Like

I’m confused by the result sorry. I mean the color on the line goes from white and fades to transparent.

I am already updating the line position on a hit like

const positionArray = line.geometry.attributes.position.array;
positionArray[5] = intersection.point.z;
line.geometry.attributes.position.needsUpdate = true;

Do you mean a visual effect like this?

image

That is exactly what I am trying to do yes. With the reticle pointer on the end. I am trying to model the Daydream view pointer,

Although not the model controller for now, they are hard to source open source anyway. That can be done by the user as they can get quite large.

I am adding it into this integration with VRController and Retlicum projects.

In the official Daydream example, the demo just uses a simple line without any transition.

https://threejs.org/examples/#webvr_daydream

To achieve the visual effect of the picture, you have to gradually decrease the opacity of the tube/line. I wonder if the A-Frame team already developed something like this for their project. /ping @donmccurdy

@danrossi I’ve updated my previous reply and its jsfiddle.

Not aware of any A-Frame components like this, although adding it to the laser-controls component would be a nice idea.

OK thankyou very much. I am using a rawshadermaterial. So you have to use a custom fragment shader and modify uniforms. I’ll see if I can repurpose it for my solution.

In the actual Daydream view laser pointer the position of the fade never changes. I am assuming here it changes on raycaster hits.

@danrossi
In my example, the length of fading (from the start of the line to the point, where the line becomes totally invisible) always equals 7 units, as I’ve set it like that float limitDistance = 7.0; in the fragment shader, just because of my wish :wink: Of course, you can use a parameter in uniforms to set this length dynamically, means to make it adjustable.

I’m trying to decipher how the opacity change works but can’t. I dont want to have to update some origin attribute.

If the position vector is updated in my example on a hit can I not just use that ?

float distance = clamp(length(vPos), 0., limitDistance);
float opacity = 1. - distance / limitDistance;

Just doing this doesn’t seem to work. I might try and make a fiddle.

I’ve tried to modify yours to suit how I am updating the geometry.

It’s a bit wonky but without the origin values it seems to fade out after the 7.0 value. This is all that is required thanks.

My line is added to the VRController object so gets updated with that.

https://jsfiddle.net/5h1ttbce/15/

Here, it means that, if the length of vPos is greater than limitDistance, then it will equal limitDistance, and then 1. - limitDistance / limitDistance will result to 0.
From here, you have an option to transform the point of intersection into coordinate system of the laser line, thus you’ll have the start point at [0,0,0] (at your controller object) and the end point is that transformed point of intersection.

1 Like

I’ve made a new jsfiddle, based on my previous one. It has the same functionality, but it works differently: without the origin uniform parameter, point of intersection transformed into the coordinate system of the laser line.
https://jsfiddle.net/prisoner849/pvn2Lxw8/

That is working also. What is the requirement of the origin value again ?

I’ll try and integrate into another example.

origin needs to be used when points of the laser line is in the world coordinate system (means that the line is added straight to the scene). Without it, you’ll get invisible (fully transparent) line, and you can see its those parts, length of position of which is less than limitDistance.
When you work in the local space of the line, you don’t need origin, as its start point is at the [0, 0, 0].

Ok not a problem. It will be added to the controller object which its position is updated with the game controller data.

I have integrated a test that will take a while to upload properly.

If I match the fade off to the native one in Daydream view , the limitDistance is 2.0.

However this causes intersect issues for some reason I think as the hit is lower on the object. I’ll need to add a circle marker to confirm.

Everything seems to be working and I am working on uploading a full integration demo with the VRController.

although I am unable to move the circle marker in front of the hit object.

Can’t find information how to do this properly.

If I do

laserMarker.position.z = intersection.point.z;

The marker is not in front of the object as I thought it would. It’s in the middle of it.

It’s the last thing to figure out I can’t seem to find any information out there. The marker needs to rescale when changing z it gets larger.

@danrossi
Could you provide explanatory pictures and code?