个人感觉assimp存在bug,打算替换至tinygltf,所以备份一下代码。

/**
* @file     DND.ModelLoader.ixx
* @brief    模型加载器,基于Assimp库
*
*
* @version  1.0
* @author   lveyou
* @date     22-09-02
*
* @note     由于Assimp库过于庞大,我们只在windows平台使用动态链接库
* @note     Assimp默认为右手坐标系(+X 指向右侧,+Y 指向上方,+Z 指向屏幕外朝向观看者)
* @note     逆时针顶点绕序为正面
* @note     行优先矩阵
*/
#ifdef WIN32module;
#include <assimp/Importer.hpp>      //C++导入接口
#include <assimp/scene.h>           //输出数据结构
#include <assimp/postprocess.h>     //后处理标志
#include <assimp/IOStream.hpp>
#include <assimp/IOSystem.hpp>
#include <assimp/Logger.hpp>
#include <assimp/DefaultLogger.hpp>
#include <assimp/Exceptional.h>       //异常处理
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>#include "DND.h"
export module DND.ModelLoader;import DND.Std;
import DND.Type;
import DND.ModelObject;
import DND.Debug;
import DND.String;
import DND.Image;
import DND.File;
import DND.Factory;
import DND.BoneAnimation;
import DND.Math;namespace dnd
{static glm::vec3 vec3_cast(const aiVector3D& v) { return glm::vec3(v.x, v.y, v.z); }
static glm::vec2 vec2_cast(const aiVector3D& v) { return glm::vec2(v.x, v.y); }
static glm::quat quat_cast(const aiQuaternion& q) { return glm::quat(q.w, q.x, q.y, q.z); }
static glm::mat4 mat4_cast(const aiMatrix4x4& m) { return glm::transpose(glm::make_mat4(&m.a1)); }
static glm::mat4 mat4_cast(const aiMatrix3x3& m) { return glm::transpose(glm::make_mat3(&m.a1)); }}DND_NAMESPACE_EXPORTusing namespace Assimp;constexpr uint8_t BONE_ID_NONE = (uint8_t)255;class ModelLoaderLogStream : public LogStream
{
public://日志void write(const char* message) {g_debug.Write(format("Assimp:{}", message));}
};class ModelLoader
{public:ModelLoader(){//日志等级const unsigned int severity = Logger::Debugging | Logger::Info | Logger::Err | Logger::Warn;DefaultLogger::create("", Logger::VERBOSE);//附加到默认loggerDefaultLogger::get()->attachStream(new ModelLoaderLogStream, severity);}~ModelLoader(){DefaultLogger::kill();}vector<ModelObject*> Load(string_view path_name, string_view res_name){string str_path = string{ path_name };string str_base = String::EraseFilename(path_name);debug(format("开始加载模型文件:{},{}", str_path, str_base));//创建导入类实例Assimp::Importer importer;const aiScene* scene = importer.ReadFile(str_path,aiProcess_CalcTangentSpace |aiProcess_Triangulate |aiProcess_JoinIdenticalVertices |aiProcess_SortByPType |aiProcess_FlipUVs);//后处理标记//aiProcess_MakeLeftHanded 可修改为左手坐标系//aiProcess_FlipWindingOrder 修改为顺时针为正面//aiProcess_FlipUVs 默认左下角,设置后为左上角//aiProcess_GenNormals 生成法线//aiProcess_LimitBoneWeights 限制骨骼权重数为4(这个有bug)//aiProcess_FindInvalidData 除错一些数据//aiProcess_SplitByBoneCount 分割具有多个骨骼的网格//aiProcess_GlobalScale 执行全局缩放//aiProcess_EmbedTextures 将纹理变为嵌入形式,文件不存在,还会检查根目录同名文件if (!scene){debug_err(format("导入场景失败:{}", importer.GetErrorString()));return {};}//debug_err(importer.GetErrorString());debug_msg(format("打开模型文件成功,开始加载模型:{}", path_name));debug(format("网格 数:{}", scene->mNumMeshes));debug(format("材质 数:{}", scene->mNumMaterials));debug(format("贴图 数:{}", scene->mNumTextures));debug(format("动画 数:{}", scene->mNumAnimations));//有动画则假设有骨骼bool has_bone = scene->mNumAnimations;//返回结构vector<ModelObject*> ret;ModelObject* model_object = new ModelObject;std::vector<Vertex>& mo_vertices =  model_object->_allVertex;std::vector<VertexBone>& mo_vertices_bone = model_object->_allVertexBone;std::vector<uint32_t>& mo_indices = model_object->_allIndex;vector<ModelObjectMaterial>& mo_material = model_object->_allMaterial;model_object->_boneData = has_bone ? new BoneData : nullptr;BoneData* bone_data = model_object->_boneData;ret.push_back(model_object);//读取材质(我们不读取DefaultMaterial)const char NAME_DEFAULT_MATERIAL[] = "DefaultMaterial";bool has_default_material = false;for (unsigned i = 0; i < scene->mNumMaterials; ++i){const aiMaterial* m = scene->mMaterials[i];if (strcmp(m->GetName().C_Str(), NAME_DEFAULT_MATERIAL) == 0){has_default_material = true;assert(i == 0);//它的id必为0continue;}unsigned count_diffuse = m->GetTextureCount(aiTextureType_DIFFUSE);unsigned count_normal = m->GetTextureCount(aiTextureType_NORMALS);aiColor3D col_diffuse;m->Get(AI_MATKEY_COLOR_DIFFUSE, col_diffuse);aiColor3D col_specular;m->Get(AI_MATKEY_COLOR_SPECULAR, col_specular);float shininess;m->Get(AI_MATKEY_SHININESS, shininess);//记录ModelObjectMaterial material;material._materialName = m->GetName().C_Str();material._material._diffuseAlbedo = { col_diffuse.r, col_diffuse.g, col_diffuse.b, 1.0f };material._material._fresnelR0 = { col_specular.r, col_specular.g, col_specular.b };material._material._roughness = ShininessToRoughness(shininess);if (count_diffuse){_read_texture(scene, res_name, m, aiTextureType_DIFFUSE, str_base, material._pathTexDiffuse);}if (count_normal){_read_texture(scene, res_name, m, aiTextureType_NORMALS, str_base, material._pathTexNormal);}for (unsigned j = 0; j < m->mNumProperties; ++j){const aiMaterialProperty* prop = m->mProperties[j];debug(format("材质属性:{},{}", j, prop->mKey.C_Str()));}PrintMaterial(material, i);mo_material.emplace_back(material);}//当前mesh的索引下标size_t index = 0;//当前mesh的起始顶点下标size_t offset_vertex = 0;//所有Node -> node_idunordered_map<const aiNode*, size_t> map_node;if (has_bone){//读取Node关系(aiNode和网格无关,只是层次关系,如果它是骨骼,则同名)//有名字的node,我们分配一个id//广度优先遍历list<aiNode*> all_node;//记录map_node[scene->mRootNode] = bone_data->_allNodeParent.size();size_t id_parent = -1;bone_data->_allNodeParent.push_back(id_parent);all_node.push_back(scene->mRootNode);//添加到栈顶//直至栈空while (!all_node.empty()){//得到栈顶aiNode* node = all_node.front();all_node.pop_front();assert(map_node.find(node) != map_node.end());size_t id_parent = map_node[node];//子节点,分配id,设置父节点id,并添加到栈尾for (unsigned i = 0; i < node->mNumChildren; ++i){aiNode* node_child = node->mChildren[i];//分配idmap_node[node_child] = bone_data->_allNodeParent.size();//指向父骨骼bone_data->_allNodeParent.push_back(id_parent);all_node.push_back(node_child);//添加到栈尾}}//父节点必须在前面
#ifndef NDEBUGfor (size_t i = 0; i < bone_data->_allNodeParent.size(); ++i){if (bone_data->_allNodeParent[i] != -1&& bone_data->_allNodeParent[i] >= i){//如果 父id 大于等于 自己debug_err(format("节点关系错误:自己{}先于父节点{}出现",i, bone_data->_allNodeParent[i]));}}
#endifbone_data->_allBoneOffset.resize(map_node.size(), glm::mat4(1.0f));//读取动画for (unsigned i = 0; i < scene->mNumAnimations; ++i){const aiAnimation* animation = scene->mAnimations[i];string ani_name = animation->mName.C_Str();if (ani_name.empty()){ani_name = format("{}#{}", res_name, i);debug_msg(format("动画名为空,生成名字为:{}", ani_name));}debug(format("#{}{:=^32}动画", i, ani_name));//每个Channel影响一个nodeassert(animation->mNumChannels && animation->mNumChannels <= map_node.size());debug(format("节点数:{}", animation->mNumChannels));double tick_per_second;if (animation->mTicksPerSecond)tick_per_second = animation->mTicksPerSecond;else{tick_per_second = 30;debug_warn("动画不存在tick每s值,将使用30!");}double ani_t = animation->mDuration / tick_per_second;debug(format("时长:{},{}/{}", ani_t, animation->mDuration, tick_per_second));//添加一个AnimationClipAnimationClip& ani_clip = bone_data->_allAnimation[ani_name];//以node的大小,而不是NumChannelsani_clip._allBoneKeyFrameMulti.resize(map_node.size());ani_clip._t0 = std::numeric_limits<real_time>::max();ani_clip._t1 = std::numeric_limits<real_time>::min();for (unsigned j = 0; j < animation->mNumChannels; ++j){const aiNodeAnim* node_ani = animation->mChannels[j];//找到node_idaiNode* node = scene->mRootNode->FindNode(node_ani->mNodeName);assert(node);assert(map_node.find(node) != map_node.end());//写入对应nodeBoneKeyFrameMulti& all_key_frame = ani_clip._allBoneKeyFrameMulti[map_node[node]];//取最大者unsigned num_key = max(max(node_ani->mNumPositionKeys,node_ani->mNumRotationKeys), node_ani->mNumRotationKeys);debug(format("节点,关键帧:{},{}", node_ani->mNodeName.C_Str(), num_key));all_key_frame._allKeyFrame.resize(num_key);for (unsigned x = 0; x < node_ani->mNumPositionKeys; ++x){BoneKeyFrame& key_frame = all_key_frame._allKeyFrame[x];const aiVectorKey& t = node_ani->mPositionKeys[x];if(key_frame._t < 0)key_frame._t = t.mTime / tick_per_second;key_frame._translation = { t.mValue.x, t.mValue.y, t.mValue.z };}for (unsigned x = 0; x < node_ani->mNumRotationKeys; ++x){BoneKeyFrame& key_frame = all_key_frame._allKeyFrame[x];const aiQuatKey& r = node_ani->mRotationKeys[x];if (key_frame._t < 0)key_frame._t = r.mTime / tick_per_second;key_frame._roationQuat = { r.mValue.w, r.mValue.x, r.mValue.y, r.mValue.z };}for (unsigned x = 0; x < node_ani->mNumScalingKeys; ++x){BoneKeyFrame& key_frame = all_key_frame._allKeyFrame[x];const aiVectorKey& s = node_ani->mScalingKeys[x];if (key_frame._t < 0)key_frame._t = s.mTime / tick_per_second;key_frame._scaling = { s.mValue.x, s.mValue.y, s.mValue.z };}
#ifndef NDEBUG//时间检查for (BoneKeyFrame& key_frame : all_key_frame._allKeyFrame){if (key_frame._t < 0){debug_err("有关键帧未读取到时间!");}}
#endif//取上下界作为动画时间BoneKeyFrame& kf_beg = all_key_frame._allKeyFrame.front();BoneKeyFrame& kf_end = all_key_frame._allKeyFrame.back();ani_clip._t0 = min(ani_clip._t0, kf_beg._t);ani_clip._t1 = max(ani_clip._t1, kf_end._t);}}//assert(bone_data->_allBoneParent.size() == bone_data->GetBoneSize());int pause = 3;}//所有mesh数据都在这里(由于mesh对应1个材质,所以简单生成sub即可for (unsigned i = 0; i < scene->mNumMeshes; ++i){aiMesh* mesh = scene->mMeshes[i];assert(mesh->HasPositions());bool has_uv = mesh->HasTextureCoords(0);bool has_normal = mesh->HasNormals();bool has_tangent = mesh->HasTangentsAndBitangents();const aiVector3D POS_ZERO = { 0,0,0 };for (unsigned j = 0; j < mesh->mNumVertices; ++j){const aiVector3D& pos = mesh->mVertices[j];const aiVector3D& uv = has_uv ? mesh->mTextureCoords[0][j] : POS_ZERO;const aiVector3D& normal = has_normal ? mesh->mNormals[j] : POS_ZERO;const aiVector3D& tangent = has_tangent ? mesh->mTangents[j] : POS_ZERO;if (has_bone){VertexBone v;v._pos = { pos.x, pos.y, pos.z };v._uv = { uv.x, uv.y };v._normal = { normal.x,normal.y,normal.z };v._tangent = { tangent.x, tangent.y, tangent.z };v._boneWeight = { 0, 0, 0 };for (uint8_t& iter : v._boneID)iter = BONE_ID_NONE;//最后需要转换为0mo_vertices_bone.emplace_back(v);}else{Vertex v;v._pos = { pos.x, pos.y, pos.z };v._uv = { uv.x, uv.y };v._normal = { normal.x,normal.y,normal.z };v._tangent = { tangent.x, tangent.y, tangent.z };mo_vertices.emplace_back(v);}}//以face读取则是索引(为mesh的索引,而不是整体)for (unsigned k = 0; k < mesh->mNumFaces; ++k){const aiFace& face = mesh->mFaces[k];assert(face.mNumIndices == 3);mo_indices.push_back((uint32_t)(face.mIndices[0] + offset_vertex));mo_indices.push_back((uint32_t)(face.mIndices[1] + offset_vertex));mo_indices.push_back((uint32_t)(face.mIndices[2] + offset_vertex));}ModelObjectSub sub;sub._range._offsetIndex = index;sub._range._countTriangle = mesh->mNumFaces;if (has_default_material)sub._indexMaterial = (size_t)mesh->mMaterialIndex - 1;elsesub._indexMaterial = mesh->mMaterialIndex;model_object->_allSub.push_back(sub);if (has_bone){//读取骨骼for (unsigned k = 0; k < mesh->mNumBones; ++k){const aiBone* bone = mesh->mBones[k];//找到node_idaiNode* node = scene->mRootNode->FindNode(bone->mName);assert(node);assert(map_node.find(node) != map_node.end());size_t node_id = map_node[node];debug(format("骨骼,下标,节点,顶点:{},{},{},{}",bone->mName.C_Str(), k, node_id, bone->mNumWeights));//记录偏移矩阵bone_data->_allBoneOffset[node_id] = mat4_cast(bone->mOffsetMatrix);//写入顶点骨骼相关数据for (unsigned m = 0; m < bone->mNumWeights; ++m){const aiVertexWeight& weight = bone->mWeights[m];if(weight.mWeight == 0)continue;//有可能它本身为0VertexBone& v = mo_vertices_bone[weight.mVertexId + offset_vertex];size_t n = 0;for (; n < NUM_BONE; ++n){//写入为255的位置if (v._boneID[n] == node_id)break;//有可能会重复(冗余)if (v._boneID[n] == BONE_ID_NONE)break;}if (n == NUM_BONE){//越界debug_warn(format("同一个顶点的骨骼数超过{},将忽略多余的!", NUM_BONE));continue;}if (n != NUM_BONE - 1){//不是最后一个位置才写入v._boneWeight[n] = weight.mWeight;}else{//最后一个进行求和验证
#ifndef NDEBUGfloat sum = v._boneWeight[0] + v._boneWeight[1]+ v._boneWeight[2] + weight.mWeight;if (abs(sum - 1.0f) > 0.1f){debug_err("骨骼权重和不为1!");Vector4 v_n = {v._boneWeight[0], v._boneWeight[1],v._boneWeight[2], weight.mWeight };Math::Normalize(v_n);v._boneWeight = glm::make_vec3(v_n.data());}
#endif}v._boneID[n] = node_id;}}}debug(format("#{}{:=^32}网格", i, mesh->mName.C_Str()));debug(format("面数,材质下标:{},{}", mesh->mNumFaces, mesh->mMaterialIndex));debug(format("顶点数:{}", mesh->mNumVertices, has_normal, has_tangent));debug(format("法线,切线:{},{}", has_normal, has_tangent));debug(format("起始,三角数:{},{}", index, mesh->mNumFaces));index = mo_indices.size();offset_vertex = has_bone ? mo_vertices_bone.size() : mo_vertices.size();}//记录到父节点矩阵if (has_bone){bone_data->_allNodeTrans2Parent.resize(map_node.size());for (auto& [node, id] : map_node){bone_data->_allNodeTrans2Parent[id] = mat4_cast(node->mTransformation);}}//顶点骨骼id置0for (VertexBone& v : mo_vertices_bone){for (uint8_t& id : v._boneID){if (id == BONE_ID_NONE)id = 0;}}//子网格合并if (true){//合并网格连续,且材质相同的项vector<ModelObjectSub> vec_sub;ModelObjectSub* pre = nullptr;//for (auto iter = model_object->_allSub.begin();iter != model_object->_allSub.end(); ++iter){ModelObjectSub& sub = *iter;if (pre&& sub._indexMaterial == pre->_indexMaterial&& (sub._range._offsetIndex == pre->_range._offsetIndex + pre->_range._countTriangle * 3)){//是连续的pre->_range._countTriangle += sub._range._countTriangle;}else{vec_sub.push_back(sub);pre = &vec_sub.back();}}debug(format("子网格合并:{}->{}", model_object->_allSub.size(), vec_sub.size()));for (ModelObjectSub& sub : vec_sub){debug(format("合并后网格,起始:{},三角数:{},材质:{}", sub._range._offsetIndex, sub._range._countTriangle, sub._indexMaterial));}swap(model_object->_allSub, vec_sub);int pause = 3;}return ret;}private:struct SceneObject{};//递归遍历nodevoid _process_node(aiNode* node, SceneObject* targetParent, aiMatrix4x4 accTransform){SceneObject* parent;aiMatrix4x4 transform;//如果node有mesh,则创建一个SceneObjectif (node->mNumMeshes > 0) {SceneObject* newObject = new SceneObject;//targetParent.addChild(newObject);//读取mesh到SceneObject_read_node_mesh(node, newObject);//这个SceneObject是所有子节点的父亲parent = newObject;//transform.SetUnity();}else{//如果nodem没有mesh,则跳过,但应用变换parent = targetParent;transform = node->mTransformation * accTransform;}//继续遍历所有子节点(广度优先)for (size_t i = 0; i < node->mNumChildren; ++i){_process_node(node->mChildren[i], parent, transform);}}//读取网格void _read_node_mesh(aiNode* node, SceneObject* object){}//光滑度 -> 粗糙度float ShininessToRoughness(float Ypoint){float a = -1;float b = 2;float c;c = (Ypoint / 100) - 1;float D;D = b * b - (4 * a * c);float x1;x1 = (-b + sqrt(D)) / (2 * a);return x1;}//打印材质信息void PrintMaterial(const ModelObjectMaterial& m, unsigned i){debug(format("#{}{:=^32}材质", i, m._materialName));debug(format("漫反射率:{}, {}, {}, {}",m._material._diffuseAlbedo[0], m._material._diffuseAlbedo[1],m._material._diffuseAlbedo[2], m._material._diffuseAlbedo[3]));debug(format("菲涅耳系数:{}, {}, {}",m._material._fresnelR0[0], m._material._fresnelR0[1], m._material._fresnelR0[2]));debug(format("粗糙度:{}", m._material._roughness));debug(format("漫反射贴图:{}", m._pathTexDiffuse));debug(format("法线贴图:{}", m._pathTexNormal));//debug(format("{:=^32}", ""));}//读取纹理,处理内嵌文件的情况,返回到str_pathvoid _read_texture(const aiScene* scene, string_view res_name, const aiMaterial* m,aiTextureType tex_type, string_view str_base, string& str_path){aiString path;m->GetTexture(tex_type, 0, &path);const aiTexture* texture = scene->GetEmbeddedTexture(path.C_Str());if (texture){if (texture->mHeight == 0){//高为0表示它是文件数据,比如png或ddsstring str_fmt = texture->achFormatHint;if (Image::IsSupportFormat(str_fmt, true)){Buffer buf((byte*)texture->pcData, (size_t)texture->mWidth);//使用它的名字避免重复导出string ai_name = path.C_Str();String::EraseNotFileName(ai_name);string str = format("{}{}#{}.{}", g_factory->GetPathTemp(), res_name, ai_name, str_fmt);if (g_file->SaveBuffer(buf, str))str_path = str;elsedebug_warn(format("导出贴图失败:{}", str));}elsedebug_warn(format("不支持的贴图格式:{}", str_fmt));}else{//为rbga32位数据assert(0 && "暂未实现!");}}else{str_path = format("{}{}", str_base, path.C_Str());String::CvtPathSlash(str_path);}}
};ModelLoader* g_modelLoader;DND_NAMESPACE_END#endif

Assimp库代码存档相关推荐

  1. OpenGL基础26:Assimp库

    一.模型文件 游戏中有很多复杂的模型往往都是美术通过3D建模工具构建出来的,当然不是程序将顶点写死在代码里的,想想看一个简单的人物模型可能就有上千个顶点,这个时候按之前"生成木箱子" ...

  2. [OpenGL] 使用Assimp库的骨骼动画

    Tutorial 38: Skeletal Animation With Assimp 最终,我们来到了这里.有数百万的读者都要求这一教程(我可能夸大了一些,但确实有不少).骨骼动画(skeletio ...

  3. Assimp库调用mtl加载obj模型

    网上查阅了很多资料,通过测试都未通过,后来在两位大神博客的帮助下最终完成了obj及mtl的加载. 参考博客链接: OpenGL学习: uniform blocks(UBO)在着色器中的使用_arag2 ...

  4. OpenGL绘制罗纳尔多三维模型-Assimp库

    本文主要讲解assimp库的使用. 先看效果-资源在文末 正视图: 后视图: 侧视图: 主要使用assimp库进行加载obj模型: 核心代码: 通过加载完的scene对象解析模型的数据: void l ...

  5. 【FCL学习第二讲】使用Assimp库导入外部模型碰撞检测

    测试模型 首先,使用Solidworks新建一个正方体,边长20mm,另存为stl格式 使用Assimp库Assimp - LearnOpenGL CN (learnopengl-cn.github. ...

  6. mingw+cmake编译Assimp库遇到undefine问题

    ### 起因: 学习OpenGL,希望在codeblocks中配置Assimp库,下载Assimp source文件再由cmake generate后,用mingw build遇到50个undefin ...

  7. 【程序员基础篇】开源中国私有库代码更新

    开源中国私有库代码更新 环境 expect bash 步骤 在开源中国新建私有库 脚本执行代码库更新 在本地web项目目录下添加远程库 在本地web目录下/Appliactions/XAMPP/htd ...

  8. golang配置export GOPRIVATE拉取私有库代码

    golang配置export GOPRIVATE拉取私有库代码 可参考链接: http://t.zoukankan.com/jwentest-p-12520378.html

  9. C++11:内联命名空间,无缝升级库代码

    前言 想象这样一种场景: 如果A代码库提供一个接口foo来完成一些工作,突然某天由于加入了新特性,需要升级接口,而有些用户喜欢新的特性但是并不愿意为了新接口去修改他们的代码,还有部分用户认为新接口影响 ...

  10. git merge;fork同步集中库代码;a标签返回

    git merge // 想要在本地库的topic分支拉取fork库的master代码 1 git checked master 2 git pull 3 git checked topic 4 gi ...

最新文章

  1. bzoj 1058: [ZJOI2007]报表统计 (Treap)
  2. 【PAT乙级】1092 最好吃的月饼 (20 分)
  3. linux rsync 原理,rsync 同步原理和类别
  4. Pixhawk代码分析-基础知识
  5. 【编译原理】如何编写BNF?
  6. 【转】为了修复打码女神脸,他们提出二阶段生成对抗网络EdgeConnect
  7. java请假审批怎么实现_java实现请假时间判断
  8. 新媒体运营的“钱途”在哪里?
  9. OpenCV案例(三): 玉米颗粒计数
  10. LVS/NAT的配置和应用
  11. 通用快速检测邮件故障思路方法(二)
  12. SCI、EI和IEEE有什么区别
  13. 是否有唯一的 Android 设备 ID?
  14. JS生成随机字符,生成一堆高逼格的乱码。。。
  15. transmission简单使用
  16. 腾讯邮件服务器备份,怎样使用邮件备份功能?
  17. 十进制转二进制(除2取余法)
  18. 迁移学习一——TCA和SSTCA
  19. ODOO15委外加工(外协)业财一体凭证生成方案
  20. mac上使用使用rz,sz命令

热门文章

  1. mysql数据库自动备份软件SQLBackupAndFTP简介(图文)
  2. 如何解决Mac电脑键盘上的大写锁定键灯不亮?
  3. python操作腾讯文档_Python调用腾讯云接口
  4. clustalX2使用以及相关的问题
  5. vm安装win7系统
  6. IDEA使用的插件记录
  7. Flutter入门进阶之旅(六)Layout Widget
  8. mac虚拟机服务器设置u盘启动不了怎么办,苹果MacBook Air u盘启动不了怎么办?
  9. HDU 6437 最小费用最大流
  10. (详细)华为荣耀8青春 PRA-AL00的usb调试模式在哪里开启的流程