games101-作业3

由于此次试验需要加载模型,涉及到本地环节,如果是windows系统,需要对main函数中的路径稍作改变:

这么写需要:
#include "windows.h"

该段代码:


#include "windows.h"
int main(int argc, const char** argv)
{
    std::vector<Triangle*> TriangleList;

    float angle = 140.0;
    bool command_line = false;


    std::string filename = "output.png";
    objl::Loader Loader;
    std::string obj_path = "\\..\\..\\test\\models\\spot\\";
    char pathBuf[MAX_PATH];
    char* p;
    if (GetModuleFileNameA(NULL, pathBuf, MAX_PATH))
    {
        p = strrchr(pathBuf, '\\');
        if (p)
        {
            *p = '\0';
            std::string exe_path = pathBuf;
            obj_path = exe_path + obj_path;
        }
    }

    bool loadout = Loader.LoadFile("D:\\opencode\\games101\\test\\test\\models\\spot\\spot_triangulated_good.obj");

同时在windows下计算很卡,可能软件模拟计算量很大的原因;

使用c++17及以上版本,

\spot_triangulated_good.obj 中的描述了顶点信息

输入到三角形列表中: 

但是 在使用的时候:

fragment_shader 即下面的着色模型:中去如何消费这个图片


    std::function<Eigen::Vector3f(fragment_shader_payload)> active_shader = normal_fragment_shader;  用于设置着色模型

着色模型

1.normal模型
// 根据法线进行不同着色
Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload& payload)
{
    Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
    Eigen::Vector3f result;
    result << return_color.x() * 255, return_color.y() * 255, return_color.z() * 255;
    return result;
}

这个函数并不是光照模型,只是根据不同法线返回不同颜色值。
第一行的代码,首先取出当前待着色像素点的法向量的X,Y,Z坐标并归一化,故此时X,Y,Z都在[-1,1]之间,加上(1.0f, 1.0f, 1.0f)后,变为[0,2],再除以2,即得[0,1],再分别乘以255即可得到各个颜色值了。

效果:

2.phong模型

Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
    // 泛光、漫反射、高光系数
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    // 灯光位置和强度
    auto l1 = light{
  
  {20, 20, 20}, {500, 500, 500}};
    auto l2 = light{
  
  {-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};// 光照
    Eigen::Vector3f amb_light_intensity{10, 10, 10};// 环境光强度
    Eigen::Vector3f eye_pos{0, 0, 10};// 相机位置

    float p = 150;

    // ping point的信息
    Eigen::Vector3f color = payload.color;
    Eigen::Vector3f point = payload.view_pos;// view space
    Eigen::Vector3f normal = payload.normal;

    Eigen::Vector3f result_color = {0, 0, 0};// 光照结果

    Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
    // 遍历每一束光
    for (auto& light : lights)
    {
        Eigen::Vector3f l = (light.position - point).normalized(), v = (eye_pos - point).normalized();// 光照方向和观察方向
        Eigen::Vector3f h = (l + v).normalized();// 半程向量
        Eigen::Vector3f I = light.intensity;// 光强
        float r2 = (light.position - point).dot(light.position - point);
        Eigen::Vector3f Ld = kd.cwiseProduct(I / r2) * std::max(0.0f, normal.dot(l));//cwiseProduct()函数允许Matrix直接进行点对点乘法,而不用转换至Array
        Eigen::Vector3f Ls = ks.cwiseProduct(I / r2) * std::pow(std::max(0.0f, normal.dot(h)), p);
        result_color += La + Ld + Ls;
    }
    //Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
    //result_color += La;
    return result_color * 255.f;
}

直接根据公式写就行,这里我认为环境光应该放在循环外,不过这样渲出来的结果相比说说明偏暗,但后面的displacement又要放在外面否则偏亮。

phong模型渲染结果:

