Reputation: 352
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
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