前言
Three.js是目前最流行的web端的三维框架之一,笔者作为对计算机图形学兴趣浓厚的前端程序员,web3D的开发者,阅读Three.js源码是提升技术、加深理解非常好的手段。Three.js经过多年发展,已经是一个功能非常庞大完善的框架了,那么,从何开始呢?笔者认为当然应该是从WebGLRenderer开始。WebGLRenderer是 Three.js 基于 WebGL 技术实现的渲染器,是最核心的组件。本文将从WebGLRenderer类的render方法入手,解析WebGLRenderer渲染的运行原理。
WebGL的绘制流程
这是一段来自的《WebGL编程指南》的代码,绘制了一个三角形。由此可以看到WebGL的绘制的大致流程。如下所示:
js
// RotatedTriangle_Matrix.js (c) matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_xformMatrix;\n' +
'void main() {\n' +
' gl_Position = u_xformMatrix * a_Position;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
// The rotation angle
var ANGLE = 90.0;
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// Write the positions of vertices to a vertex shader
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
// Create a rotation matrix
var radian = Math.PI * ANGLE / 180.0; // Convert to radians
var cosB = Math.cos(radian), sinB = Math.sin(radian);
// Note: WebGL is column major order
var xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]);
// Pass the rotation matrix to the vertex shader
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix');
return;
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
// Specify the color for clearing <canvas>
gl.clearColor(0, 0, 0, 1);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// Draw the rectangle
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3; // The number of vertices
// Create a buffer object
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return false;
}
// Bind the buffer object to target
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// Write date into the buffer object
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
// Assign the buffer object to a_Position variable
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// Enable the assignment to a_Position variable
gl.enableVertexAttribArray(a_Position);
return n;
}
WebGLRenderer的初始化
对WebGL绘制的流程有了一定的了解后,接下来可以开始介绍WebGLRenderer类的源码了,但是,在介绍渲染流程,先介绍一下initGLContext函数,这个函数中初始化了一系列变量,这些变量都是Three.js中和渲染相关的类的实例对象,这里挑选一些我认为重要的变量一一介绍。
js
function initGLContext() {
..........................................省略..........................................
utils = new WebGLUtils( _gl, extensions );
capabilities = new WebGLCapabilities( _gl, extensions, parameters, utils );
state = new WebGLState( _gl, extensions );
info = new WebGLInfo( _gl );
properties = new WebGLProperties();
textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
cubemaps = new WebGLCubeMaps( _this );
cubeuvmaps = new WebGLCubeUVMaps( _this );
attributes = new WebGLAttributes( _gl );
bindingStates = new WebGLBindingStates( _gl, attributes );
geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
objects = new WebGLObjects( _gl, geometries, attributes, info );
morphtargets = new WebGLMorphtargets( _gl, capabilities, textures );
clipping = new WebGLClipping( properties );
programCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );
materials = new WebGLMaterials( _this, properties );
renderLists = new WebGLRenderLists();
renderStates = new WebGLRenderStates( extensions );
background = new WebGLBackground( _this, cubemaps, cubeuvmaps, state, objects, _alpha, premultipliedAlpha );
shadowMap = new WebGLShadowMap( _this, objects, capabilities );
uniformsGroups = new WebGLUniformsGroups( _gl, info, capabilities, state );
bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info );
indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info );
..........................................省略..........................................
}
WebGLState
WebGLState的作用是管理和维护 WebGL 渲染上下文的状态。它封装了与颜色、深度和模板缓冲区、混合、剔除、纹理处理以及其他 WebGL 特定设置相关的功能。举例说明,比如:
setMaterial方法主要设置材质属性,包括:
- 面选择(前/后/双面,gl.CULL_FACE、gl.frontFace)。
- 混合设置(gl.BLEND、gl.blendFunc等)。
- 深度buffer测试/写入。
- 颜色buffer写入。
- 模板buffer测试和操作。
- 多边形偏移(gl.POLYGON_OFFSET_FILL)。
- alpha 值转换为像素的采样覆盖率设置(gl.SAMPLE_ALPHA_TO_COVERAGE)。
对帧缓冲区操作的封装,包括:
ColorBuffer构造函数,封装了如下方法:
- setMask:颜色掩码设置。
- setClear:清除颜色值设置。
- setLocked:锁定缓冲区状态。
DepthBuffer构造函数,封装了如下方法:
- setTest:深度测试启用/禁用。
- setFunc:深度函数的设置。
- setMask:深度掩码设置。
- setClear:清除深度值。
- setReversed:EXT_clip_control设置反向深度行为通过。
StencilBuffer构造函数,封装了如下方法:
- setTest:模板测试启用/禁用。
- setFunc:模板函数的设置。
- setOp:设置操作。
- setMask:模板写入掩码的设置。
- setClear:清除模板。
activeTexture方法,激活纹理单元。
bindTexture方法,将纹理绑定到目标。
bindFramebuffer方法,绑定帧缓冲。
drawBuffers方法,设置绘制缓冲区。
WebGLRenderStates
WebGLRenderStates包含一个WeakMap,key为scene对象,value为WebGLRenderState,WebGLRenderState主要包含light对象、带shadow的light对象、camera。
WebGLRenderLists
WebGLRenderLists包含一个WeakMap,key为scene对象,value为WebGLRenderList,WebGLRenderList按照material的属性,将场景scene中的每个object分为三种类型:
- 透明(transparent)
- 透光物体(transmissive)
- 不透明(transparent) 这三种类型分别保存一个数组,数组中每个元素都是一个对象,这个对象的属性包括:object、geometry、material、顺序、投影到屏幕上的z值,group(几何体顶点的群组编号,顶点的群组编号对应material数组的index,如果material是数组,则每个material都会对应一个几何体中的一组顶点)。
WebGLShadowMap
WebGLShadowMap的作用是实现光源投射阴影的功能,它的两个主要属性: type可以选择四个枚举值:BasicShadowMap、PCFShadowMap、PCFSoftShadowMap、VSMShadowMap,其中后三种都是软阴影。 enabled:渲染器是否开启阴影功能,默认不开启。 这个类还有一个最核心的方法render,这个方法就是在场景中的各个光源相机下渲染一张深度图,这个深度图将被用于后续的渲染阴影。
WebGLAttributes
WebGLAttributes的作用是管理WebGL的顶点属性,即WebGL绘制流程中提到的创建、绑定缓冲区(vbo),为geometry的attributes中的每个属性都创建了一个缓冲区(attributes是一个对象,对象的key为属性名称,如position,uv,normal,color等,value为一个BufferAttribute的对象),并且设置缓冲区中的数据。还提供了更新属性的缓冲区数据的功能,如果某属性的版本发生变化,就更新缓冲区中的数据。
其中包含一个名为buffers的WeakMap,key值为场景中geometry的BufferAttribute,value为一个对象,包含创建的缓冲区、数据类型、占据的字节大小等。
WebGLObjects
每一帧更新一次geometry中的顶点属性的数据,调用WebGLAttributes中的update方法。
WebGLBindingStates
WebGLBindingStates的作用是管理每一个需要渲染的geometry的顶点属性,如:position、uv、color等,管理vao和顶点缓冲区,为每一个被渲染的geometry创建一个state,用于存储geometry顶点属性的启用状态。如果在geometry的attributes中没有该属性,则不启用,设置默认值,即调用gl.vertexAttrib*。如果属性启用,则调用gl.vertexAttribPointer和gl.enableVertexAttribArray,为每个属性设置启用状态和访问方式,使得着色器可以从缓冲区中找到数据。
WebGLUniforms
这个构造函数管理全部的uniform变量,主要功能就是获取各个uniform变量的信息,包括名称、类型、存储地址等。给不同类型uniform(包括纹理类型)变量选择不同的更新接口,主要功能就是设置uniform变量的值。
WebGLTextures
管理全部纹理,根据Three.js中的Texture类型对象,创建、绑定、激活WebGL中的纹理对象,设置WebGL纹理对象的关键参数,如gl.UNPACK_FLIP_Y_WEBGL、gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL等,上传纹理数据(一般是图像数据)。这里管理的主要作用就是对同一个纹理,在数据和属性不变的情况下,不进行重复的创建、上传、设置参数等操作,使得纹理可以被复用。
WebGLPrograms
管理WebGLProgram,根据材质的类型选择合适的着色器代码,根据不同的参数,在着色器代码上拼接不同的宏定义,编译着色器程序,使用着色器程序。
render方法
接下来介绍渲染器的render方法,使用过Three.js同学都知道,Three.js一个场景的最后一步一般都是调用这个render函数,而且这个函数一般要放在requestAnimationFrame中。
在准备这篇文章时,笔者刻意省略了xr、裁剪(clipping)、透光度(transmission)、vsm阴影贴图、MRT、LightProbe等话题,主要是这些话题占用的篇幅比较大,想写清楚需要准备的知识点很多,考虑还是放在以后的专题文章中介绍。
按照代码的顺序,将render函数按照源码拆分成几段来介绍,中间有一些省略:
js
..........................................省略..........................................
if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
..........................................省略..........................................
currentRenderState = renderStates.get( scene, renderStateStack.length );
currentRenderState.init( camera );
renderStateStack.push( currentRenderState );
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
_frustum.setFromProjectionMatrix( _projScreenMatrix );
..........................................省略..........................................
currentRenderList = renderLists.get( scene, renderListStack.length );
currentRenderList.init();
renderListStack.push( currentRenderList );
这段代码为最开始的更新scene对象,更新camera,初始化WebGLRenderState,如上文所说,这个对象负责管理光源和阴影,renderStateStack是一个栈,主要用来在嵌套渲染中隔离不同的renderState(比如某个模型在onBeforeRender中调用render方法),为每次render调用都创建一个独立的WebGLRenderState。初始化WebGLRenderList也类似,这个对象主要负责管理要渲染的object。
js
function projectObject( object, camera, groupOrder, sortObjects ) {
if ( object.visible === false ) return;
const visible = object.layers.test( camera.layers );
if ( visible ) {
if ( object.isGroup ) {
groupOrder = object.renderOrder;
} else if ( object.isLOD ) {
if ( object.autoUpdate === true ) object.update( camera );
} else if ( object.isLight ) {
currentRenderState.pushLight( object );
if ( object.castShadow ) {
currentRenderState.pushShadow( object );
}
} else if ( object.isSprite ) {
if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {
if ( sortObjects ) {
_vector4.setFromMatrixPosition( object.matrixWorld )
.applyMatrix4( _projScreenMatrix );
}
const geometry = objects.update( object );
const material = object.material;
if ( material.visible ) {
currentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );
}
}
} else if ( object.isMesh || object.isLine || object.isPoints ) {
if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {
const geometry = objects.update( object );
const material = object.material;
if ( sortObjects ) {
if ( object.boundingSphere !== undefined ) {
if ( object.boundingSphere === null ) object.computeBoundingSphere();
_vector4.copy( object.boundingSphere.center );
} else {
if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
_vector4.copy( geometry.boundingSphere.center );
}
_vector4
.applyMatrix4( object.matrixWorld )
.applyMatrix4( _projScreenMatrix );
}
if ( Array.isArray( material ) ) {
const groups = geometry.groups;
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const group = groups[ i ];
const groupMaterial = material[ group.materialIndex ];
if ( groupMaterial && groupMaterial.visible ) {
currentRenderList.push( object, geometry, groupMaterial, groupOrder, _vector4.z, group );
}
}
} else if ( material.visible ) {
currentRenderList.push( object, geometry, material, groupOrder, _vector4.z, null );
}
}
}
}
const children = object.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
projectObject( children[ i ], camera, groupOrder, sortObjects );
}
}
接下来调用projectObject函数,上面贴的是projectObject函数的源码,这个函数比较清晰,就是将light对象传入到currentRenderState中。
对所有的点线面object调用objects.update获取geometry,objects.update这个方法里面会为每个attribute都创建一个缓冲区,并设置缓冲区数据。如果geometry的attributes中的某个attribute有更新,会更新缓冲区数据。
然后判断object是否在相机的视椎体中,即能否被相机看见,如果可以,就加入到currentRenderList中。再递归调用这个方法,对object的children也执行该操作。
js
currentRenderList.finish();
if ( _this.sortObjects === true ) {
currentRenderList.sort( _opaqueSort, _transparentSort );
}
将上一次渲染中currentRenderList中多余的元素清空,给currentRenderList排序,这里可以设置自定义的排序方法,如果没设置,会根据groupOrder、renderOrder、材质id、投影后的z值依次比较排序。
js
const shadowsArray = currentRenderState.state.shadowsArray;
shadowMap.render( shadowsArray, scene, camera );
渲染shadowMap,将shadowMap通过渲染写在开启shadow的light对象的map中,shadowMap本质上就是一张深度图,记录着场景中在light的相机中看到的最小深度。这样在最终渲染时就可以依照深度对比来判断某一点是否在阴影内。
js
currentRenderState.setupLights();
这里会初始化一个currentRenderState中的WebGLLights类型的对象lights,这个lights对象会根据场景中的light的类型和属性值,初始化和更新与光照有关的uniforms数据。
js
if ( _renderBackground ) background.render( scene );
renderScene( currentRenderList, scene, camera );
清空上一帧的缓冲区,渲染场景。
js
function renderScene( currentRenderList, scene, camera, viewport ) {
const opaqueObjects = currentRenderList.opaque;
const transmissiveObjects = currentRenderList.transmissive;
const transparentObjects = currentRenderList.transparent;
currentRenderState.setupLightsView( camera );
if ( _clippingEnabled === true ) clipping.setGlobalState( _this.clippingPlanes, camera );
if ( viewport ) state.viewport( _currentViewport.copy( viewport ) );
if ( opaqueObjects.length > 0 ) renderObjects( opaqueObjects, scene, camera );
if ( transmissiveObjects.length > 0 ) renderObjects( transmissiveObjects, scene, camera );
if ( transparentObjects.length > 0 ) renderObjects( transparentObjects, scene, camera );
// Ensure depth buffer writing is enabled so it can be cleared on next render
state.buffers.depth.setTest( true );
state.buffers.depth.setMask( true );
state.buffers.color.setMask( true );
state.setPolygonOffset( false );
}
这是renderScene函数的源码,currentRenderList分为opaque、transmissive、transparent分别调用renderObjects函数渲染,本文只关注opaque(不透明)类型。renderObjects函数遍历currentRenderList,每个元素都存储着object、geometry、material、group(几何体顶点的群组编号),每个元素都调用renderObject函数。
js
function renderObject( object, scene, camera, geometry, material, group ) {
object.onBeforeRender( _this, scene, camera, geometry, material, group );
object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
object.normalMatrix.getNormalMatrix( object.modelViewMatrix );
material.onBeforeRender( _this, scene, camera, geometry, object, group );
if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {
material.side = BackSide;
material.needsUpdate = true;
_this.renderBufferDirect( camera, scene, geometry, material, object, group );
material.side = FrontSide;
material.needsUpdate = true;
_this.renderBufferDirect( camera, scene, geometry, material, object, group );
material.side = DoubleSide;
} else {
_this.renderBufferDirect( camera, scene, geometry, material, object, group );
}
object.onAfterRender( _this, scene, camera, geometry, material, group );
}
这是renderObject函数的源码,开始先调用object的onBeforeRender,然后计算object的模型视图矩阵(modelViewMatrix)和法线矩阵(normalMatrix),modelViewMatrix是模型顶点从模型的局部空间变换到视图空间的矩阵,normalMatrix是模型顶点的法线从模型的局部空间变换到视图空间的矩阵。normalMatrix是modelViewMatrix的逆转置矩阵。然后调用renderBufferDirect方法渲染到帧缓冲区。最后调用object的onAfterRender。
js
this.renderBufferDirect = function ( camera, scene, geometry, material, object, group ) {
if ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null)
const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 );
const program = setProgram( camera, scene, geometry, material, object );
state.setMaterial( material, frontFaceCW );
//
let index = geometry.index;
let rangeFactor = 1;
if ( material.wireframe === true ) {
index = geometries.getWireframeAttribute( geometry );
if ( index === undefined ) return;
rangeFactor = 2;
}
//
const drawRange = geometry.drawRange;
const position = geometry.attributes.position;
let drawStart = drawRange.start * rangeFactor;
let drawEnd = ( drawRange.start + drawRange.count ) * rangeFactor;
if ( group !== null ) {
drawStart = Math.max( drawStart, group.start * rangeFactor );
drawEnd = Math.min( drawEnd, ( group.start + group.count ) * rangeFactor );
}
if ( index !== null ) {
drawStart = Math.max( drawStart, 0 );
drawEnd = Math.min( drawEnd, index.count );
} else if ( position !== undefined && position !== null ) {
drawStart = Math.max( drawStart, 0 );
drawEnd = Math.min( drawEnd, position.count );
}
const drawCount = drawEnd - drawStart;
if ( drawCount < 0 || drawCount === Infinity ) return;
//
bindingStates.setup( object, material, program, geometry, index );
let attribute;
let renderer = bufferRenderer;
if ( index !== null ) {
attribute = attributes.get( index );
renderer = indexedBufferRenderer;
renderer.setIndex( attribute );
}
//
if ( object.isMesh ) {
if ( material.wireframe === true ) {
state.setLineWidth( material.wireframeLinewidth * getTargetPixelRatio() );
renderer.setMode( _gl.LINES );
} else {
renderer.setMode( _gl.TRIANGLES );
}
} else if ( object.isLine ) {
let lineWidth = material.linewidth;
if ( lineWidth === undefined ) lineWidth = 1; // Not using Line*Material
state.setLineWidth( lineWidth * getTargetPixelRatio() );
if ( object.isLineSegments ) {
renderer.setMode( _gl.LINES );
} else if ( object.isLineLoop ) {
renderer.setMode( _gl.LINE_LOOP );
} else {
renderer.setMode( _gl.LINE_STRIP );
}
} else if ( object.isPoints ) {
renderer.setMode( _gl.POINTS );
} else if ( object.isSprite ) {
renderer.setMode( _gl.TRIANGLES );
}
if ( object.isBatchedMesh ) {
if ( object._multiDrawInstances !== null ) {
// @deprecated, r174
warnOnce( 'THREE.WebGLRenderer: renderMultiDrawInstances has been deprecated and will be removed in r184. Append to renderMultiDraw arguments and use indirection.' );
renderer.renderMultiDrawInstances( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances );
} else {
if ( ! extensions.get( 'WEBGL_multi_draw' ) ) {
const starts = object._multiDrawStarts;
const counts = object._multiDrawCounts;
const drawCount = object._multiDrawCount;
const bytesPerElement = index ? attributes.get( index ).bytesPerElement : 1;
const uniforms = properties.get( material ).currentProgram.getUniforms();
for ( let i = 0; i < drawCount; i ++ ) {
uniforms.setValue( _gl, '_gl_DrawID', i );
renderer.render( starts[ i ] / bytesPerElement, counts[ i ] );
}
} else {
renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );
}
}
} else if ( object.isInstancedMesh ) {
renderer.renderInstances( drawStart, drawCount, object.count );
} else if ( geometry.isInstancedBufferGeometry ) {
const maxInstanceCount = geometry._maxInstanceCount !== undefined ? geometry._maxInstanceCount : Infinity;
const instanceCount = Math.min( geometry.instanceCount, maxInstanceCount );
renderer.renderInstances( drawStart, drawCount, instanceCount );
} else {
renderer.render( drawStart, drawCount );
}
};
renderBufferDirect方法的功能是渲染每个object。首先调用setProgram函数设置并获取一个Program对象,这里不再贴setProgram源码了,大体总结下,这个函数做了以下操作:
- 根据material的类型获取着色器代码模版。
- 根据material属性值,在着色器代码上拼接不同的宏定义。
- 编译着色器,创建WebGL的program对象。
- 复用相同的program,不重复编译。
- 根据material的uniforms字段值,设置program中的uniform变量值。
接下来执行setMaterial方法,上文介绍了,不再赘述。
然后重要的一步就是执行bindingStates.setup方法,这里也不在贴源码了,这个方法的作用是为着色器中的attribute变量设置启用状态和访问方式,使得着色器可以从缓冲器中找到数据。具体的如果属性在geometry的attributes中没有该属性,则不启用,设置默认值,即调用gl.vertexAttrib*。如果属性启用,则调用gl.vertexAttribPointer和gl.enableVertexAttribArray。
最后就是根据有没有geometry.index选择bufferRenderer还是indexedBufferRenderer来进行最终绘制,bufferRenderer使用gl.drawArrays,indexedBufferRenderer使用gl.drawElements。根据object类型选择图元类型,根据geometry.drawRange计算顶点(索引)数量和初始的偏移量。
至此,WebGLRenderer的render方法的主体流程就分析到这里。作为框架的渲染器,事无巨细介绍WebGLRenderer的源码远远超过一篇文章的篇幅,所以这篇文章不会逐行介绍所有源码,而是尽量聚焦主要的渲染流程,主要目的是对Three.js的渲染器工作原理有一个整体大致的认识,也是为理解Three.js其他部分的源码打基础。对于一些分支话题也做了适当的简化省略,笔者计划将在后续的文章中对这些话题做专题介绍。