3.texture模型
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
    Eigen::Vector3f return_color = {0, 0, 0};
    if (payload.texture)
    {
        return_color = payload.texture->getColor(payload.tex_coords.x(), payload.tex_coords.y());// 获取材质颜色信息
    }
    Eigen::Vector3f texture_color;
    texture_color << return_color.x(), return_color.y(), return_color.z();

    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = texture_color / 255.f;// 材质颜色影响漫反射系数
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{
  
  {20, 20, 20}, {500, 500, 500}};
    auto l2 = light{
  
  {-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = texture_color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    Eigen::Vector3f result_color = {0, 0, 0};

    Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
    for (auto& light : lights)
    {
        Eigen::Vector3f l = (light.position - point).normalized(), v = (eye_pos - point).normalized();// 光照方向和观察方向
        Eigen::Vector3f h = (l + v).normalized();// 半程向量
        Eigen::Vector3f I = light.intensity;// 光强
        float r2 = (light.position - point).dot(light.position - point);
        Eigen::Vector3f Ld = kd.cwiseProduct(I / r2) * std::max(0.0f, normal.dot(l));//cwiseProduct()函数允许Matrix直接进行点对点乘法,而不用转换至Array
        Eigen::Vector3f Ls = ks.cwiseProduct(I / r2) * std::pow(std::max(0.0f, normal.dot(h)), p);
        result_color += La + Ld + Ls;
    }
    // Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
    // result_color += La;
    return result_color * 255.f;
}

 

    Eigen::Vector3f getColor(float u, float v)
    {
        // 限制(u, v)坐标范围
        u = std::fmin(1, std::fmax(u, 0));
        v = std::fmin(1, std::fmax(v, 0));

        auto u_img = u * (width-1);
        auto v_img = (1 - v) * (height-1);
        auto color = image_data.at<cv::Vec3b>(v_img, u_img);// 四舍五入
        return Eigen::Vector3f(color[0], color[1], color[2]);
    }

更换一个材质:

4 displacement_fragment_shader 

Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
    
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{
  
  {20, 20, 20}, {500, 500, 500}};
    auto l2 = light{
  
  {-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color; 
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    float kh = 0.2, kn = 0.1;
    
    float x = normal.x();
	float y = normal.y();
	float z = normal.z();
	Eigen::Vector3f t{ x * y / std::sqrt(x * x + z * z), std::sqrt(x * x + z * z), z*y / std::sqrt(x * x + z * z) };
	Eigen::Vector3f b = normal.cross(t);
	Eigen::Matrix3f TBN;
	TBN <<  t.x(), b.x(), normal.x(),
		    t.y(), b.y(), normal.y(),
		    t.z(), b.z(), normal.z();
	float u = payload.tex_coords.x();
	float v = payload.tex_coords.y();
	float w = payload.texture->width;
	float h = payload.texture->height;
	float dU = kh * kn * (payload.texture->getColor(u + 1 / w , v).norm() - payload.texture->getColor(u, v).norm());
	float dV = kh * kn * (payload.texture->getColor(u, v + 1 / h).norm() - payload.texture->getColor(u, v).norm());
	Eigen::Vector3f ln{-dU, -dV, 1};
    //与凹凸贴图的区别就在于这句话
    point += (kn * normal * payload.texture->getColor(u , v).norm());
	normal = (TBN * ln).normalized();
    Eigen::Vector3f result_color = {0, 0, 0};

    for (auto& light : lights)
    {
        Eigen::Vector3f l = (light.position - point).normalized();      // 光
		Eigen::Vector3f v = (eye_pos - point).normalized();		        // 眼
        Eigen::Vector3f h = (l + v).normalized();                       // 半程向量
	    double r_2 = (light.position - point).dot(light.position - point);
        Eigen::Vector3f Ld = kd.cwiseProduct(light.intensity / r_2) * std::max(0.0f, normal.dot(l));    //cwiseProduct()函数允许Matrix直接进行点对点乘法,而不用转换至Array。
        Eigen::Vector3f Ls = ks.cwiseProduct(light.intensity / r_2) * std::pow(std::max(0.0f, normal.dot(h)), p);
		result_color += (Ld + Ls);   
    }
    Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
    result_color += La; 
    return result_color * 255.f;
}

 draw函数(顶点、三角形处理阶段) 

void rst::rasterizer::draw(std::vector<Triangle*>& TriangleList) {

	float f1 = (50 - 0.1) / 2.0;// zfar和znear距离的一半
	float f2 = (50 + 0.1) / 2.0;// zfar和znear的中心z坐标

	Eigen::Matrix4f mvp = projection * view * model;// 计算MVP变换矩阵
	// 遍历每个小三角形
	for (const auto& t : TriangleList)
	{
		Triangle newtri = *t;

		// 计算viewspace_pos,其中viewspace_pos的坐标是经过MV变换,没有经过P投影变换
		// 所以默认在相机坐标系而不是世界坐标系

		// 记录三角形顶点MV变换后坐标
		std::array<Eigen::Vector4f, 3> mm{
				(view * model * t->v[0]),
				(view * model * t->v[1]),
				(view * model * t->v[2])
		};

		std::array<Eigen::Vector3f, 3> viewspace_pos;

		// 存入viewspace_pos
		std::transform(mm.begin(), mm.end(), viewspace_pos.begin(), [](auto& v) {
			return v.template head<3>();
			});

		// 得到经过mvp后的坐标
		Eigen::Vector4f v[] = {
				mvp * t->v[0],
				mvp * t->v[1],
				mvp * t->v[2]
		};

		// 换算齐次坐标
		for (auto& vec : v) {
			vec.x() /= vec.w();
			vec.y() /= vec.w();
			vec.z() /= vec.w();
		}

		// 计算在MV转换后各顶点的法向量
		// 利用原来点法向量推出MV变换后法向量
		// 因为光线作用是在view_space下进行的
		Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();
		Eigen::Vector4f n[] = {
				inv_trans * to_vec4(t->normal[0], 0.0f),
				inv_trans * to_vec4(t->normal[1], 0.0f),
				inv_trans * to_vec4(t->normal[2], 0.0f)
		};

		// 视口变换 得到顶点在屏幕上的坐标 即screen space
		for (auto& vert : v)
		{
			vert.x() = 0.5 * width * (vert.x() + 1.0);
			vert.y() = 0.5 * height * (vert.y() + 1.0);
			// 为了Zbuffer保留Z值
			// (透视)投影变换最后一步是从正交投影变换到正则立方体
			// 而这一步就是把正则立方体的z值还原到正交投影时的z值,即原始z值
			vert.z() = vert.z() * f1 + f2;
		}

		// 记录经过MVP视口变换后的顶点坐标
		// 完成顶点变换,变换到屏幕空间
		for (int i = 0; i < 3; ++i)
		{
			//screen space coordinates
			newtri.setVertex(i, v[i]);
		}

		// 记录顶点的法向量
		for (int i = 0; i < 3; ++i)
		{
			//view space normal
			newtri.setNormal(i, n[i].head<3>());
		}

		// 设置颜色
		newtri.setColor(0, 148, 121.0, 92.0);
		newtri.setColor(1, 148, 121.0, 92.0);
		newtri.setColor(2, 148, 121.0, 92.0);

		// 对这个小三角形进行光栅化
		// 传入viewspace_pos的坐标,光线的作用是在viewspace下的
		rasterize_triangle(newtri, viewspace_pos);
	}
}
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 
{
    auto v = t.toVector4(); //v[0],v[1],v[2]分别为三角形的三个顶点,是四维向量
    //比较三个顶点的横纵坐标,确定包围盒的边界并取整
    double min_x = std::min(v[0][0], std::min(v[1][0], v[2][0]));
    double max_x = std::max(v[0][0], std::max(v[1][0], v[2][0]));
    double min_y = std::min(v[0][1], std::min(v[1][1], v[2][1]));
    double max_y = std::max(v[0][1], std::max(v[1][1], v[2][1]));
    min_x = static_cast<int>(std::floor(min_x));
    min_y = static_cast<int>(std::floor(min_y));
    max_x = static_cast<int>(std::ceil(max_x));
    max_y = static_cast<int>(std::ceil(max_y));
    //此处实现的是MSAA
    std::vector<Eigen::Vector2f> pos
    {                               //对一个像素分割四份 当然你还可以分成4x4 8x8等等甚至你还可以为了某种特殊情况设计成不规则的图形来分割单元
        {0.25,0.25},                //左下
        {0.75,0.25},                //右下
        {0.25,0.75},                //左上
        {0.75,0.75}                 //右上
    };
    for (int i = min_x; i <= max_x; ++i)
    {
        for (int j = min_y; j <= max_y; ++j)
        {
            int count = 0; //开始遍历四个小格子,获得平均值
            for (int MSAA_4 = 0; MSAA_4 < 4; ++MSAA_4)
            {
                if (insideTriangle(static_cast<float>(i+pos[MSAA_4][0]), static_cast<float>(j+pos[MSAA_4][1]),t.v))
                    ++count;
            }
            if(count) //至少有一个小格子在三角形内
            {
                //此处是框架中代码,获得z,见原程序注释:
                //    * v[i].w() is the vertex view space depth value z.
                //    * Z is interpolated view space depth for the current pixel
                //    * zp is depth between zNear and zFar, used for z-buffer
                auto[alpha, beta, gamma] = computeBarycentric2D(i + 0.5, j + 0.5, t.v);
                float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                zp *= Z;
                //end
                if (depth_buf[get_index(i, j)] > zp)
                {
                    depth_buf[get_index(i, j)] = zp;//更新深度
                    //这里注意,虽然说明上说"反转了z,保证都是正数,并且越大表示离视点越远",
                    //但经过我的查看,实际上并没有反转,因此还是按照-z近大远小来做,当然也可以在上面补一个负号不过没必要

                    //利用重心坐标插值各种值
					auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1);
					auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1).normalized();
					auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1);
					auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1);
                    //shadingcoords是由view_pos插值得到,也就是物体表面的点在相机坐标系的位置。他们会在shader中被用到,来计算光照等信息。

                    //此处是框架中代码,获得z,见原程序注释:
					fragment_shader_payload payload(interpolated_color, interpolated_normal, interpolated_texcoords, texture ? &*texture : nullptr);
					payload.view_pos = interpolated_shadingcoords;
					auto pixel_color = fragment_shader(payload);
                    //end

					// 设置颜色
					set_pixel(Eigen::Vector2i(i, j), pixel_color * (count / 4.0));
                }
            }
        }
    }
}

