3ds max sdk导出插件编写的心得
作者:yhchinabest
来自:CG先生-3D图形插件开发网http://www.cgsir.com

写在前面

为什么要写这个心得?去年11月份的时候我写过一篇3ds Max导出程序的一些尝试,抱着学习的态度把一些心得发到网上供大家参考,不过当时写的还很不完善,很多东西没说清楚。今年6月份开始又做了3ds Max导出导入程序的一些研究,感觉3ds Max SDK实在是博大精深,初学者入门还是很不方便,所以觉得以前发的心得应该得到补充,因而写了这样一个导出程序介绍,还是抱着学习的态度,不过还是希望能够对大家有所帮助。

由于时间精力有限,只写出导出程序的一些体会,以后会写出导入程序的体会。希望大家多批评指教。
环境配置

步骤1.首先你得有VS2005,3ds Max 9,如果有就好办了,否则想办法搞到手吧,在中国做到这点应该不难。至于其他相近版本的IDE和MAX,情况基本类似。

步骤2.在3ds Max9 SDK/maxsdk/howto/3dsmaxPluginWizard中有个readme.txt,它会向你介绍如何配置3ds Max9 plugin的向导。

步骤3.启动vs2005,新建Visual C++项目,如果在右侧的模板组中能够找到”3dsmaxPluginWizard”,并且选择后能够弹出欢迎界面,说面配置已经成功了。


第一个导出程序

这里仅仅是为了让大家更好的了解导出插件是如何工作的,所以什么都不导出,做个测试而已。

1.       在plugin Type中选择File Export。选择下一步,然后给你的导出类起个名字,比如”MyExport”。选择下一步,再输入你的MAXSDK路径,插件存放的路径,3dsmax.exe存放路径。然后Finish。

2.       找到class MyExport的函数const TCHAR *MyExport::Ext(int n)定义。该函数用来显示导出文件的扩展名,改一下,例如return _T(“My3D”)。

3.       再找到const TCHAR *MyExport:: ShortDesc ()的定义,该函数显示插件的描述信息,也改一下,例如return _T(“MyExportPlugin”)

4.       为了了解导出程序的入口,在函数DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)内添加:

AllocConsole();

_cprintf( "Export Begin/n" );//记得#include <conio.h>

生成并调试你的插件,系统会执行3dsmax.exe以启动3ds Max,然后选择“文件”->”导出”,希望你能看到”MyExportPlugin (*.My3D)”的选项,然后随便敲个什么名字,“确定”,如果能看到控制台输出”Export Begin”,那么第一个导出程序的实验便成功了。当然,你一定不会对导出文件的描述字符串和扩展名感兴趣,那么请你重点关注DoExport这个函数,特别是ExpInterface *ei这个参数,请在3ds Max SDK中查阅ExpInterface的相关信息,下一章将会使用到它。另外,你应该已经了解到,导出程序是从DoExport这个函数开始的。


Mesh,Material,Light,Camera,让我们找到它们

1.  首先说说上一章提到的ExpInterface,在3ds Max9的SDK中找到它,可以看到,它继承于MaxHeapOperators,并包含IScene *theScene。按照它的描述,thsScene是用来枚举场景中所有node的。看来这个node就是我们要寻找的对象。先不急着看node,先来看看IScene *theScene。

2.  IScene有个重要的函数: int EnumTree ( ITreeEnumProc *proc )。看看这个函数的描述:

Remarks:

Implemented by the System..
This may be called to enumerate every INode in the scene. The callback may flag any of these nodes (using INode::FlagForeground()).

Parameters:

ITreeEnumProc *proc
This callback object is called once for each INode in the scene.

Returns:

Nonzero if the process was aborted by the callback (TREE_ABORT); otherwise 0.

可以看出,这个函数会被系统自动调用。它会枚举场景中的每个结点。对每个结点,它再调用ITreeEnumProc *proc,估计这个proc就是用来解析每个结点的东西。

3.  再看看ITreeEnmuProc的描述:

Description:

This is the callback object used by IScene::EnumTree(). To use it, derive a class from this class, and implement the callback method.

它有个一个成员函数int callback(INode *node)看来我们需要的就是它了,这个函数会让系统传给你你要的node,你来实现这个callback函数。

