目录
Merge合并
现象
思路
实现
为单个geometry添加映射
通过id检索Merge后的Geometry映射属性,获取顶点坐标
onBeforeCompile修改编译前材质的着色代码
编译前材质的顶点着色代码
编译前材质的片元着色代码
着色器代码
注意
效果
Merge合并
mergeBufferGeometries 是用于合并多个几何体(BufferGeometry)为一个几何体的工具。这种方法适用于当有大量的几何体需要渲染时,可以将它们合并为一个几何体来减少渲染调用,从而提高性能。合并后的几何体将会生成一个大的缓冲区,包含了所有合并的几何体的数据,这样在渲染时只需一次性加载这个大缓冲区即可,减少了渲染调用和资源占用。
现象
mergeBufferGeometries 是将每个小geometry的顶点信息做合并,所有的顶点坐标按序存放在合并后的缓冲对象 position数组中。一个大的geometry对应一个材质生成一个合并后的物体
由于没有单个的概念,也就无法通过直接修改材质实现对单个geometry的控制
思路
- 给每个geometry添加缓冲属性,存储 id和geometry顶点数量。merge合并后,每个geometry的自定义映射属性会同position一样push在一个数组中
- 要单个控制时:通过id检索映射数组,可以得到当前geometry的顶点数量,从而得到这段顶点在merge的position中的位置
- 根据当前geometry顶点坐标,通过onBeforeCompile,修改材质的着色代码
实现
为单个geometry添加映射
每执行次函数创建一个geometry,为当前几何体添加自定义属性customId,存储当前id和顶点数量,每两个为1组
function createLineGeometry(points: Array<Vector3>, id: number) {
const geometry = new BufferGeometry();
geometry.setFromPoints(points);
const position = geometry.getAttribute('position')
const customIdAttribute = new BufferAttribute(new Int16Array([id, position.count]), 2)
geometry.setAttribute('customId', customIdAttribute);
return geometry;
}
如下图,id为0,geometry顶点数量为24
当前几何体的postion(24*3)
通过id检索Merge后的Geometry映射属性,获取顶点坐标
如下,Merge后的Geometry,每个geometry的id和顶点数依次存放在customId中(奇数id,偶数顶点数量)
当前合并了32个geometry,每个几何体的顶点数都是24(合并时,顶点数量不一定一致,这也是要映射顶点数的关键)
Merge后的position
如下函数,检索customId数组,根据id获取当前顶点在总顶点中的开始索引,结束索引
例如,要控制id为1的geometry,此函数应该返回 24、47
const getGeometryVextexHeadTailIndex = (merge) => {
// 当前几何体的顶点数量
let vertexCount = 0
// 顶点起始索引
let vertexStartIndex = 0
// 顶点结束索引
let vertexEndIndex = 0
const customId = merge.geometry.getAttribute('customId')
if(!customId || !mergeId.value.length) return
for (let i = 0; i < customId.array.length; i+=2) {
if (customId.array[i] == mergeId.value) {
// 检索到id,+1 偶 则为当前顶点数
vertexCount = customId.array[i + 1]
vertexEndIndex = vertexStartIndex + vertexCount - 1
return { vertexStartIndex, vertexEndIndex }
}
vertexStartIndex += customId.array[i + 1]
}
}
onBeforeCompile修改编译前材质的着色代码
根据顶点索引,顶点着色器动态传递Varying类型的高亮色,片元着色器会收到插值后的Varying Color,判断当前片元的插值颜色是否和uniform的高亮色一致,一致则修改,效果达成(要高亮的所有顶点组成的每个图元一个色,所以插值后的每个片元也是这个色)
编译前材质的顶点着色代码
对 void main 进行修改
编译前材质的片元着色代码
对 void main 和 vec4 diffuseColor = vec4( diffuse, opacity ); 进行修改
着色器代码
const beforeCompileMaterial = (merge, { vertexStartIndex, vertexEndIndex }) => {
// console.log(vertexStartIndex, vertexEndIndex);
merge.material.onBeforeCompile = (shader) => {
/*
三个uniform
开始索引
结束索引
高亮色
*/
shader.uniforms.vertexStartIndex = { value: vertexStartIndex };
shader.uniforms.vertexEndIndex = { value: vertexEndIndex };
shader.uniforms.highLightColor = { value: merge.highLightColor };
// 修改顶点着色器
shader.vertexShader = shader.vertexShader.replace(
'void main() {',
[
'uniform int vertexStartIndex;',
'uniform int vertexEndIndex;',
'uniform vec3 highLightColor;',
'varying vec3 vColor;',
'void main() {',
// 如果当前顶点索引在 起止索引 间,varing向片元传递高亮色
`if(gl_VertexID >= vertexStartIndex && gl_VertexID <= vertexEndIndex) {`,
'vColor = highLightColor;',
'}'
].join('\n')
)
// 修改片元着色器
shader.fragmentShader = shader.fragmentShader.replace(
'void main() {',
[
'uniform vec3 highLightColor;',
// 如果顶点着色器与片元着色器中有类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值就会被自动地传入片元着色器
'varying vec3 vColor;',
'void main() {'
].join('\n')
)
shader.fragmentShader = shader.fragmentShader.replace(
'vec4 diffuseColor = vec4( diffuse, opacity );',
[
'vec4 diffuseColor;',
// 插值后的vColor,当前片元的vColor如果和高亮色一致
'if(vColor == highLightColor) {',
// 修改当前片元为高亮色
'diffuseColor = vec4( vColor, 1.0 );',
'} else {',
// 别的片元不变
'diffuseColor = vec4( diffuse, opacity );',
'}'
].join('\n')
)
}
}
注意
为每个小geometry添加映射时,需要添加缓冲属性,而不是直接类js添加属性,因为Merge的源码是循环geometry数组,逐个push每个geometry的缓冲属性(源码38 ~ 53行),无需修改源码,性能消耗也比较友好
mergeBufferGeometries 源码
/**
* @param {Array<BufferGeometry>} geometries
* @param {Boolean} useGroups
* @return {BufferGeometry}
*/
mergeBufferGeometries: function ( geometries, useGroups ) {
var isIndexed = geometries[ 0 ].index !== null;
var attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );
var morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );
var attributes = {};
var morphAttributes = {};
var morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;
var mergedGeometry = new BufferGeometry();
var offset = 0;
for ( var i = 0; i < geometries.length; ++ i ) {
var geometry = geometries[ i ];
var attributesCount = 0;
// ensure that all geometries are indexed, or none
if ( isIndexed !== ( geometry.index !== null ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );
return null;
}
// gather attributes, exit early if they're different
for ( var name in geometry.attributes ) {
if ( ! attributesUsed.has( name ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' );
return null;
}
if ( attributes[ name ] === undefined ) attributes[ name ] = [];
attributes[ name ].push( geometry.attributes[ name ] );
attributesCount ++;
}
// ensure geometries have the same number of attributes
if ( attributesCount !== attributesUsed.size ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );
return null;
}
// gather morph attributes, exit early if they're different
if ( morphTargetsRelative !== geometry.morphTargetsRelative ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );
return null;
}
for ( var name in geometry.morphAttributes ) {
if ( ! morphAttributesUsed.has( name ) ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' );
return null;
}
if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];
morphAttributes[ name ].push( geometry.morphAttributes[ name ] );
}
// gather .userData
mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || [];
mergedGeometry.userData.mergedUserData.push( geometry.userData );
if ( useGroups ) {
var count;
if ( isIndexed ) {
count = geometry.index.count;
} else if ( geometry.attributes.position !== undefined ) {
count = geometry.attributes.position.count;
} else {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );
return null;
}
mergedGeometry.addGroup( offset, count, i );
offset += count;
}
}
// merge indices
if ( isIndexed ) {
var indexOffset = 0;
var mergedIndex = [];
for ( var i = 0; i < geometries.length; ++ i ) {
var index = geometries[ i ].index;
for ( var j = 0; j < index.count; ++ j ) {
mergedIndex.push( index.getX( j ) + indexOffset );
}
indexOffset += geometries[ i ].attributes.position.count;
}
mergedGeometry.setIndex( mergedIndex );
}
// merge attributes
for ( var name in attributes ) {
var mergedAttribute = this.mergeBufferAttributes( attributes[ name ] );
if ( ! mergedAttribute ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' attribute.' );
return null;
}
mergedGeometry.setAttribute( name, mergedAttribute );
}
// merge morph attributes
for ( var name in morphAttributes ) {
var numMorphTargets = morphAttributes[ name ][ 0 ].length;
if ( numMorphTargets === 0 ) break;
mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
mergedGeometry.morphAttributes[ name ] = [];
for ( var i = 0; i < numMorphTargets; ++ i ) {
var morphAttributesToMerge = [];
for ( var j = 0; j < morphAttributes[ name ].length; ++ j ) {
morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );
}
var mergedMorphAttribute = this.mergeBufferAttributes( morphAttributesToMerge );
if ( ! mergedMorphAttribute ) {
console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );
return null;
}
mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );
}
}
return mergedGeometry;
}