Basil Gass
Basil Gass

Reputation: 352

Three.js uniform dashed line relative to camera

I'm working to display geometric figures in 3D, using three.js.

When you draw (by hand) hidden lines as dashed lines, the 'dashes' are regular for all them. This means that a line parallel to the camera plane or a line (nearly) perpendicular to the camera plane should do have the same length and gap.

But this seems to not work with LineDashedMaterial.

For the attached example, I'm using this (very) basic code:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();

renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var geometry = new THREE.BoxGeometry( 2, 2, 2 );
var LINES_DASHED = new THREE.LineSegments(
    new THREE.EdgesGeometry(geometry),
    new THREE.LineDashedMaterial({
        linewidth: 2,
        color: 0x000000,
        dashSize: 0.2,
        gapSize: 0.1,
        depthTest: false,
        polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
    })
);
LINES_DASHED.computeLineDistances();
scene.add( LINES_DASHED );
scene.background = new THREE.Color( 0xffffff);
camera.position.z = 5;

var animate = function () {
    requestAnimationFrame( animate );
    LINES_DASHED.rotation.x += 0.01;
    LINES_DASHED.rotation.y += 0.01;
    renderer.render( scene, camera );
};

animate();
body { margin: 0; }
canvas { width: 100%; height: 100% }
<script src="https://threejs.org/build/three.min.js"></script>

Working example:

https://bs4.scolcours.ch/_dev/3js_ex.php

I thougth that using:

line.computeLineDistance();

will solve the problem. But it seems to calculate the line length in 3D space (which seems to be logical).

Is there something I missed ?

Thanks for your help!

Upvotes: 4

Views: 1652

Answers (1)

Rabbid76
Rabbid76

Reputation: 210908

That's abroad task. It seems that THREE.LineDashedMaterial does not support this.
But it is possible to write a shader and to use a THREE.ShaderMaterial.

The trick is to know the start of a line in the fragment shader. In general this easy by using a flat interpolation qualifier.
Sadly WebGL 1.0 / GLSL ES 1.00 doesn't support this. So we have to use WebGL 2.0 / GLSL ES 3.00.
In OpenGL ES there exists the extension GL_NV_shader_noperspective_interpolation. Unfortunately there doesn't seem to be a corresponding WebGL extension. (See WebGL Extension Registry)

So lets cerate a THREE.WebGLRenderer with a WebGL2 context. See How to use WebGL2:

var canvas = document.createElement( 'canvas' );
var context = canvas.getContext( 'webgl2' );
var renderer = new THREE.WebGLRenderer( { canvas: canvas, context: context } );

The vertex shader has to pass the normalized device coordinate to the fragment shader. Once with default interpolation and once with no (flat) interpolation. This causes that in the fragment shade the first input parameter contains the NDC coordinate of the actual position on the line and th later the NDC coordinate of the start of the line.

flat out vec3 startPos;
out vec3 vertPos;

void main() {
    vec4 pos    = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    gl_Position = pos;
    vertPos     = pos.xyz / pos.w;
    startPos    = vertPos;
}

Additionally the varying inputs, the fragment shader has uniform variables. u_resolution contains the width and the height of the viewport. u_dashSize contains the length of line and u_gapSize the length of a gap in pixel.

So the length of the line from the start to the actual fragment can be calculated:

vec2  dir  = (vertPos.xy-startPos.xy) * u_resolution/2.0;
float dist = length(dir);

And fragment on the gab can be discarded, by the discard command.

if (fract(dist / (u_dashSize + u_gapSize)) > u_dashSize/(u_dashSize + u_gapSize))
    discard; 

Fragment shader:

precision highp float;

flat in vec3 startPos;
in vec3 vertPos;

uniform vec3  u_color;
uniform vec2  u_resolution;
uniform float u_dashSize;
uniform float u_gapSize;