完整代码:vs2022

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/962267.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 日志:项目的“行车记录仪”

一、什么是Spring Boot日志 &#xff08;一&#xff09;日志引入 在正式介绍日志之前&#xff0c;我们先来看看上篇文章中&#xff08;Spring Boot 配置文件&#xff09;中的验证码功能的一个代码片段&#xff1a; 这是一段校验用户输入的验证码是否正确的后端代码&#xff0c…

【大厂AI实践】OPPO:大规模知识图谱及其在小布助手中的应用

导读&#xff1a;OPPO知识图谱是OPPO数智工程系统小布助手团队主导、多团队协作建设的自研大规模通用知识图谱&#xff0c;目前已达到数亿实体和数十亿三元组的规模&#xff0c;主要落地在小布助手知识问答、电商搜索等场景。 本文主要分享OPPO知识图谱建设过程中算法相关的技…

机器学习周报-文献阅读

文章目录 摘要Abstract 1 相关知识1.1 WDN建模1.2 掩码操作&#xff08;Masking Operation&#xff09; 2 论文内容2.1 WDN信息的数据处理2.2 使用所收集的数据构造模型2.2.1 Gated graph neural network2.2.2 Masking operation2.2.3 Training loss2.2.4 Evaluation metrics 2…

工具的应用——安装copilot