4.    看来我们要写些代码了(我估计你也早等不及了),让我们写一个继承ITreeEnumProc的类:

class MyTreeEnum : public ITreeEnumProc

{

public:

MyTreeEnum(void);

~MyTreeEnum(void);

public:

int callback( INode *node );

};

然后实现int callback( INode *node ):

int MyTreeEnum::callback(INode *node)

{

ObjectState os = node->EvalWorldState(10);

if ( os.obj->CanConvertToType( Class_ID(TRIOBJ_CLASS_ID, 0) ) )

{

_cprintf( "TRIOBJECT %s/n", node->GetName());

Mtl *pMtl = node->GetMtl();

if ( pMtl )

{

_cprintf( "MATERIAL %s/n",pMtl->GetName() );

}

return TREE_CONTINUE;

}

if (os.obj)

{

switch(os.obj->SuperClassID())

{

case CAMERA_CLASS_ID:

_cprintf( "CAMERA %s/n", node->GetName());

break;

case LIGHT_CLASS_ID:

_cprintf( "LIGHT %s/n", node->GetName());

break;

}

}

return TREE_CONTINUE;

}

接着,让我们调用这个函数,这只需要修改DoExport()函数

int  MaxExportTest::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options)

{

MyTreeEnum tempProc;

ei->theScene->EnumTree( &tempProc );

return TRUE;

}

最后,编译它,开始调试,找一个有物体,材质,灯光,摄像机的场景进行导出,如果你能在控制台输出窗口看到每个结点的名字,说明你的代码成功了。

5.    让我们再来看这些代码,首先来关注INode,根据3ds max sdk的说明,INode是场景中每个结点的接口,它可以代表不同类型的物体,如几何体,灯光,摄像机。要访问这些物体,你就得调用INode::EvalWorldState,它会返回一个ObjectState,这个ObjectState又包含一个Object *obj,越说越复杂了,这两个类型都很重要,但我们现在只需要这个obj来帮助我们判断当前传入的node是属于什么类型。这就需要两个函数,canConvertToType()和SuperClassID(),它们是obj的成员函数。在这之前,先看看Super Class ID和Class ID,这是Super Class ID的一段摘要:

GEOMOBJECT_CLASS_ID - Used by geometric objects.

CAMERA_CLASS_ID - Used by plug-in cameras.

LIGHT_CLASS_ID - Used by plug-in lights.

SHAPE_CLASS_ID - Used by spline shapes.

HELPER_CLASS_ID - Used by helper objects.

SYSTEM_CLASS_ID - Used by system plug-ins.

OSM_CLASS_ID - Used by Object Space Modifiers.

WSM_CLASS_ID - Used by Space Warp Modifiers (World Space Modifiers).

这是Class ID的一段摘要:

Subclasses of GEOMOBJECT_CLASS_ID

Built into core

TRIOBJ_CLASS_ID - TriObject

PATCHOBJ_CLASS_ID - PatchObject

Subclasses of LIGHT_CLASS_ID:

OMNI_LIGHT_CLASS_ID - Omni Light

SPOT_LIGHT_CLASS_ID - Spot Light

DIR_LIGHT_CLASS_ID - Directional Light

FSPOT_LIGHT_CLASS_ID - Free Spot Light

TDIR_LIGHT_CLASS_ID - Target Directional Light

由此可见,Class ID 应该是Super Class ID的一个子集,比如要判断是否是灯光,只要看它的Super Class ID是否是LIGHT_CLASS_ID,函数SuperClassID()可以达到这个目的。而要看它具体是哪种灯光,就需要canConvertToType函数了。不过让我不解的是,摄像机和灯光的目标节点也被归为GEOMOBJECT_CLASS_ID了,我不知道3ds Max为什么要这样设计,所以我只好用canConverToType来判断这个物体是否为三角网物体。

 

6.    好了,我们大概找到了我们需要的东西,下一章,我会示范如何从这些较大的范围中得到我所感兴趣的具体的信息,如灯光的位置和方向,以及最重要的Mesh的顶点信息等。


Mesh,Material,Light,Camera,让我们解析他们

1.         首先说说mesh和material吧,这两者结合相当密切。上一章说到如何获得TriObject,通过它可以获得一个mesh:

