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

最终效果如上:

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值