How to get three.js LineSegments to only render visible lines

社会主义新天地 提交于 2020-01-02 07:51:51

问题


I’m trying to get Three.js to render only the FrontSide outlines of geometries. What I want to achieve is a look as close as possible to this:

With BoxGeomtry I came close to what I want, but using LineSegments on a CylinderGeometry gives vertical lines, which makes sense. Can you think of a way I can draw only the "visible" outlines?

Here is what I tried so far:

let coloredMaterial = new THREE.MeshBasicMaterial({
  color: 0xFFD033,
  polygonOffset: true,
  polygonOffsetFactor: 1,
  polygonOffsetUnits: 1
});

let brick = new THREE.Mesh(geometry, coloredMaterial);

let edges = new THREE.EdgesGeometry(brick.geometry);
var outline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
  color: 0x1B3740,
  linewidth: 1.5
}));

let knobGeometry = new THREE.CylinderGeometry(7, 7, 7, 20);
let knob = new THREE.Mesh(knobGeometry, coloredMaterial);

let knobOutline = new THREE.LineSegments(
  new THREE.EdgesGeometry(knob.geometry),
  new THREE.LineBasicMaterial({
    color: 0x1B3740,
    linewidth: 1.5
  })
);

回答1:


This answer is based on @WestLangley's suggestion on the comments, specifically the model used by LDrawLoader on conditional lines.


Control points model

The idea behind conditional lines is to use control points to determine which lines should be drawn.

If the two control points lie on the same side of the clip plane, created by extrapolating the line to infinity, then the line is drawn. Else it is discarded.

Let's consider 2 lines (E,B) and (F,C):

For (E,B), let's use (A) and (C) as controls points. We can clearly see that both control points are on the same side of the plane created by (E,B). Therefore, this line is drawn.

For (F,C), let's use (B) and (D) as controls points. Now, both control points lie on different sides of the plane. So, this line is discarded.


As the implementation of this model can be quite lengthy, I've setup a JSFiddle that can be used as a reference. It's not quite perfect, but I believe it should be helpful enough.

We can't use CylinderBufferGeometry as a base for the edges geometry, because it uses an indexed buffer geometry. Since the control points are determined by each of the lines, and not the vertices, we don't use indices.

For edges that aren't conditional, we can use the same point for both controls, e.g top and bottom circles.

An important thing to notice is that we can't determine, with this model, if a line would be occluded by geometry (what you described as frontSide). So, I used the actual knobs to occlude the back lines.

<html>

<head>

	<title> Edges Outline </title>

	<style>
		body { margin: 0; position: fixed;}
		canvas { width: 100%; height: 100%; display: block;}
	</style>

	<script src="https://threejs.org/build/three.js"></script>
	<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