Mesh* pMesh = &tri->GetMesh();

在SDK里查看mesh的描述,发现它可以导出很多信息,而我们一般希望从mesh中获得顶点坐标,法线向量,纹理坐标,顶点颜色等信息,以及顶点的索引值。对于只贴了一个纹理的mesh,我们可以简单的获得这些信息。

Mesh* pMesh = &tri->GetMesh();

int VerticesNum = pMesh->getNumVerts()

for ( int i=0; i<VerticesNum; i++ )

{

Point3 Coord, Normal, TCoord, VColor;

if( pMesh->getNumVerts()>0 ) //导出顶点坐标

{

Coord = pMesh->getVert( i );

}

if ( pMesh->faces )          //导出法线向量

{

Normal = pMesh->getNormal( j );

}

if ( pMesh->getNumTVerts()>0 )//导出纹理坐标

{

TCoord = pMesh->tVerts[j] ;

}

if ( pMesh-> vertCol ) //导出顶点颜色

{

VColor = pMesh->vertCol[i];

}

}

然后是这个mesh使用的纹理,这里仅列举漫反射贴图:

Mtl *pMtl = pNode->GetNode

if ( pMtl!=NULL )

{

Texmap *pTexMap = pMtl->GetSubTexmap(ID_DI); //获取漫反射材质的贴图

BitmapTex *pBMPTex = (BitmapTex *) pTexMap;

if ( pBMPTex )

{

char *MapName = pBMPTex->GetMapName();    //获取漫反射贴图的路径

}

}

而对于贴了多个纹理的情况,就要复杂的多,例如一个立方体,每个面都贴了一个纹理,那么就需要知道这个mesh面的数量,材质的数量,面和材质的对应关系,面和顶点的对应关系,等等。我设计了一种解析方法,经过一些模型的测试,结果正确,拿出来供大家参考参考。

先说说具体思想,假设一个立方体每个面都贴了一张纹理,那么可以把这个mesh看作划分了6个子mesh,一个子mesh就是一个面。首先,遍历原来mesh的所有面,计算非重复的材质ID。从而得知这个mesh的子mesh个数。然后再次遍历原来mesh所有的面,将所有具有相同材质ID的面的顶点集合到一个子mesh,这些顶点只存储该顶点在原mesh中的索引。当然,需要重新计算这些顶点在子mesh里的索引值。因此再遍历原mesh的所有面。

struct FaceVertex

{

int m_Index;     //表示该顶点在原mesh里的索引值

int m_FaceIndex; //表示该顶点所属的面在原mesh里的索引值

int m_TriIndex;  //表示该顶点在所属三角形里的索引值,值为0,1,2

bool operator == (const FaceVertex &refVertex)

{

if ( m_Index == refVertex.m_Index )

{

return true;

}

else

{

return false;

}

}

};

class MaxDivideMesh

{

public:

vector<FaceVertex> m_VertexArray;

vector<int> m_IndexArray;

};

void MyTreeEnum::CreateMutilMesh(  INode *pNode, Mesh *pMesh, Mtl *pMtl )