一、介绍Copilot copilot是一个AI辅助编程的助手&#xff0c;作为需要拥抱AI的程序员可以从此尝试进入&#xff0c;至于好与不好&#xff0c;应当是小马过河&#xff0c;各有各的心得。这里不做评述。重点在安装copilot的过程中遇到了一些问题&#xff0c;然后把它总结下&…

后盾人JS--闭包明明白白

延伸函数环境生命周期 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> <…

顺启逆停程序

两台电机用Q0.0和Q0.1表示&#xff0c;分别有自身的启动和停止按钮&#xff0c;第一台电机启动后&#xff0c;第二台电机才能启动。停止时&#xff0c;第二台电机停止后&#xff0c;第一台电机才能停止。 1. 按下按钮SB1&#xff0c;接触器KM1线圈得电吸合&#xff0c;主触点…

登录授权流程

发起一个网络请求需要&#xff1a;1.请求地址 2.请求方式 3.请求参数 在检查中找到request method&#xff0c;在postman中设置同样的请求方式将登录的url接口复制到postman中&#xff08;json类型数据&#xff09;在payload中选择view parsed&#xff0c;将其填入Body-raw中 …

CUDA学习-内存访问

一 访存合并 1.1 说明 本部分内容主要参考: 搞懂 CUDA Shared Memory 上的 bank conflicts 和向量化指令(LDS.128 / float4)的访存特点 - 知乎 1.2 share memory结构 图1.1 share memory结构 放在 shared memory 中的数据是以 4 bytes(即 32 bits)作为 1 个 word,依…

