THREEJS中用shader实现边框效果,附代码

THREEJS中用shader实现边框效果,附代码_第1张图片

最终效果如上:

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: ,
 *  linewidth: ,
 *  dashed: ,
 *  dashScale: ,
 *  dashSize: ,
 *  dashOffset: ,
 *  gapSize: ,
 *  resolution: , // 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 
    #include 
    #include 
    #include 
    #include 
    varying vec3 vPos;
    varying vec3 size;
    attribute vec3 center;
    varying vec3 vCenter;
    varying vec3 vNormal;
    #include 
    #include 
    #include 
    #include 
    #include 
    void main() {
      vCenter = center;
      vNormal = normalize(normal);
      vPos = position;
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
    }
		`,

	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 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  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 
    vec4 diffuseColor = vec4( diffuse, opacity );
    vec3 totalEmissiveRadiance = emissive;
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 

    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 

    reflectedLight.indirectDiffuse *= diffuseColor.rgb;

    vec3 outgoingLight = reflectedLight.indirectDiffuse + totalEmissiveRadiance;

    #include 

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
   
    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属性,计算边框的位置。

你可能感兴趣的:(javascript)