{

vector <int> MeshMtls; //该Mesh用到的子材质的数量,用来计算子Mesh的划分

//每个元素表示一个材质ID。

for( int i=0; i<pMesh->getNumFaces(); i++ )

{

/*计算子Mesh数量,通过计算所有面使用的非重复材质数量而得*/

int MatID = pMesh->getFaceMtlIndex(i);

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

if ( MatIndex == MeshMtls.end() )

{

MeshMtls.push_back(MatID );//该材质未在MeshMtls里出现过,说明是个

//新材质

}

}

//DivideMeshArray,计算Mesh划分的拓扑信息

vector<MaxDivideMesh> DivideMeshArray;

DivideMeshArray.resize( MeshMtls.size() );//指定划分数量

//

//此处有内存的分配

//GMeshD3D是我自己设计的一个类型,用来表示一个子Mesh

//GTextureD3D用来表示Texture

GMeshD3D *pMeshArray = new GMeshD3D[MeshMtls.size()];

GTextureD3D *pTextureArray = new GTextureD3D[MeshMtls.size()];

GObjectMAXD3D tempObj; //GObjectMAXD3D表示一个模型,有n个mesh和texture组成

tempObj.SetMeshNum( MeshMtls.size() );

tempObj.SetTextureNum( MeshMtls.size() );

for ( int i=0; i<MeshMtls.size(); i++ )

{

if ( pMtl!=NULL )

{

Mtl *pSubMtl = pMtl->GetSubMtl( MeshMtls[i] );

Texmap *pTexMap = pSubMtl->GetSubTexmap(ID_DI); //获取漫反射材质的贴//图

BitmapTex *pBMPTex = (BitmapTex *) pTexMap;

if ( pBMPTex )

{

char *MapName = pBMPTex->GetMapName();    //获取漫反射贴图名称

if ( MapName!=NULL )

{

pTextureArray[i].SetMapName(MapName);

}

}

pMeshArray[i].m_MatID = i;

tempObj.SetTexture( &pTextureArray[i], i );

}

}

/*这里开始对原有mesh进行重新划分*/

for( int i=0; i<pMesh->getNumFaces(); i++ )

{

int MatID = pMesh->getFaceMtlIndex(i); //计算该面的材质ID

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

int MeshID = MatIndex - MeshMtls.begin(); //计算该MatID在TextureArray的纹理索引,使MeshID从0开始编号

for ( int j=0; j<3; j++)

{

int Index = pMesh->faces[i].v[j];//Index表示在全局顶点数组里的索引

FaceVertex tempVertex;

tempVertex.m_Index = Index;

vector<FaceVertex>::iterator VertexIter = find( DivideMeshArray[MeshID].m_VertexArray.begin(),

DivideMeshArray[MeshID].m_VertexArray.end(), tempVertex );

if ( VertexIter == DivideMeshArray[MeshID].m_VertexArray.end() )

//在DivideMeshArray里寻找顶点索引值相同的顶点,如果没找到该顶点,表示//要添加该顶点

{

int VertexIndex = VertexIter - DivideMeshArray[MeshID].m_VertexArray.begin();

FaceVertex tempFVertex;

tempFVertex.m_Index = Index;

tempFVertex.m_FaceIndex = i;

tempFVertex.m_TriIndex = j;

DivideMeshArray[MeshID].m_VertexArray.push_back( tempFVertex );

}

}

}

/*计算顶点在每个子mesh中的索引*/

for( int i=0; i<pMesh->getNumFaces(); i++ )

{

int MatID = pMesh->getFaceMtlIndex(i);

vector<int>::iterator MatIndex = find( MeshMtls.begin(), MeshMtls.end(), MatID );

int MeshID = MatIndex - MeshMtls.begin(); //计算该MatID的纹理索引

for ( int j=0; j<3; j++)

{

int Index = pMesh->faces[i].v[j];

FaceVertex tempVertex;

tempVertex.m_Index = Index;

//若在子mesh里能找到该点,则计算该点在子mesh的索引

vector<FaceVertex>::iterator VertexIter = find( DivideMeshArray[MeshID].m_VertexArray.begin(), DivideMeshArray[MeshID].m_VertexArray.end(), tempVertex );

if ( VertexIter != DivideMeshArray[MeshID].m_VertexArray.end() )

{

int VertexIndex = VertexIter - DivideMeshArray[MeshID].m_VertexArray.begin();//VertexIndex表//示该顶点在子Mesh的索引

DivideMeshArray[MeshID].m_IndexArray.push_back( VertexIndex );

}

}

}

/*余下部分开始到处顶点信息*/

for ( int i=0; i<MeshMtls.size(); i++ )

{

pMeshArray[i].SetFVF( GFVF );

pMeshArray[i].SetVerticeNum( DivideMeshArray[i].m_VertexArray.size() );

int VerticesNum;

pMeshArray[i].GetVerticeNum( VerticesNum );

for ( int j=0; j<VerticesNum; j++ )

{

GVertex tempVertex;

Point3 Coord,Normal,TCoord;

if( pMesh->getNumVerts()>0 ) //导出顶点坐标

{

int index = DivideMeshArray[i].m_VertexArray[j].m_Index;

Coord = pMesh->getVert(

DivideMeshArray[i].m_VertexArray[j].m_Index );

tempVertex.PosCoord = D3DXVECTOR3( Coord.x, Coord.y, -Coord.z );

}

if ( pMesh->faces )          //导出法线向量

{

Normal = pMesh->getNormal( DivideMeshArray[i].m_VertexArray[j].m_Index );

tempVertex.NormalVector = D3DXVECTOR3( Normal.x, Normal.y, Normal.z );

}

if ( pMesh->getNumTVerts()>0 )//导出纹理坐标

{

FaceVertex tempFVertex = DivideMeshArray[i].m_VertexArray[j];

TCoord = pMesh->tVerts[pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex)] ;

int TCoordIndex = pMesh->tvFace[tempFVertex.m_FaceIndex].getTVert(tempFVertex.m_TriIndex);

_cprintf( "TextureCoord Index%d/n", TCoordIndex );

tempVertex.TexCoord = D3DXVECTOR2( TCoord.x, TCoord.y );

}

DWORD VColor = 0xffffffff;

tempVertex.Color = VColor;

pMeshArray[i].SetVertex( tempVertex, j );

}

pMeshArray[i].SetFaceNum( DivideMeshArray[i].m_IndexArray.size()/3 );

WORD FaceNum;

pMeshArray[i].GetFaceNum( FaceNum );

for ( int j=0; j<FaceNum; j++ )

{

pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3], j*3 );

pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3+2], j*3+1 );

pMeshArray[i].SetIndex( DivideMeshArray[i].m_IndexArray[j*3+1], j*3+2 );

}

}

tempObj.WritetoFile();

delete []pMeshArray;

delete []pTextureArray;

}

2.       导出摄像机,我们一般关注摄像机的位置,方向,FOV角,近远平面之类的信息。还好3ds Max SDK 里提供CameraObject,CameraState,Camera,GenCamera等对象来访问这些信息。不过摄像机的位置和方向等信息需要通过节点函数来访问。比较简单,直接见代码。

void MyTreeEnum::CreateCamera( INode *pNode )

{

ObjectState os = pNode->EvalWorldState( 10 );

CameraObject* CameraObj = (CameraObject*)os.obj;

struct CameraState cs;

Interval valid = FOREVER;

CameraObj->EvalCameraState( 10, valid, &cs );

Matrix3 SourceMat = pNode->GetNodeTM( 10 );//获取摄像机源点的变换矩阵

Matrix3 destMatrix;

pNode->GetTargetTM( 0, destMatrix ); //获取摄像机目标点的变换矩阵

cs.fov;      //获取FOV角

cs.hither;   //获取摄像机近平面

cs.yon;      //获取摄像机远平面

}

3.       导出灯光,与摄像机差不多,也是位置和方向需要结点函数来获得,而其余信息通过访问Light,LightState,GenLight,LightObject获得。

void MyTreeEnum::CreateLight( INode *pNode )

{

ObjectState os = pNode->EvalWorldState(10);

GenLight* light = (GenLight*)os.obj;

struct LightState ls;

Interval valid = FOREVER;

light->EvalLightState( 10, valid, &ls );

Matrix3 SourceMat = pNode->GetNodeTM( 10 );

Matrix3 TargetMat;

pNode->GetTargetTM( 10, TargetMat );

float Theta, Phi;

Theta = ls.hotsize;

Phi = ls.fallsize;

switch(ls.type) //导出灯光类型

{

case OMNI_LIGHT:  _cprintf( "%s/n", "ID_LIGHT_TYPE_OMNI" ); break;

case TSPOT_LIGHT: _cprintf( "%s/n", "ID_LIGHT_TYPE_TARG" ); break;

case DIR_LIGHT:   _cprintf( "%s/n", "ID_LIGHT_TYPE_DIR" );  break;

case FSPOT_LIGHT: _cprintf( "%s/n", "ID_LIGHT_TYPE_FREE" ); break;

}

}

呼。。。。。终于写完了,感觉还是很多东西没说清楚。希望大家多学学3ds Max SDK,给我多提出意见,呵呵。