void main(){

    vec2  dir  = (vertPos.xy-startPos.xy) * u_resolution/2.0;
    float dist = length(dir);

    if ( fract(dist / (u_dashSize + u_gapSize)) > u_dashSize/(u_dashSize + u_gapSize) )
        discard; 
    gl_FragColor = vec4(u_color.rgb, 1.0);
}

Setup the THREE.ShaderMaterial and the uniforms:

var uniforms = {
    u_resolution: {type: 'v2', value: {x: vpSize[0], y: vpSize[1]}},
    u_dashSize : {type:'f', value: 10.0},
    u_gapSize : {type:'f', value: 5.0},
    u_color : {type: 'v3', value: {x:0.0, y:0.0, z:0.0} }
};

var material = new THREE.ShaderMaterial({  
        uniforms: uniforms,
        vertexShader: document.getElementById('vertex-shader').textContent,
        fragmentShader: document.getElementById('fragment-shader').textContent
});

var LINES_DASHED = new THREE.LineSegments(
    new THREE.EdgesGeometry(geometry),
    material);

Note, if the resolution of the canvas changes, the values of the u_resolution have to be set:

e.g.

LINES_DASHED.material.uniforms.u_resolution.value.x = window.innerWidth;
LINES_DASHED.material.uniforms.u_resolution.value.y = window.innerHeight;

I applied the suggestions to your original code. See the preview and the example:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 60, window.innerWidth/window.innerHeight, 0.1, 1000 );
var canvas = document.createElement( 'canvas' );
var context = canvas.getContext( 'webgl2' );
var renderer = new THREE.WebGLRenderer( { canvas: canvas, context: context } );

var vpSize = [window.innerWidth, window.innerHeight];
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var geometry = new THREE.BoxGeometry( 2, 2, 2 );

var uniforms = {
    u_resolution: {type: 'v2', value: {x: vpSize[0], y: vpSize[1]}},
    u_dashSize : {type:'f', value: 10.0},
    u_gapSize : {type:'f', value: 5.0},
    u_color : {type: 'v3', value: {x:0.0, y:0.0, z:0.0} }
};
    
var material = new THREE.ShaderMaterial({  
        uniforms: uniforms,
        vertexShader: document.getElementById('vertex-shader').textContent,
        fragmentShader: document.getElementById('fragment-shader').textContent
});

var LINES_DASHED = new THREE.LineSegments(
    new THREE.EdgesGeometry(geometry),
    material);

LINES_DASHED.computeLineDistances();
scene.add( LINES_DASHED );
scene.background = new THREE.Color( 0xffffff);
camera.position.z = 5;

var animate = function () {
    requestAnimationFrame( animate );
    LINES_DASHED.rotation.x += 0.01;
    LINES_DASHED.rotation.y += 0.01;
    renderer.render( scene, camera );
};

window.onresize = function() {
    vpSize = [window.innerWidth, window.innerHeight];
    LINES_DASHED.material.uniforms.u_resolution.value.x = window.innerWidth;
    LINES_DASHED.material.uniforms.u_resolution.value.y = window.innerHeight;
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
}

animate();
<script type='x-shader/x-vertex' id='vertex-shader'>
flat out vec3 startPos;
out vec3 vertPos;

void main() {
    vec4 pos    = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    gl_Position = pos;
    vertPos     = pos.xyz / pos.w;
    startPos    = vertPos;
}
</script>

<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;

flat in vec3 startPos;
in vec3 vertPos;

uniform vec3  u_color;
uniform vec2  u_resolution;
uniform float u_dashSize;
uniform float u_gapSize;

void main(){

    vec2  dir  = (vertPos.xy-startPos.xy) * u_resolution.xy/2.0;
    float dist = length(dir);
    
    if (fract(dist / (u_dashSize + u_gapSize)) > u_dashSize/(u_dashSize + u_gapSize))
        discard; 
    gl_FragColor = vec4(u_color.rgb, 1.0);
}
</script>

<script src="https://rawcdn.githack.com/mrdoob/three.js/r128/build/three.js"></script>

Upvotes: 6

Related Questions