最终效果如上:
import { BufferAttribute, Vector3 } from 'three';
const _vector = new Vector3();
export function computeVertexNormals(positionAttribute, index) {
if ( positionAttribute !== undefined ) {
let normalAttribute = new BufferAttribute( new Float32Array( positionAttribute.count * 3 ), 3 );
const pA = new Vector3(), pB = new Vector3(), pC = new Vector3();
const nA = new Vector3(), nB = new Vector3(), nC = new Vector3();
const cb = new Vector3(), ab = new Vector3();
// indexed elements
if ( index ) {
for ( let i = 0, il = index.count; i < il; i += 3 ) {
const vA = index.getX( i + 0 );
const vB = index.getX( i + 1 );
const vC = index.getX( i + 2 );
pA.fromBufferAttribute( positionAttribute, vA );
pB.fromBufferAttribute( positionAttribute, vB );
pC.fromBufferAttribute( positionAttribute, vC );
cb.subVectors( pC, pB );
ab.subVectors( pA, pB );
cb.cross( ab );
nA.fromBufferAttribute( normalAttribute, vA );
nB.fromBufferAttribute( normalAttribute, vB );
nC.fromBufferAttribute( normalAttribute, vC );
nA.add( cb );
nB.add( cb );
nC.add( cb );
normalAttribute.setXYZ( vA, nA.x, nA.y, nA.z );
normalAttribute.setXYZ( vB, nB.x, nB.y, nB.z );
normalAttribute.setXYZ( vC, nC.x, nC.y, nC.z );
}
} else {
// non-indexed elements (unconnected triangle soup)
for ( let i = 0, il = positionAttribute.count; i < il; i += 3 ) {
pA.fromBufferAttribute( positionAttribute, i + 0 );
pB.fromBufferAttribute( positionAttribute, i + 1 );
pC.fromBufferAttribute( positionAttribute, i + 2 );
cb.subVectors( pC, pB );
ab.subVectors( pA, pB );
cb.cross( ab );
normalAttribute.setXYZ( i + 0, cb.x, cb.y, cb.z );
normalAttribute.setXYZ( i + 1, cb.x, cb.y, cb.z );
normalAttribute.setXYZ( i + 2, cb.x, cb.y, cb.z );
}
}
for ( let i = 0, il = normalAttribute.count; i < il; i ++ ) {
_vector.fromBufferAttribute( normalAttribute, i );
_vector.normalize();
normalAttribute.setXYZ( i, _vector.x, _vector.y, _vector.z );
}
normalAttribute.needsUpdate = true;
return normalAttribute;
}
}
export function setUpBarycentricCoordinates(geometry) {
let positions = geometry.attributes.position.array;
const normal = computeVertexNormals(geometry.attributes.position, geometry.index);
let normals = normal.array;
// Build new attribute storing barycentric coordinates
// for each vertex
let centers = new BufferAttribute(new Float32Array(positions.length), 3);
// start with all edges disabled
for (let f = 0; f < positions.length; f++) { centers.array[f] = 1; }
geometry.setAttribute( 'center', centers );
// Hash all the edges and remember which face they're associated with
// (Adapted from THREE.EdgesHelper)
function sortFunction ( a, b ) {
if (a[0] - b[0] != 0) {
return (a[0] - b[0]);
} else if (a[1] - b[1] != 0) {
return (a[1] - b[1]);
} else {
return (a[2] - b[2]);
}
}
let edge = [ 0, 0 ];
let hash = {};
let face;
let numEdges = 0;
for (let i = 0; i < positions.length/9; i++) {
let a = i * 9
face = [ [ positions[a+0], positions[a+1], positions[a+2] ] ,
[ positions[a+3], positions[a+4], positions[a+5] ] ,
[ positions[a+6], positions[a+7], positions[a+8] ] ];
for (let j = 0; j < 3; j++) {
let k = (j + 1) % 3;
let b = j * 3;
let c = k * 3;
edge[ 0 ] = face[ j ];
edge[ 1 ] = face[ k ];
edge.sort( sortFunction );
const key = edge[0] + ' | ' + edge[1];
if ( hash[ key ] == undefined ) {
hash[ key ] = {
face1: a,
face1vert1: a + b,
face1vert2: a + c,
face2: undefined,
face2vert1: undefined,
face2vert2: undefined
};
numEdges++;
} else {
hash[ key ].face2 = a;
hash[ key ].face2vert1 = a + b;
hash[ key ].face2vert2 = a + c;
}
}
}
let index = 0;
for (let key in hash) {
const h = hash[key];
// ditch any edges that are bordered by two coplanar faces
let normal1, normal2;
if ( h.face2 !== undefined ) {
normal1 = new Vector3(normals[h.face1+0], normals[h.face1+1], normals[h.face1+2]);
normal2 = new Vector3(normals[h.face2+0], normals[h.face2+1], normals[h.face2+2]);
if ( normal1.dot( normal2 ) >= 0.9999 ) { continue; }
}
// mark edge vertices as such by altering barycentric coordinates
let otherVert;
otherVert = 3 - (h.face1vert1 / 3) % 3 - (h.face1vert2 / 3) % 3;
centers.array[h.face1vert1 + otherVert] = 0;
centers.array[h.face1vert2 + otherVert] = 0;
otherVert = 3 - (h.face2vert1 / 3) % 3 - (h.face2vert2 / 3) % 3;
centers.array[h.face2vert1 + otherVert] = 0;
centers.array[h.face2vert2 + otherVert] = 0;
}
}
首先需要对mesh的geometry加上一个新的attribute——center,这里 是根据法线来计算的,原理是当相邻的法线间角度太大,就代表这里有一个硬的边框。
然后是shader的代码:
/**
* parameters = {
* color: <hex>,
* linewidth: <float>,
* dashed: <boolean>,
* dashScale: <float>,
* dashSize: <float>,
* dashOffset: <float>,
* gapSize: <float>,
* resolution: <Vector2>, // to be set by renderer
* }
*/
import {
ShaderLib,
ShaderMaterial,
UniformsLib,
Color,
UniformsUtils
} from 'three';
UniformsLib.edge = {
useUv: {value: 1.0},
smoothness: {
value: 0.2
},
outlineWidth: { type: 'f', value: 0.05 }, // 边框线条的宽度
outlineColor: { type: 'c', value: new Color(0xffffff) }, // 边框线条的颜色
outlineGlowColor: { type: 'c', value: new Color(0xff0000) }, // 流光的颜色
time: { type: 'f', value: 0.0 },
thickness: { value: 2.5 },
};
ShaderLib['edge'] = {
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.fog,
UniformsLib.edge
]),
vertexShader: /* glsl */ `
#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
varying vec3 vPos;
varying vec3 size;
attribute vec3 center;
varying vec3 vCenter;
varying vec3 vNormal;
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
vCenter = center;
vNormal = normalize(normal);
vPos = position;
#include <uv_vertex>
#include <uv2_vertex>
#include <color_vertex>
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <worldpos_vertex>
#include <envmap_vertex>
#include <fog_vertex>
}
`,
fragmentShader: /* glsl */ `
uniform vec3 diffuse;
uniform vec3 emissive;
uniform float time;
uniform float outlineWidth;
uniform vec3 outlineColor;
uniform vec3 outlineGlowColor;
uniform float opacity;
uniform float thickness;
varying vec3 vPos;
varying vec3 vNormal;
varying vec3 vCenter;
#include <common>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <alphatest_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_pars_fragment>
#include <cube_uv_reflection_fragment>
#include <fog_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
uniform float smoothness;
float edgeFactor(vec2 p){
vec2 grid = abs(fract(p - 0.5) - 0.5) / fwidth(p) / thickness;
return min(grid.x, grid.y);
}
float edgeFactorTri() {
vec3 d = fwidth(vCenter.xyz);
vec3 a3 = smoothstep(vec3(0.0), d * 1.5, vCenter.xyz);
return min(min(a3.x, a3.y), a3.z);
}
float posFactor() {
vec2 coord = vPos.xy;
// Compute anti-aliased world-space grid lines
vec2 grid = abs(fract(coord - 0.5) - 0.5) / fwidth(coord);
float line = min(grid.x, grid.y);
return line;
}
void main() {
#include <clipping_planes_fragment>
vec4 diffuseColor = vec4( diffuse, opacity );
vec3 totalEmissiveRadiance = emissive;
#include <logdepthbuf_fragment>
#include <map_fragment>
#include <color_fragment>
#include <alphamap_fragment>
#include <alphatest_fragment>
#include <specularmap_fragment>
#include <emissivemap_fragment>
ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
// accumulation (baked indirect lighting only)
#ifdef USE_LIGHTMAP
vec4 lightMapTexel= texture2D( lightMap, vUv2 );
reflectedLight.indirectDiffuse += lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;
#else
reflectedLight.indirectDiffuse += vec3( 1.0 );
#endif
// modulation
#include <aomap_fragment>
reflectedLight.indirectDiffuse *= diffuseColor.rgb;
vec3 outgoingLight = reflectedLight.indirectDiffuse + totalEmissiveRadiance;
#include <envmap_fragment>
#include <output_fragment>
#include <tonemapping_fragment>
#include <encodings_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
#include <dithering_fragment>
vec3 e = mix(vec3(1.0), vec3(0.2), edgeFactorTri());
// 计算流光效果
vec3 glowColor = outlineColor;
// 将流光和边框颜色混合
if(e.x > 0.4){
gl_FragColor = vec4( outlineColor, 0.5 );
}
// gl_FragColor = vec4(color, 1.0);
}
`,
};
class EdgeMaterial extends ShaderMaterial {
constructor(parameters) {
super({
type: 'EdgeMaterial',
uniforms: UniformsUtils.clone(ShaderLib['edge'].uniforms),
vertexShader: ShaderLib['edge'].vertexShader,
fragmentShader: ShaderLib['edge'].fragmentShader,
clipping: true, // required for clipping support
});
Object.defineProperties(this, {
color: {
enumerable: true,
get() {
return this.uniforms.diffuse.value;
},
set(value) {
this.uniforms.diffuse.value = value;
},
},
smoothness: {
enumerable: true,
get() {
return this.uniforms.smoothness.value;
},
set(value) {
this.uniforms.smoothness.value = value;
},
},
thickness: {
enumerable: true,
get() {
return this.uniforms.thickness.value;
},
set(value) {
this.uniforms.thickness.value = value;
},
},
time: {
enumerable: true,
get() {
return this.uniforms.time.value;
},
set(value) {
this.uniforms.time.value = value;
},
},
map: {
enumerable: true,
get() {
return this.uniforms.map.value;
},
set(value) {
this.uniforms.map.value = value;
},
},
opacity: {
enumerable: true,
get() {
return this.uniforms.opacity.value;
},
set(value) {
this.uniforms.opacity.value = value;
},
}
});
this.setValues(parameters);
}
}
EdgeMaterial.prototype.isEdgeMaterial = true;
export { EdgeMaterial };
核心的计算方法就是
float edgeFactorTri() {
vec3 d = fwidth(vCenter.xyz);
vec3 a3 = smoothstep(vec3(0.0), d * 1.5, vCenter.xyz);
return min(min(a3.x, a3.y), a3.z);
}
这里通过之前添加的vCenter属性,计算边框的位置。