</head>
<body>

	<script>

		var conditionalLineVertShader = /* glsl */`

		attribute vec3 control0;
		attribute vec3 control1;
		attribute vec3 direction;

		varying float discardFlag;

		#include <common>
		#include <color_pars_vertex>
		#include <fog_pars_vertex>
		#include <logdepthbuf_pars_vertex>
		#include <clipping_planes_pars_vertex>

		void main() {

		#include <color_vertex>

		vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
		gl_Position = projectionMatrix * mvPosition;

		// Transform the line segment ends and control points into camera clip space
		vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
		vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
		vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );

		c0.xy /= c0.w;
		c1.xy /= c1.w;
		p0.xy /= p0.w;
		p1.xy /= p1.w;

		// Get the direction of the segment and an orthogonal vector
		vec2 dir = p1.xy - p0.xy;
		vec2 norm = vec2( -dir.y, dir.x );

		// Get control point directions from the line
		vec2 c0dir = c0.xy - p1.xy;
		vec2 c1dir = c1.xy - p1.xy;

		// If the vectors to the controls points are pointed in different directions away
		// from the line segment then the line should not be drawn.
		float d0 = dot( normalize( norm ), normalize( c0dir ) );
		float d1 = dot( normalize( norm ), normalize( c1dir ) );

		discardFlag = float( sign( d0 ) != sign( d1 ) );

		#include <logdepthbuf_vertex>
		#include <clipping_planes_vertex>
		#include <fog_vertex>

		}
		`;

		var conditionalLineFragShader = /* glsl */`

		uniform vec3 diffuse;
		varying float discardFlag;

		#include <common>
		#include <color_pars_fragment>
		#include <fog_pars_fragment>
		#include <logdepthbuf_pars_fragment>
		#include <clipping_planes_pars_fragment>

		void main() {

		if ( discardFlag > 0.5 ) discard;

		#include <clipping_planes_fragment>

		vec3 outgoingLight = vec3( 0.0 );
		vec4 diffuseColor = vec4( diffuse, 1.0 );

		#include <logdepthbuf_fragment>
		#include <color_fragment>

		outgoingLight = diffuseColor.rgb; // simple shader

		gl_FragColor = vec4( outgoingLight, diffuseColor.a );

		#include <premultiplied_alpha_fragment>
		#include <tonemapping_fragment>
		#include <encodings_fragment>
		#include <fog_fragment>

		}
		`;

	</script>

	<script>

		var renderer = new THREE.WebGLRenderer( { antialias: true } );
		renderer.setSize( window.innerWidth, window.innerHeight );
		document.body.appendChild( renderer.domElement );

		var scene = new THREE.Scene();
		scene.background = new THREE.Color( 0xffffff );
		var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
		var controls = new THREE.OrbitControls( camera, renderer.domElement );

		camera.position.set( 10, 13, 10 );
		controls.target.set( 0, 0, 0 );


		// cube

		var cubeGeometry = new THREE.BoxBufferGeometry( 10, 5, 10 );
		var cubeMaterial = new THREE.MeshBasicMaterial( {
			color: 0xFFD033,
			polygonOffset: true,
			polygonOffsetFactor: 1,
			polygonOffsetUnits: 1
		} );
		var cube = new THREE.Mesh( cubeGeometry, cubeMaterial );
		scene.add( cube );

		var edgesGeometry = new THREE.EdgesGeometry( cubeGeometry );
		var edgesCube = new THREE.LineSegments( edgesGeometry, new THREE.LineBasicMaterial( { color: 0x1B3740, linewidth: 1.5 } ) );
		edgesCube.position.y += 0.6;
		scene.add( edgesCube );


		// knob

		var knobGeometry = new THREE.CylinderGeometry( 1.4, 1.4, 0.8, 30 );
		var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
		knob.position.set( -2.5, 2.9, -2.5 );
		scene.add( knob );

		var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
		knob.position.set( 2.5, 2.9, 2.5 );
		scene.add( knob );

		var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
		knob.position.set( -2.5, 2.9, 2.5 );
		scene.add( knob );

		var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
		knob.position.set( 2.5, 2.9, -2.5 );
		scene.add( knob );


		// knob edges

		var edgesMaterial = new THREE.ShaderMaterial( {
			vertexShader: conditionalLineVertShader,
			fragmentShader: conditionalLineFragShader,
			uniforms: {
				diffuse: { value: new THREE.Color( 0x1B3740 ) }
			},
			linewidth: 1.5
		} );

		var edgesKnob = createCylinderEdges( 1.4, 0.8, 30 );
		edgesKnob.position.set( -2.5, 2.9 + 0.6, -2.5 );
		scene.add( edgesKnob );

		var edgesKnob = createCylinderEdges( 1.4, 0.8, 30 );
		edgesKnob.position.set( 2.5, 2.9 + 0.6, 2.5 );
		scene.add( edgesKnob );

		var edgesKnob = createCylinderEdges( 1.4, 0.8, 30 );
		edgesKnob.position.set( -2.5, 2.9 + 0.6, 2.5 );
		scene.add( edgesKnob );

		var edgesKnob = createCylinderEdges( 1.4, 0.8, 30 );
		edgesKnob.position.set( 2.5, 2.9 + 0.6, -2.5 );
		scene.add( edgesKnob );


		window.addEventListener( 'resize', onResize );


		function animate() {

			requestAnimationFrame( animate );
			controls.update();
			renderer.render( scene, camera );

		};


		function createCylinderEdges( radius, height, segments ) {

			var geometry = new THREE.BufferGeometry();

			var v0 = new THREE.Vector3();
			var v1 = new THREE.Vector3();

			var vertices = [];
			var control0 = [];
			var control1 = [];
			var directions = [];

			// top / bottom circles

			for ( var v = 0; v <= 1; v ++ ) {

				for ( var x = 0; x < segments; x ++ ) {

					var th = ( x / segments ) * Math.PI * 2;
					var c0 = ( (x-1) / segments ) * Math.PI * 2;
					var c1 = ( (x+1) / segments ) * Math.PI * 2;

					var sinTheta = Math.sin( th );
					var cosTheta = Math.cos( th );

					v0.x = radius * sinTheta;
					v0.y = - v * height + height/2;
					v0.z = radius * cosTheta;

					sinTheta = Math.sin( c1 );
					cosTheta = Math.cos( c1 );

					v1.x = radius * sinTheta;
					v1.y = - v * height + height/2;
					v1.z = radius * cosTheta;

					vertices.push( v0.x, v0.y, v0.z );
					vertices.push( v1.x, v1.y, v1.z );

					control0.push( v0.x, v0.y, v0.z );
					control0.push( v0.x, v0.y, v0.z );

					control1.push( v0.x, v0.y, v0.z );
					control1.push( v0.x, v0.y, v0.z );

					directions.push( v1.x - v0.x, v1.y - v0.y, v1.z - v0.z );
					directions.push( v1.x - v0.x, v1.y - v0.y, v1.z - v0.z );

				}


			}

			// vertical edges

			for ( var x = 0; x < segments; x ++ ) {

				var th = ( x / segments ) * Math.PI * 2;
				var c0 = ( (x-1) / segments ) * Math.PI * 2;
				var c1 = ( (x+1) / segments ) * Math.PI * 2;

				var sinTheta = Math.sin( th );
				var cosTheta = Math.cos( th );

				v0.x = radius * sinTheta;
				v0.y = height/2;
				v0.z = radius * cosTheta;

				v1.x = radius * sinTheta;
				v1.y = - height + height/2;
				v1.z = radius * cosTheta;

				vertices.push( v0.x, v0.y, v0.z );
				vertices.push( v1.x, v1.y, v1.z );

				directions.push( v1.x - v0.x, v1.y - v0.y, v1.z - v0.z );
				directions.push( v1.x - v0.x, v1.y - v0.y, v1.z - v0.z );

				var sinTheta = Math.sin( c0 );
				var cosTheta = Math.cos( c0 );

				v0.x = radius * sinTheta;
				v0.y = height/2;
				v0.z = radius * cosTheta;

				control0.push( v0.x, v0.y, v0.z );
				control0.push( v0.x, v0.y, v0.z );

				var sinTheta = Math.sin( c1 );
				var cosTheta = Math.cos( c1 );

				v0.x = radius * sinTheta;
				v0.y = height/2;
				v0.z = radius * cosTheta;

				control1.push( v0.x, v0.y, v0.z );
				control1.push( v0.x, v0.y, v0.z );

			}


			geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
			geometry.addAttribute( 'control0', new THREE.Float32BufferAttribute( control0, 3, false ) );
			geometry.addAttribute( 'control1', new THREE.Float32BufferAttribute( control1, 3, false ) );
			geometry.addAttribute( 'direction', new THREE.Float32BufferAttribute( directions, 3, false ) );

			return new THREE.LineSegments( geometry, edgesMaterial );

		}

		function onResize() {

			var w = window.innerWidth;
			var h = window.innerHeight;

			camera.aspect = w / h;
			camera.updateProjectionMatrix();

			renderer.setSize( w, h );

		}

		animate();

	</script>

</body>

</html>



回答2:


It looks like you could probably copy the approach used in the toon shader example

Not only does it use MeshToonMaterial for the solid color, but also uses OutlineEffect found in ./jsm/effects/OutlineEffect.js. You can see in the source code, the relevant points are:

import { OutlineEffect } from './jsm/effects/OutlineEffect.js';

// Pass renderer to effect constructor
renderer = new THREE.WebGLRenderer();
effect = new OutlineEffect( renderer );

// In the render loop:
effect.render( scene, camera );


来源:https://stackoverflow.com/questions/56795062/how-to-get-three-js-linesegments-to-only-render-visible-lines

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!