问题
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