基于Springboot的社区药房管理系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业多年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了多年的设计程序开发&#xff0c;开发过上千套设计程序&#xff0c;没有什么华丽的语言&#xff0c;只有实…

【力扣系列题目】最后一块石头的重量 分割回文串 验证回文串 等差数列划分{最大堆 背包 动态规划}

文章目录 七、最后一块石头的重量最后一块石头的重量【堆】[最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/)【背包】 八、分割回文串分割回文串【分割子串方案数量】[分割回文串 II](https://leetcode.cn/problems/omKAoA/)【最少分割次数】[分割…

KIMI K1.5:用大语言模型扩展强化学习(论文翻译)

文章目录 KIMI K1.5技术报告摘要 1. 引言2. 方法&#xff1a;基于大语言模型的强化学习2.1 强化学习提示集整理2.2 长思维链监督微调2.3 强化学习2.3.1 问题设定2.3.2 策略优化2.3.3 长度惩罚2.3.4 采样策略2.3.5 训练方法的更多细节 2.4 长到短&#xff1a;短思维链模型的上下…

【Linux系统】进程间通信:实现命名管道通信

认识命名管道通信 命名管道通信的结构图示&#xff1a; 图中的 Server 和 Client 是不同的进程&#xff0c; Server 负责发送数据&#xff0c; Client 则是接收数据&#xff0c;进程之间通过命名管道进行数据通信 准备工作&#xff1a; 创建以下文件 Server.hpp #服务器类的…

SpringBoot Web开发(SpringMVC)

SpringBoot Web开发&#xff08;SpringMVC) MVC 核心组件和调用流程 Spring MVC与许多其他Web框架一样&#xff0c;是围绕前端控制器模式设计的&#xff0c;其中中央 Servlet DispatcherServlet 做整体请求处理调度&#xff01; . 除了DispatcherServletSpringMVC还会提供其他…

Linux《基础指令》

在之前的Linux《Linux简介与环境的搭建》当中我们已经初步了解了Linux的由来和如何搭建Linux环境&#xff0c;那么接下来在本篇当中我们就要来学习Linux的基础指令。在此我们的学习是包括两个部分&#xff0c;即指令和关于Linux的基础知识&#xff1b;因此本篇指令和基础知识的…

我的求职面经:(1)C++里指针和数组的区别

经典问题&#xff1a; char s1[]"hello"; char *s2"hello"; 1、s1的值是放在栈上的&#xff0c;值是可以修改的&#xff0c;而hello是一个字符串常量放在静态存储区是不能修改的。 2、内存大小不一样 #include<stdio.h>int main(){char s1[]&quo…

react中如何获取dom元素

实现代码 const inputRef useRef(null) inputRef.current.focus()

【LLM】Deepseek本地部署学习

文章目录 1. 访问ollama官网安装平台2. 选择配置3. 下载和运行 1. 访问ollama官网安装平台 https://ollama.com/ 2. 选择配置 参考以下配置要求 3. 下载和运行 ollama run deepseek-r1:7b

deepseek R1 14b显存占用

RTX2080ti 11G显卡&#xff0c;模型7b速度挺快&#xff0c;试试14B也不错。 7B显存使用5.6G&#xff0c;14B显存刚好够&#xff0c;出文字速度差不多。 打算自己写个移动宽带的IPTV播放器&#xff0c;不知道怎么下手&#xff0c;就先问他了。

【漫话机器学习系列】064.梯度下降小口诀(Gradient Descent rule of thume)

梯度下降小口诀 为了帮助记忆梯度下降的核心原理和关键注意事项&#xff0c;可以用以下简单口诀来总结&#xff1a; 1. 基本原理 损失递减&#xff0c;梯度为引&#xff1a;目标是让损失函数减少&#xff0c;依靠梯度指引方向。负梯度&#xff0c;反向最短&#xff1a;沿着负…

让万物「听说」:AI 对话式智能硬件方案和发展洞察

本文整理自声网 SDK 新业务探索组技术负责人&#xff0c;IoT 行业专家 吴方方 1 月 18 日在 RTE 开发者社区「Voice Agent 硬件分享会」上的分享。本次主要介绍了 AI 对话式智能硬件的发展历程&#xff0c;新一波 AI 浪潮所带来的创新机遇、技术挑战以及未来的展望。 在语音交…