10、基于osg引擎生成热力图高度图实现3D热力图可视化、3D热力图实时更新(带过渡效果)

devtools/2025/3/20 16:49:41/

1、结果
在这里插入图片描述
在这里插入图片描述
2、完整C++代码

#include <sstream>
#include <iomanip>
#include <iostream>
#include <vector>
#include <random>
#include <cmath>
#include <functional>
#include <osgViewer/viewer>
#include <osgDB/ReadFile>
#include <osg/Texture3D>
#include <osg/Texture1D>
#include <osgDB/FileUtils>
#include <osg/Billboard>
#include <osg/TexGenNode>
#include <osg/ClipNode>
#include <osgDB/WriteFile>
#include <osg/Point>
#include <osg/ShapeDrawable>
#include <osg/PositionAttitudeTransform>
#include <osg/MatrixTransform>
#include <osgGA/TrackballManipulator>
#include <osg/ComputeBoundsVisitor>
#include <osg/TransferFunction>
#include <array>
#include <osgViewer/ViewerEventHandlers>
#include <osgGA/StateSetManipulator>
#include <osgUtil/SmoothingVisitor>
const std::string DATA_PATH = R"(..\data\)";
const std::string SHADER_PATH = R"(..\shaders\)";
const float RADIUS = 35;//设置热力点的影响半径
struct Point { int x, y; float value; };
// 生成随机数
int getRandomInt(int min, int max) {return min + std::rand() % (max - min + 1);
}// 颜色插值(用于热力图渐变)
osg::Vec4 lerpColor(float value) {value = osg::clampBetween(value, 0.0f, 1.0f);  // 确保 value 在 [0, 1] 范围内osg::Vec4 colors[] = {osg::Vec4(49/255.0, 54/255.0, 149/255.0, value),osg::Vec4(69/255.0, 117/255.0, 180/255.0, value),osg::Vec4(116/255.0, 173/255.0, 209/255.0, value),osg::Vec4(171/255.0, 217/255.0, 233/255.0, value),osg::Vec4(224/255.0, 243/255.0, 248/255.0, value),osg::Vec4(255/255.0, 255/255.0, 191/255.0, value),osg::Vec4(254/255.0, 224/255.0, 144/255.0, value),osg::Vec4(253/255.0, 174/255.0, 97/255.0, value),osg::Vec4(244/255.0, 109/255.0, 67/255.0, value),osg::Vec4(215/255.0, 48/255.0, 39/255.0, value),osg::Vec4(165/255.0, 0.0, 38/255.0, value)};//osg::Vec4 colors[] = {//    osg::Vec4(50 / 255.0, 136 / 255.0, 189 / 255.0, value),   //    osg::Vec4(102 / 255.0, 194 / 255.0, 165 / 255.0, value),//    osg::Vec4(171 / 255.0, 221 / 255.0, 164 / 255.0, value),//    osg::Vec4(230 / 255.0, 245 / 255.0, 152 / 255.0, value),//    osg::Vec4(254 / 255.0, 224 / 255.0, 139 / 255.0, value),//    osg::Vec4(253 / 255.0, 174 / 255.0, 97 / 255.0, value),//    osg::Vec4(244 / 255.0, 109 / 255.0, 67 / 255.0, value),//    osg::Vec4(213 / 255.0, 62 / 255.0, 79 / 255.0, value),//};int numColors = sizeof(colors) / sizeof(colors[0]);float t = value * (numColors - 1);      // 乘以 (数量-1)int index = static_cast<int>(t);// 处理边界情况,避免越界if (index >= numColors - 1) {return colors[numColors - 1];       // 直接返回最后一个颜色}t -= index;// 提取 t 的小数部分,作为插值比例return colors[index] * (1 - t) + colors[index + 1] * t;
}std::vector<Point> generateData(int width, int height, int pointCount)
{std::vector<Point> points;for (int i = 0; i < pointCount; ++i) {points.push_back({ getRandomInt(10, width - 10), getRandomInt(10, height - 10), getRandomInt(0, 100) / 100.0f });}return points;
}// 生成热力图图像
osg::ref_ptr<osg::Image> generateHeatmap(int width, int height, std::vector<Point> points) {osg::ref_ptr<osg::Image> image = new osg::Image();image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE);std::vector<unsigned char> buffer(width * height * 4, 0); // 初始化RGBA缓冲区// 绘制点 (模拟圆形模糊)for (const auto& p : points) {for (int dx = -RADIUS; dx <= RADIUS; ++dx) {for (int dy = -RADIUS; dy <= RADIUS; ++dy) {int px = p.x + dx;//计算当前像素的x坐标int py = p.y + dy;//计算当前像素的y坐标//确保当前像素点在图像范围内if (px >= 0 && px < width && py >= 0 && py < height) {//计算该像素点与中心点的归一化距离float dist = std::sqrt(dx * dx + dy * dy) / RADIUS;//如果距离在热力点影响范围内if (dist <= 1.0f) {//计算当前像素在缓冲区中的索引int index = (py * width + px) * 4;//计算当前像素的影响强度float intensity = (1.0f - dist) * p.value;//叠加透明度float oldAlpha = buffer[index + 3] / 255.0f; // 读取背景透明度(归一化到0~1)float newAlpha = intensity + oldAlpha * (1.0f - intensity); // 计算混合透明度buffer[index + 3] = static_cast<int>(std::min(255.0f, newAlpha * 255)); // 更新透明度}}}}}// 颜色映射for (int i = 0; i < width * height; ++i) {float alpha = buffer[i * 4 + 3] / 255.0f; // 归一化透明度osg::Vec4 color = lerpColor(alpha);buffer[i * 4] = static_cast<unsigned char>(color.r() * 255);buffer[i * 4 + 1] = static_cast<unsigned char>(color.g() * 255);buffer[i * 4 + 2] = static_cast<unsigned char>(color.b() * 255);buffer[i * 4 + 3] = static_cast<unsigned char>(color.a() * 255);}// 复制数据到 osg::Imagememcpy(image->data(), buffer.data(), buffer.size());return image;
}// 生成高度图
osg::ref_ptr<osg::Image> generateHeightmap(int width, int height, std::vector<Point> points)
{osg::ref_ptr<osg::Image> image = new osg::Image;image->allocateImage(width, height, 1, GL_LUMINANCE, GL_UNSIGNED_BYTE);std::vector<float> heightBuffer(width * height, 0.0);//浮点型高度缓存// 计算高度影响(使用高斯衰减)for (const auto& p : points){for (int dx = -RADIUS; dx <= RADIUS; ++dx) {for (int dy = -RADIUS; dy <= RADIUS; ++dy) {int px = p.x + dx;int py = p.y + dy;if (px >= 0 && px < width && py >= 0 && py < height){float distance = std::sqrt(dx * dx + dy * dy);if (distance <= RADIUS) {float normalizedDist = distance / RADIUS;//高斯衰减系数float falloff = std::exp(-normalizedDist * normalizedDist * 4.0f);int index = py * width + px;heightBuffer[index] += falloff * p.value;}}}}}// 归一化处理float maxHeight = *std::max_element(heightBuffer.begin(), heightBuffer.end());std::vector<unsigned char> grayBuffer(width * height);for (int i = 0; i < width * height; ++i){float normalized = heightBuffer[i] / maxHeight;grayBuffer[i] = static_cast<unsigned char>(normalized * 255);}memcpy(image->data(), grayBuffer.data(), grayBuffer.size());return image;
}void generateHeatmapTexture()
{std::vector<Point> data = generateData(1024, 1024, 100);osg::ref_ptr<osg::Image> heatmap2dImage = generateHeatmap(1024, 1024, data);osg::ref_ptr<osg::Image> heightmapImage = generateHeightmap(1024, 1024, data);// 使用OSG保存图像if (osgDB::writeImageFile(*heatmap2dImage, "heatmap2d.png")) {std::cout << "Image saved as " << "heatmap_osg.png" << std::endl;}else {std::cerr << "Error saving image." << std::endl;}if (osgDB::writeImageFile(*heightmapImage, "heightmap.png")) {std::cout << "Image saved as " << "heightmap.png" << std::endl;}else {std::cerr << "Error saving image." << std::endl;}
}// 创建地形节点
osg::ref_ptr<osg::Node> createTerrain(osg::Image* heightmap, osg::Image* heatmap) {// 创建几何体osg::ref_ptr<osg::Geometry> geometry = new osg::Geometry();osg::ref_ptr<osg::Geode> geode = new osg::Geode();geode->addDrawable(geometry);// 创建顶点数组osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array();const int width = heightmap->s();const int height = heightmap->t();const float terrainSize = 200.0f; // 地形物理尺寸const float heightScale = 50.0f;  // 高度缩放系数// 生成顶点数据unsigned char* heightData = heightmap->data();for (int y = 0; y < height; ++y) {for (int x = 0; x < width; ++x) {// 计算顶点位置(居中)float xPos = (x - width / 2.0f) * (terrainSize / width);float yPos = (y - height / 2.0f) * (terrainSize / height);float zPos = heightData[y * width + x] / 255.0f * heightScale;vertices->push_back(osg::Vec3(xPos, yPos, zPos));}}// 创建索引数组(三角形带)osg::ref_ptr<osg::DrawElementsUShort> indices =new osg::DrawElementsUShort(GL_TRIANGLES);for (int y = 0; y < height - 1; ++y) {for (int x = 0; x < width - 1; ++x) {// 创建两个三角形组成四边形int i0 = y * width + x;int i1 = i0 + 1;int i2 = i0 + width;int i3 = i2 + 1;indices->push_back(i1);indices->push_back(i2);indices->push_back(i0);indices->push_back(i3);indices->push_back(i2);indices->push_back(i1);}}// 设置几何体数据geometry->setVertexArray(vertices);geometry->addPrimitiveSet(indices);// 自动生成法线osgUtil::SmoothingVisitor::smooth(*geometry);// 创建纹理坐标数组osg::ref_ptr<osg::Vec2Array> texCoords = new osg::Vec2Array();for (int y = 0; y < height; ++y) {for (int x = 0; x < width; ++x) {// 归一化纹理坐标 [0,1]float u = static_cast<float>(x) / (width - 1);float v = static_cast<float>(y) / (height - 1);texCoords->push_back(osg::Vec2(u, v));}}geometry->setTexCoordArray(0, texCoords);// 应用纹理osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D();texture->setImage(heatmap);texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);osg::StateSet* stateset = geode->getOrCreateStateSet();stateset->setTextureAttributeAndModes(0, texture);stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF);stateset->setMode(GL_BLEND, osg::StateAttribute::ON);return geode;
}osg::Geode* createHeatmap3D() {const int GRID_SIZE = 256;osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;osg::ref_ptr<osg::Vec2Array> texCoords = new osg::Vec2Array;for (int y = 0; y < GRID_SIZE; ++y) {for (int x = 0; x < GRID_SIZE; ++x) {float u = x / (GRID_SIZE - 1.0f);float v = y / (GRID_SIZE - 1.0f);vertices->push_back(osg::Vec3(u * 100 - 50, v * 100 - 50, 0)); // 居中显示texCoords->push_back(osg::Vec2(u, v));}}// 创建索引osg::ref_ptr<osg::DrawElementsUInt> indices =new osg::DrawElementsUInt(GL_TRIANGLES);for (int y = 0; y < GRID_SIZE - 1; ++y) {for (int x = 0; x < GRID_SIZE - 1; ++x) {int i0 = y * GRID_SIZE + x;int i1 = i0 + 1;int i2 = i0 + GRID_SIZE;int i3 = i2 + 1;indices->push_back(i1);indices->push_back(i2);indices->push_back(i0);indices->push_back(i3);indices->push_back(i2);indices->push_back(i1);}}geom->setVertexArray(vertices);geom->setTexCoordArray(0, texCoords);geom->addPrimitiveSet(indices);osgUtil::SmoothingVisitor::smooth(*geom);osg::Geode* geode = new osg::Geode;geode->addDrawable(geom);return geode;
}osg::Texture2D* createTexture(osg::Image* image) {osg::Texture2D* tex = new osg::Texture2D;tex->setImage(image);tex->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);tex->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);return tex;
}void setupStateSet(osg::Geode* geode) {osg::StateSet* ss = geode->getOrCreateStateSet();// 混合设置ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);ss->setMode(GL_BLEND, osg::StateAttribute::ON);std::vector<Point> data = generateData(256, 256, 100);osg::ref_ptr<osg::Image> heatmap2dImage1 = generateHeatmap(256, 256, data);osg::ref_ptr<osg::Image> heightmapImage1 = generateHeightmap(256, 256, data);struct Textures {osg::ref_ptr<osg::Texture2D> current;osg::ref_ptr<osg::Texture2D> next;float transition = 0.0f;bool updating = false;};Textures heightTex, heatmapTex;// 初始纹理heightTex.current = createTexture(heightmapImage1);heatmapTex.current = createTexture(heatmap2dImage1);data = generateData(256, 256, 200);osg::ref_ptr<osg::Image> heatmap2dImage2 = generateHeatmap(256, 256, data);osg::ref_ptr<osg::Image> heightmapImage2 = generateHeightmap(256, 256, data);heightTex.next = createTexture(heightmapImage2);heatmapTex.next = createTexture(heatmap2dImage2);osg::Program* program = new osg::Program;ss->setAttribute(program);program->addShader(osgDB::readRefShaderFile(osg::Shader::VERTEX, SHADER_PATH + R"(heatmap3d.vert)"));program->addShader(osgDB::readRefShaderFile(osg::Shader::FRAGMENT, SHADER_PATH + R"(heatmap3d.frag)"));ss->setAttributeAndModes(program);program->addBindAttribLocation("texCoord", osg::Drawable::TEXTURE_COORDS_0);// 绑定纹理单元ss->setTextureAttribute(0, heightTex.current);ss->setTextureAttribute(1, heightTex.next);ss->setTextureAttribute(2, heatmapTex.current);ss->setTextureAttribute(3, heatmapTex.next);// Uniform绑定ss->addUniform(new osg::Uniform("heightMap", 0));ss->addUniform(new osg::Uniform("nextHeightMap", 1));ss->addUniform(new osg::Uniform("heatmap", 2));ss->addUniform(new osg::Uniform("nextHeatmap", 3));ss->addUniform(new osg::Uniform("transitionProgress", 0.0f));
}osg::Timer_t startTime;
bool startSimulate = false;
class KeyboardEventHandler : public osgGA::GUIEventHandler {
public:KeyboardEventHandler(){}bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) override {if (ea.getEventType() == osgGA::GUIEventAdapter::KEYDOWN) {switch (ea.getKey()) {case 't':  if (!startSimulate){startTime = osg::Timer::instance()->tick();startSimulate = true;}return true;default:return false;}}return false;}private:
};class TimeUpdateCallback : public osg::NodeCallback
{
public:TimeUpdateCallback()  {}virtual void operator()(osg::Node* node, osg::NodeVisitor* nv){if (startSimulate){osg::StateSet* ss = node->getStateSet();if (ss){float time = osg::Timer::instance()->delta_s(startTime, osg::Timer::instance()->tick()) / 3.0f;time = osg::clampBetween(time, 0.0f, 1.0f);if (time == 1.0){std::random_device rd;  // 用于获取随机种子std::mt19937 gen(rd()); // 使用 Mersenne Twister 算法// 定义一个分布范围 [100, 200]std::uniform_int_distribution<> dis(100, 200);std::vector<Point> data = generateData(256, 256, dis(gen));osg::ref_ptr<osg::Image> heatmap2dImage = generateHeatmap(256, 256, data);osg::ref_ptr<osg::Image> heightmapImage = generateHeightmap(256, 256, data);ss->setTextureAttribute(0, ss->getTextureAttribute(1, osg::StateAttribute::TEXTURE));ss->setTextureAttribute(1, createTexture(heightmapImage));ss->setTextureAttribute(2, ss->getTextureAttribute(3, osg::StateAttribute::TEXTURE));ss->setTextureAttribute(3, createTexture(heatmap2dImage));ss->getUniform("transitionProgress")->set(0.0f);startTime = osg::Timer::instance()->tick();}elsess->getUniform("transitionProgress")->set(time);}traverse(node, nv);}}};int preview3DHeatmapWithAnimate()
{osg::Geode* geode = createHeatmap3D();setupStateSet(geode);geode->setUpdateCallback(new TimeUpdateCallback());osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;osg::ref_ptr<osg::Group> group = new osg::Group;group->addChild(geode);viewer->setSceneData(group);viewer->addEventHandler(new osgGA::StateSetManipulator(viewer->getCamera()->getOrCreateStateSet()));viewer->addEventHandler(new osgViewer::StatsHandler());osg::ref_ptr<KeyboardEventHandler> keyboardHandler = new KeyboardEventHandler;viewer->addEventHandler(keyboardHandler);return viewer->run();
}int preview3DHeatmap()
{std::vector<Point> data = generateData(256, 256, 100);osg::ref_ptr<osg::Image> heatmap2dImage = generateHeatmap(256, 256, data);osg::ref_ptr<osg::Image> heightmapImage = generateHeightmap(256, 256, data);osg::ref_ptr<osg::Node> node = createTerrain(heightmapImage, heatmap2dImage);osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;osg::ref_ptr<osg::Group> group = new osg::Group;group->addChild(node);viewer->setSceneData(group);viewer->addEventHandler(new osgGA::StateSetManipulator(viewer->getCamera()->getOrCreateStateSet()));viewer->addEventHandler(new osgViewer::StatsHandler());return viewer->run();
}int preview2DHeatmap() {std::vector<Point> data = generateData(256, 256, 100);osg::ref_ptr<osg::Image> heatmap2dImage = generateHeatmap(256, 256, data);// 创建带纹理的四边形几何体osg::ref_ptr<osg::Geometry> quad = osg::createTexturedQuadGeometry(osg::Vec3(-1.0f, -1.0f, 0.0f), // 左下角顶点osg::Vec3(2.0f, 0.0f, 0.0f),  // 宽度向量osg::Vec3(0.0f, 2.0f, 0.0f),  // 高度向量0.0f, 0.0f, 1.0f, 1.0f        // 纹理坐标 (左下角到右上角));osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;texture->setImage(heatmap2dImage);osg::ref_ptr<osg::StateSet> stateSet = quad->getOrCreateStateSet();stateSet->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON);// 创建Geode并添加几何体osg::ref_ptr<osg::Geode> geode = new osg::Geode;geode->addDrawable(quad);// 创建场景图根节点osg::ref_ptr<osg::Group> root = new osg::Group;root->addChild(geode);// 创建并设置ViewerosgViewer::Viewer viewer;viewer.setSceneData(root);return viewer.run();
}int main()
{//return preview2DHeatmap();//return preview3DHeatmap();return preview3DHeatmapWithAnimate();
}

3、着色器代码

//heatmap3d.vert
#version 110
/* GLSL 1.10需要显式声明精度 (OpenGL ES要求) */
#ifdef GL_ES
precision highp  float;
#endif
/* 自定义 uniforms */
uniform sampler2D heightMap;
uniform sampler2D nextHeightMap;
uniform float transitionProgress;
attribute vec2 texCoord;
// 输入纹理坐标属性(对应几何体的第0层纹理)
varying vec2 vTexCoord;void main() {float x = gl_Vertex.x;float y = gl_Vertex.y;vTexCoord = texCoord;// 使用 texture2D 代替 texture 函数float currentHeight = texture2D(heightMap, texCoord.xy).r * 50.0;float nextHeight = texture2D(nextHeightMap, texCoord.xy).r * 50.0;// 高度插值计算float finalHeight = mix(currentHeight, nextHeight, transitionProgress);// 坐标变换vec4 pos = vec4(x, y, finalHeight, 1.0);gl_Position = gl_ModelViewProjectionMatrix * pos;
}
//heatmap3d.frag
#version 110
/* GLSL 1.10需要显式声明精度 (OpenGL ES要求) */
#ifdef GL_ES
precision mediump float;
#endifuniform sampler2D heatmap;
uniform sampler2D nextHeatmap;
uniform float transitionProgress;
varying vec2 vTexCoord;void main() {// 使用texture2D替代texture函数vec4 currentColor = texture2D(heatmap, vTexCoord);vec4 nextColor = texture2D(nextHeatmap, vTexCoord);// 使用gl_FragColor替代自定义输出gl_FragColor = mix(currentColor, nextColor, transitionProgress);
}

http://www.ppmy.cn/devtools/168672.html

相关文章

python3+pytest+allure自动化框架搭建

博主之前的工作中使用过Pythonrobotframework框架进行接口自动化和webUI自动化&#xff0c;现在按照自己的过往经验&#xff0c;搭建pythonpytestallure的接口自动化框架。 很多人会python、requests、json等库去发送请求并处理请求&#xff0c;但是有些人还是不知道接口自动化…

【网络协议】基于UDP的可靠协议:KCP

TCP是为流量设计的&#xff08;每秒内可以传输多少KB的数据&#xff09;&#xff0c;讲究的是充分利用带宽。而 KCP是为流速设计的&#xff08;单个数据包从一端发送到一端需要多少时间&#xff09;&#xff0c;以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。TCP信…

Python基于深度学习的多模态人脸情绪识别研究与实现

一、系统架构设计 A[数据采集] --> B[预处理模块] B --> C[特征提取] C --> D[多模态融合] D --> E[情绪分类] E --> F[系统部署] F --> G[用户界面] 二、数据准备与处理 1. 数据收集 - 视频数据&#xff1a;FER2013&#xff08;静态图像&#xff0…

【虚幻C++笔记】打印输出的方式

目录 UE_LOGGEngine->AddOnScreenDebugMessage UE_LOG UE_LOG 宏是UnrealEngine 中用于日志记录的标准方式。它可以输出日志信息到控制台和日志文件&#xff0c;支持多种日志级别&#xff08;如Log&#xff0c;Warning&#xff0c;Error") UE_LOG(LogCategory, LogVer…

MongoDB 更新集合名

MongoDB 更新集合名 引言 在MongoDB中&#xff0c;集合&#xff08;Collection&#xff09;是存储数据的基本单位。在实际应用中&#xff0c;我们可能会因为各种原因需要更改集合的名称。本文将详细介绍如何在MongoDB中更新集合名&#xff0c;并探讨一些相关的注意事项。 更…

【软件工程】09_软件实现、测试和维护

目录 9.1 软件实现 宏观与微观视角 软件实现的目标 软件实现的任务 9.2 软件测试基础 9.2.1 软件测试概述 定义 目的 原则 9.2.2 软件的可测试性 9.2.3 软件测试的对象 9.2.4 软件测试信息流 9.2.5 软件测试步骤 软件测试与软件开发各阶段的关系 9.3 软件测试方法…

IvorySQL 增量备份与合并增量备份功能解析

1. 概述 IvorySQL v4 引入了块级增量备份和增量备份合并功能&#xff0c;旨在优化数据库备份与恢复流程。通过 pg_basebackup 工具支持增量备份&#xff0c;显著降低了存储需求和备份时间。同时&#xff0c;pg_combinebackup 工具能够将多个增量备份合并为单个完整备份&#x…

【6】组合计数学习笔记

前言 关于今天发现自己连快速幂都忘记怎么写这件事 这篇博客是组合计数基础&#xff0c;由于大部分内容都是 6 6 6 级&#xff0c;所以我就给整个提高级的组合数学评了 6 6 6 级。 组合计数基础 加法原理与乘法原理 加法原理&#xff08;分类计数原理&#xff09;&#…