3ds max sdk导出插件编写的心得相关推荐

  1. 3D MAX导出插件编写

    文章版权归博客园 BigCoder所有,转载请于明显位置标明原文作者及出处,以示尊重!! 原文出处:http://www.cnblogs.com/csyisong/archive/2009/09/01 ...

  2. 3D MAX导出插件编写I

    3D MAX导出插件编写I 想想研究3D MAX 的SDK已经有了不短的时间,真正算起来也有两个月了吧,但是讲到收获,确实不大.作为一个3D MAX二次开发的学习者,我首先学习了导出插件的编写,网上有 ...

  3. MAX SDK之插件概述(一)

    一.MAX 插件概述 1.1 插件的功能 所谓插件,就是开发者自己开发的一组程序用以扩展MAX的功能.如果使用过3DS MAX,我们会发现,MAX的每组功能都封装在某一类插件中,如创建物体,修改器等等 ...

  4. 3ds max文件导出osg或者ive格式

    osg/osgEarth系列文章目录 文章目录 osg/osgEarth系列文章目录 前言 参考 前言 首先下载插件osgexp Osgexp的下载地址 安装上之后,如果3ds max导出里面已经可以 ...

  5. 3DsMax导出插件编写(一)——vs2010和3dsmax2011(64位)的配置方法

    作为3D开发人员,特别是3D引擎开发人员,经常会接触到各种三维模型的数据.虽然说3dsmax已经可以导出很多格式的模型,不过总是不一定合适自己用.所以总有一种想自己写一个导出插件,让3dmax导出自己 ...

  6. 炫云云渲染3ds max支持的插件有哪些?

    很多人都想知道炫云3ds max都支持哪些插件,下面就为大家介绍下. 炫云支持的插件有SigerScratches.ThinFilm.tyfolw.Anima.Autograss.Autohedge. ...

  7. 3ds Max模型导出/转换为Revit族文件(可编辑材质)

    目标 将3ds Max模型中的整体或局部模型导出/转换为Revit族文件,族文件的每部分支持单独设置及编辑材质. → 步骤 1.3ds Max端操作 (1)将需转换模型导出为3ds格式文件 选中需转换 ...

  8. png在ai转为路径_AI路径导入3ds Max并导出图像

    在使用3ds Max的过程中有时会用到比较复杂的路径,虽然它本身的路径功能很齐全,但对于我来说,它操作起来很别扭,不顺手. 相比之下,AI 的路径功能比较强大,且操作起来很方便.所以我一般是在AI画好 ...

  9. 3DsMax导出插件编写(三)——使用IGame收集模型信息

    之前介绍过用SDK的常规方法来获取模型的网格信息.这里再介绍另外一种方法. MaxSdk里面带了一个叫做IGame的包,里面包含有很多方便我们获取模型信息的方法.在sdk的自带例子里面,同样也有这个I ...

最新文章

  1. 总结个人项目设计保障5大原则
  2. Web开发工具包收藏
  3. 使用Mockito模拟自动装配的字段
  4. .net framework4与其client profile版本的区别
  5. Eclipse安装lombook
  6. Hands-on Lab (15) - 使用Prometheus Operator监控应用
  7. 2018程序员必读书单
  8. 16 | 把大象装进冰箱:HTTP传输大文件的方法
  9. web安全攻防渗透+赵雨佳43
  10. QGIS加载在线地图:高德、天地图等
  11. girl_noise.jpg恢复去噪
  12. jQuery如何根据元素值删除数组元素
  13. 斐波那契堆的实现和比较(相对二项堆)
  14. Mybatis--动态sql之choose、when、otherwise语句(只匹配其中的一个条件)
  15. 【立创开发板】梁山派初体验
  16. keil遇到FCARM - Output Name not specified, please check 'Options for Target - Utilities'解决方法
  17. 解决ReactNative老项目跑不起来的问题
  18. 2021 最新分享 Java 面试题库万字精华 github 上标星 80
  19. 【誉天】这几道华为云计算认证题,你答对了
  20. 学生怎么在网上赚钱,想赚钱就要学会这些!

热门文章

  1. 洛谷——P2006 赵神牛的游戏
  2. 字符串型String
  3. MySQL二十八规范数据库设计
  4. JUC本质解析+进程/线程
  5. 通过源码理解反射与注解是什么东西?
  6. Qt之QListView使用
  7. input file控件使用accept过滤 限制的文件类型
  8. 2引擎帮助文档_使用Sentence Transformers和Faiss构建语义搜索引擎
  9. Shell脚本学习-阶段七-信息过滤磁盘分区
  10. Linux命令解释之chown