3D模型实时变形算法

最近,在尝试渲染一些奇怪的形状后,我陷入了计算机图形学的困境。事实证明,对于我试图解决的具体问题,没有现有的选项完全适合我想要做的事情。几周后,我终于带着一些答案再次浮出水面,写了很多行代码!亲爱的读者,我现在将拉着你一起去。

NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 

1、我的目标

基本上,我想采用 3D 形状并在 WebGL 中将其扭曲制作动画。具体来说,我想结合以下三方面的内容:

  • 输入形状:例如平面、球体、文件中的 3D 模型等。
  • 畸变函数:给定空间中的一个点,告诉我该点偏移多少
  • 材质:使用任何光照模型渲染形状(是否有光泽?是否用彩虹渐变着色?)

形状+变形+材质=无尽的时间沉没!

我为什么要这个?主要是因为它看起来很酷,但也因为它将是一个非常灵活的创意编码框架。输入形状+畸变函数+材质的不同组合可以产生各种各样的视觉结果。在 WebGL 中实时执行此操作而不是使用 Blender 节点执行此操作意味着它也可以是交互式的,这是一个优点。

不过,我不只是希望它能在 WebGL 中工作。我希望它能够与 p5.js 一起使用,p5.js 是一个非常通用的 Web 图形库。它比用于 3D 任务的 Three.js 之类的东西受到更多限制,但它使 2D 和 3D 画布之间的互操作变得更加容易。但是,p5 不提供用于在网格中传递附加顶点属性的 API。它可以通过直接挂钩底层 WebGL 调用来完成,但对我来说理想的解决方案只能利用顶点着色器中提供的标准信息:位置、法线、纹理坐标和对象/相机变换矩阵。

2、存在什么问题?

事实证明,采用任何形状+任何扭曲函数并使其适用于任何材料是相当困难的。该问题与表面法线有关。让我们快速回顾一下它们是什么以及它们为何如此重要。

2.1 使用法线渲染

在典型的 WebGL 渲染管道中,你通过顶点着色器发送形状信息,然后通过片段着色器发送形状信息,然后图像到达屏幕上。顶点着色器处理形状中的每个顶点,计算它在屏幕上的最终位置以及一些其他属性,例如该点的纹理坐标,以及法线,即该点远离表面的方向。在网格的每个三角形内部,这些属性会根据每个角的属性自动为每个像素进行插值,并发送到片段着色器,在其中它们用于查找像素的颜色。

顶点通过顶点着色器放置在屏幕上的位置,片段着色器计算这些顶点之间的每个像素的颜色。

在片段着色器中,法线对于计算着色非常重要。材质基色的亮度取决于表面是否面向光线:如果直接指向光线,则接收到完全直射光;当它偏离时,接收到的光线越来越少,直到它面向 90 度或更大,此时它接收不到直射光。法线告诉我们表面面向的方向,因此我们使用它来执行此计算。当表面具有反射性时,我们也会使用法线,因为它告诉我们表面的方向,以及光线如何从表面反射。

2.2 扭曲后法线

3D 模型通常带有为每个顶点预先计算的法线,并且顶点着色器不需要对它们做太多操作。如果对象已在场景中旋转,则可能需要将相同的旋转应用于法线,但除此之外,它只是将其传递给片段着色器。

不幸的是,当你扭曲形状而不是仅仅严格旋转它时,事情会变得更加复杂。想象一下,取一个平面,然后用正弦波扭曲它。未扭曲的平面的法线全部面向同一方向。扭曲的平面现在具有指向许多不同方向的表面,具体取决于它们落在波浪上的位置。

扭曲网格中的顶点后,原始法线可能不再正确

我们可以想象这个效果。这里我们有两个形状,左侧法线未更改,右侧法线已更改。这里的颜色代表它认为表面指向的方向:红色表示它指向左/右,绿色表示它指向上/下,蓝色表示它面向屏幕/面向屏幕外:

如果忽略由于扭曲而导致的法线变化,阴影看起来会明显错误。如果单击并拖动,你可以看到左侧未更改的法线斑点上的反射并没有按照预期的方式跟随凹凸:

3、简单的修复方法不起作用

所以我们知道我们必须对法线做一些事情。我们有什么选择?

3.1 自动片段着色器衍生品

核心问题是从顶点着色器发送的法线是错误的。那么,如果我们忽略它们并尝试完全在片段着色器中找出法线呢?

可以使用两个表面切线的叉积找到表面法线。如果表面位置 p 可以通过两个变量 u 和 v 以某种方式参数化,则表面法线为:

方便的是,在片段着色器中,我们可以通过 x 和 y 中的像素位置来参数化表面位置! GLSL 提供了函数 dFdx(some_variable) 和 dFdy(some_variable),它们可以查看水平相邻像素和垂直相邻像素中变量的值,并告诉你其值发生了多少变化。通过在位置变量上使用这些函数,我们可以获得表面在两个方向上的斜率。每个斜率都是沿 x 或 y 轴与表面相切的矢量。我们可以计算表面法线作为它们的叉积:

attribute vec3 position;
uniform vec3 baseColor;void main() {vec3 normal = normalize(cross(dFdx(position), dFdy(position)));// Now use the normal for lighting, e.g. Lambertian diffuse shadingvec3 toLight = vec3(0.0, 0.0, 1.0);float brightness = max(dot(normal, toLight), 0.0);gl_Fragcolour = vec4(brightness * baseColor, 1.0);
}

到目前为止,一切都很好!但我们的结果有问题。如果我们使用法线方向作为形状的颜色来为形状着色,效果如下:

我们看到的多面阴影是由于法线在表面上变化不平滑造成的。法线不会平滑变化,因为表面本身实际上并不平滑:它由三角形组成,我们从这些三角形中获得法线。表面之前看起来只是平滑的,因为 WebGL 管道正在为每个三角形上的每个像素插入从顶点着色器发送的法线。由于我们自己重新计算法线,因此我们在这里无法获得平滑的法线插值。

3.2 手动顶点着色器衍生品

好的,如果我们希望在到达片段着色器的过程中对法线进行平滑插值,我们需要在顶点着色器中计算法线。我们想要做与之前尝试的基本相同的事情,获取两个曲面切线的叉积,但这次是在顶点着色器中。

然而,在顶点着色器中,我们不再有 dFdx 和 dFdy 函数了!如果我们想要导数,我们就必须自己写!

这本质上并不是一个问题。没有任何技术限制可以阻止我们编写失真函数的导数,然后我们可以用它来计算调整后的法线。限制在于我的键盘和椅子之间。作为一项破冰活动,当人们问我想要什么超能力时,我的首选答案是我希望能够第一次、每次都正确地做代数。不幸的是,我没有这种超能力,所以当我必须为相对复杂的失真函数编写导数时,我会忘记某处的减号,并且我会浪费几个小时来调试它。因此,出于这个原因,我将否决将此称为“解决方案”。

4、核心方案:自动生成着色器代码

我否决手写导数不是因为计算导数是一门艺术,而是因为它实际上是一个必须遵循的相当严格的算法,而我易犯错误的人类大脑不擅长遵循严格的算法。但你知道遵循严格算法的好处是什么吗?一台电脑。那么,如果我们让计算机来编写导数呢?

4.1 自动微分架构

计算函数相对于某个变量的导数是一种相当简单的递归算法。这是你可能在高中时学到的。每个基本操作都有一个预定义的导数。

例如, ∂sin(x)/∂x=cos(x)。如果某个运算应用于某个更复杂的函数 u,则我们使用递归链式法则,将正弦运算的导数乘以 u 的导数:  ∂sin(u)/∂x = cos (u) ∂u/∂x。你只需继续深入工作,从最外层的操作向内进行,直到你无法进一步分解为止。

以面向对象的方式对此进行建模,每个操作都将是一个知道如何计算其自身导数的对象。它的参数本身可以是导数运算。像  0.1 * (sin(10*x + 20))^2 这样的表达式将转换为如下所示的对象层次结构:

0.1 * (sin(10*x + 20))^2 的语法树细分

sin 运算的实现可能如下所示,其中包含生成表示其值的代码以及表示其导数值的代码的方法,这两者都在其参数上递归调用函数:

class Sin {constructor(u) {this.u = u;}code() {return `sin(${u.code()})`}derivativeCode() {`cos(${u.code()}) * ${u.derivativeCode()}`}
}

然后,你可以通过拼接 derivativeCode()的输出来在着色器中使用它:

const sinOp = new Sin( /* TODO build up tree of operations here */ );
const vert = `uniform mat4 uModelViewMatrix;uniform mat4 uProjectionMatrix;attribute vec3 position;void main() {vec3 newPosition = position;newPosition.y += ${sinOp.code()};float deriv = ${sinOp.derivativeCode()};// TODO do something with deriv heregl_Position = uProjectionMatrix * uProjectionMatrix * vec4(newPosition, 1.0);}
`;

然后,如果你通过构建运算树编写想要求导数的代码,还可以免费生成计算导数的代码!

当然,通过构造代表每个操作的对象来编写代码可能会很乏味。我直接知道这一点:我在滑铁卢读本科时的第四年设计项目想法的早期原型让我这样做,这是我们放弃该项目想法的主要原因之一。

一种选择可能是让你将 GLSL 代码编写为字符串,将该字符串解析为语法树,然后将语法树中的每个节点映射到一个操作。这个选项很好,但实施起来需要大量工作。作为妥协,我选择使用类似构建器的设计模式和可链接的方法,以使编写变得不那么冗长。由于操作对象也是普通的 Javascript 对象,因此它们也可以在循环和条件中分配和重新分配 —从生成的 GLSL 的角度来看,使用的任何 Javascript 代码都像宏一样。

我创建的库可在 NPM 上以 @davepagurek/glsl-autodiff 的形式获取。这也是本页上“正确法线”示例的动力!它允许你编写一些表达式,然后根据输入变量获取这些表达式的导数。以下是使用最终库的片段:

import { gen } from '@davepagurek/glsl-autodiff';const vert = `void main(void) {vec4 objSpacePosition = vec4(aPosition, 1.0);float x = objSpacePosition.x;float y = objSpacePosition.y;${gen((ad) => {const x = ad.param('x');const y = ad.param('y');const time = ad.param('time');const speedX = 1.5;const speedY = 2.8;let offset = ad.val(0);for (let i = 0; i < 3; i++) {offset = offset.add(ad.sin(ad.sum(offset.mult(0.5).add(x.mult(speedX)).add(y.mult(speedY)),time.mult(0.002),)));}offset = offset.mult(0.1);offset.output('z');offset.outputDeriv('dzdx', x);offset.outputDeriv('dzdy', y);})}objSpacePosition.z = z;vec3 tangentX = vec3(1.0, 0.0, dzdx);vec3 tangentY = vec3(0.0, 1.0, dzdy);vNormal = uNormalMatrix * normalize(cross(tangentX, tangentY));vec4 worldSpacePosition = uModelViewMatrix * objSpacePosition;gl_Position = uProjectionMatrix * worldSpacePosition;}
`;

5、我们实际上用这些衍生品做什么?

太棒了,我们现在可以编写一个失真函数并自动获得导数。我们如何利用这些衍生品来获得我们一直在拼命寻找的常态?

5.1 使平面变形

我从一个简单但仍然实用的案例开始:在一维上扭曲平面。

给定平面上的 x 和 y 位置,我将提供一个生成新 z 值的函数。这个设置是一个很好的起点,因为我们可以通过获取 x 和 y 的导数来轻松得出两个曲面切线,就像我们在片段着色器中所做的那样。

使用 glsl-autodiff,我们可以将以下内容拼接到顶点着色器中,以获得 x 和 y 中的位移及其导数:

gen((ad) => {const position = ad.vec3Param('position');// Imagine we have a displace() function, generating offset based on x and yconst offset = displace(position.x(), position.y());offset.output('offset');offset.outputDeriv('doffset_by_dx', position.x());offset.outputDeriv('doffset_by_dy', position.y());
})

现在我们需要从中获得表面法线。如果平面表面上的原始位置为 p,新的偏移量为 k,则扭曲位置为 p′= p + [0 0 k]T。由于我们的输入是一个平面,我们还知道原始位置相对于 x 和 y 的导数相当简单(在这种情况下,x 和 y 导数只是分别指向 x 或 y 方向的单位向量)。正常情况下的数学计算结果如下:

由于位移直接给出了两个表面切向量,因此我们可以通过将它们交叉来轻松获得法线。

这是使用自动差异输出变量 doffset_by_dx 和 doffset_by_dy 在着色器的其余部分中的外观:

// Use the offset
vec3 outPosition = position;
outPosition.z += offset;// The normal is the cross product of two surface tangents, which are
// the changes in z given a unit change in our two surface directions
vec3 tangentX = vec3(1., 0., doffset_by_dx);
vec3 tangentY = vec3(0., 1., doffset_by_dy);
vec3 normal = normalize(cross(tangentX, tangentY));

5.2 使具有现有法线的形状变形

当你在开始时并不平坦的输入形状上使用相同的 x, y => z 位移时,事情会变得更加复杂。平面恰好很容易由 x 和 y 参数化,让我们很容易获得方向导数,但任何任意输入形状可能都不是。

仔细阅读维基百科有关计算表面法线的文章并没有多大帮助,因为它主要关注由函数定义的形状。如果我们有一个定义完整形状的函数,我们可以对该函数对两个参数求导。但在顶点着色器内部,除了两条信息之外,我们无法对表面的形状做出任何假设:当前顶点的位置及其现有法线。我们只有形状位移的函数。 Morten Mikkelsen 于 2010 年发表的一篇论文很接近:它描述了如何计算任意表面上任意凹凸贴图的法线,但仅限于使表面沿表面法线方向变形不同量的凹凸贴图。在我们的例子中,我们的位移方向可能与表面法线方向完全无关。

我找到的解决方案是计算位移引起的法线的旋转,然后将其应用于输入法线。对于给定的位置,想象我们从一个平面开始,然后扭曲该平面。我们知道平面的法线从指向 z 方向开始,然后最终指向 cross(tangentX, tangentY) 方向。我们可以计算这两个向量之间的旋转,然后将相同的旋转应用于非平面网格的原始法线!

如果我们想象一个平面正在变形,我们可以计算变形引起的法线旋转。然后我们可以将相同的旋转应用于任何其他形状的初始表面法线。

任意两个向量 v1 和 v2 之间的角度为 cos−1(v1⋅v2)。在这种情况下,旋转轴将是垂直于两个输入向量的向量,我们可以通过 v1×v2 得到它。使用这个轴和角度,我们可以构造一个旋转矩阵,它将相同的旋转应用于与之相乘的任何向量。这是 GLSL 中的样子:

// Assume we have a function to generate an axis/angle rotation matrix:
mat4 axisAngleRotation(vec3 axis, float angle) { /* ... */ }
// See http://www.neilmendoza.com/glsl-rotation-about-an-arbitrary-axis for an implementation// Compute a normal like before, as if the mesh were a plane
vec3 tangentX = vec3(1., 0., doffset_by_dx);
vec3 tangentY = vec3(0., 1., doffset_by_dy);
vec3 displacedPlaneNormal = normalize(cross(tangentX, tangentY));// The un-displaced normal for our hypothetical plane. This is the result
// of crossing the undistorted surface normals: cross(vec3(1.,0.,0.), vec3(0.,1.,0.))
vec3 originalPlaneNormal = vec3(0., 0., 1.);// Find the rotation induced by the displacement
float angle = acos(dot(originalPlaneNormal, noDisplacementNormal));
vec3 axis = normalize(cross(originalPlaneNormal, noDisplacementNormal));
mat4 rotation = axisAngleRotation(axis, angle);// Apply the rotation to the original normal
vec3 normal = (rotation * vec4(origNormal, 0.)).xyz;

5.3 使形状在三个维度上变形

我们需要做出的最后一步是支持不仅仅是 z 轴上的扭曲,将完整的顶点位置作为输入并生成完整的 3D 向量作为输出(我将其称为矢量k,而不是我们的标量 k)之前)。

由于我们需要获得两个曲面切线的叉积,因此我们将再次需要两个方向导数。以前,由于失真仅取决于 x 和 y,因此用于方向导数的明显轴是 x 和 y。但由于我们现在依赖于 x、y 和 z,因此我们需要导数来考虑所有这三个因素。解决这个问题的一种方法是使用 x 和 u 作为轴,其中我们定义 u=y+z。

为了获得关于 u 的导数,我们可以查看位移函数的雅可比行列式,其中 J=[∂k/∂x ∂k/∂y ∂k/∂z],每个偏导数都给我们一个现在位移 k 是一个 3D 向量。雅可比矩阵与方向向量的点积给出了该方向向量的方向导数。在我们的例子中,关于 u 的导数最终只是 y 和 z 导数之和:

现在我们可以用它来进行数学计算以获得正常值。平面相对于 u 的导数又很简单,就是 [0 1 1]。正常的结果如下:

我向自己证明,这与我们的一维变形法线一致,因为以前,我们的斜率向量具有轴向量加上由于该轴的变化而产生的位移的形式。之前我们的位移恰好位于与我们方向正交的轴上,例如 x 轴斜率 [1 0 0]T + [0 0 ∂k/∂x]T。碰巧的是,现在我们的位移向量可能在方向向量的轴上具有非零分量,但我们仍然具有相同的形式:[1 0 0]T + ∂k/∂x。

此计算的 GLSL 版本看起来与之前非常相似:

// doffset_by_d{x,y,z} are all vec3s now instead of a floats
vec3 tangentX = vec3(1., 0., 0.) + doffset_by_dx;
vec3 tangentYZ = vec3(0., 1., 1.) + doffset_by_dy + doffset_by_dz;
vec3 displacedPlaneNormal = normalize(cross(tangentX, tangentY));// The un-displaced normal for our hypothetical plane
vec3 originalPlaneNormal = normalize(cross(vec3(1., 0., 0.), vec3(0., 1., 1.));// Everything from here on is the same as before!// Find the rotation induced by the displacement
float angle = acos(dot(originalPlaneNormal, noDisplacementNormal));
vec3 axis = normalize(cross(originalPlaneNormal, noDisplacementNormal));
mat4 rotation = axisAngleRotation(axis, angle);// Apply the rotation to the original normal
vec3 normal = (rotation * vec4(origNormal, 0.)).xyz;

...就是这样!这就是我们需要的着色器代码,以实现采用任意形状、扭曲和材质并使法线正常工作的目标。

6、用它做点事情

现在我们已经构建了一个失真管道,让我们实际用它做一些事情!因为我刚刚让一切正常工作(再加上一两周的数学计算,以说服自己这种方法实际上有一些正确工作的合理理由),所以我只会你您留下一个使用它的草图。但预计这会巧妙地包含在未来的项目中!

这是一个草图,我将称之为“飞机”!其灵感大致来自 1980 年经典电影的电影海报。如果你想检查源代码,可以在 p5 Web 编辑器上找到它。


原文链接:3D模型实时变形算法 - BimAnt

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DS:顺序表、单链表的相关OJ题训练(2)

欢迎各位来到 Harper.Lee 的学习世界&#xff01; 博主主页传送门&#xff1a;Harper.Lee的博客主页 想要一起进步的uu欢迎来后台找我哦&#xff01; 一、力扣--141. 环形链表 题目描述&#xff1a;给你一个链表的头节点 head &#xff0c;判断链表中是否有环。如果链表中有某个…

HTML学习|网页基本信息、网页基本标签、图像标签、超链接标签、列表标签、表格标签、媒体元素、页面结构分析、iframe内联框架

网页基本信息 DOCTYPE是设置使用什么规范&#xff0c;网页整个信息都在html标签中&#xff0c;head标签里包含字符集设置&#xff0c;网页介绍等信息&#xff0c;title标签是网页的名称&#xff0c;网页的主干都在body标签中 网页基本标签 标题标签 h1~h6都是标题标签&#x…

2024数维杯数学建模A题B题C题思路+模型+代码(开赛后第一时间更新)

2024数维杯数学建模A题B题C题思路模型代码&#xff08;开赛后第一时间更新&#xff09; https://mbd.pub/o/bread/ZpWakpdq https://mbd.pub/o/bread/ZpWakpdq 2024年第九届数维杯大学生数学建模挑战赛参赛规则 竞赛要求及论文提交方式; ①本次参赛作品统一在线提交到竞赛…

软件需求工程复习——知识点

一&#xff0c;单选题&#xff08;20分&#xff09; 二&#xff0c;判断题&#xff08;10分&#xff09; 三&#xff0c;简答题&#xff08;20分&#xff09; 四&#xff0c;分析与设计题&#xff08;50分&#xff09; 一、导论 1、应用型软件的模拟特性 应用型软件在“模拟”…

基于无监督学习算法的滑坡易发性评价的实施(k聚类、谱聚类、Hier聚类)

基于无监督学习算法的滑坡易发性评价的实施 1. k均值聚类2. 谱聚类3. Hier聚类4. 基于上述聚类方法的易发性实施本研究中的数据集和代码可从以下链接下载: 数据集实施代码1. k均值聚类 K-Means 聚类是一种矢量量化方法,最初来自信号处理,旨在将 N 个观测值划分为 K 个聚类,…

Web 安全漏洞之文件上传

目录 文件上传漏洞及危害 文件名 HTML 和 SVG 软链 服务器磁盘 防御方法 网络安全学习路线 &#xff08;2024最新整理&#xff09; 学习资料的推荐 1.视频教程 2.SRC技术文档&PDF书籍 3.大厂面试题 特别声明&#xff1a; 文件上传漏洞及危害 文件上传漏洞…

基于百川大语言模型的RSS新闻过滤应用【云服务器+公网网页,随时随地看自己DIY订阅的新闻内容】

背景 目前从公众号、新闻媒体上获得的新闻信息,都是经过算法过滤推荐的,很多时候会感到内容的重复性和低质量,因为他们也要考虑到自己的利益,并非完全考虑用户想要的、对用户有价值的信息。这时,如果要获取自己认为重要的信息,定制化开发自己的筛选算法更好。 效果 素材…

计算机网络(第八版 谢希仁 编著) 期末复习大纲

一.每章总结 第一章&#xff1a;分组交换&#xff0c;计网定义、范围划分&#xff0c;性能指标&#xff0c;五层体系结构&#xff0c;TCP/IP体系结构 第二章&#xff1a;物理层&#xff0c;码元&#xff0c;基带调制(数字信号->数字信号&#xff0c;也叫编码)&#xff0c;带…

本来还挺喜欢……

前阵子买了个天空星开发板&#xff0c;到手之后发觉不对劲。 之前我们玩玩开发板都是用的面包板的&#xff0c;就算是ESP那种比较宽的板子用两个面包板拼一下也勉强可以用。 但是天空星它的引脚是分为两组&#xff0c;每组有两排&#xff0c;如果我们还是直接使用面包板的话&a…

浅析安全用电监控系统在工厂的研究与应用论述

摘 要&#xff1a;随着社会时代的发展&#xff0c;人们的安全意识越来越强烈&#xff0c;在人们生活和工作中离不开各种用电设备&#xff0c;用电设备的安全使用是保障人们生命安全的重要内容。工厂因自身厂内工作环境的特殊性&#xff0c;用电设备的种类多且复杂&#xff0c;如…

Meilisearch vs Elasticsearch

2个搜索引擎的比较&#xff08;官方说法&#xff09;&#xff1a;Meilisearch vs Elasticsearch Elasticsearch 做为老牌搜索引擎&#xff0c;功能基本满足&#xff0c;但复杂&#xff0c;重量级&#xff0c;适合大数据量。 MeiliSearch 设计目标针对数据在 500GB 左右的搜索需…

【知识碎片】2024_05_13

本文记录了两道代码题【自除数】和【除自身以外数组的乘积】&#xff08;利用了前缀积和后缀积&#xff0c;值得再看&#xff09;&#xff0c;第二部分记录了关于指针数组和逗号表达式的两道选择题。 每日代码 自除数 . - 力扣&#xff08;LeetCode&#xff09; /*** Note: T…

软件2班20240513

第三次作业 package com.yanyu;import java.sql.*; import java.util.ResourceBundle;public class JDBCTest01 {public static void main(String[] args) {ResourceBundle bundle ResourceBundle.getBundle("com/resources/db");// ctrl alt vString driver …

Unity打开安卓设备不同的设置面板

1&#xff0c;打开安卓设备不同的设置面板&#xff0c;我还贴心的把Android官网的链接放下面了 2&#xff0c;使用也很方便&#xff1a;unity按钮事件上拖这个脚本&#xff0c;注册MyOpenAndroidSettings方法&#xff0c;参数 填 和枚举值相应的数字 // 功能&#xff1a;打开…

贪吃蛇游戏(C语言实现)

目录 游戏效果展示文件代码的展示test.cSnake.cSnake.h 下一个坐标不是食物 游戏效果展示 QQ录屏20240507163633 文件 代码的展示 test.c #define _CRT_SECURE_NO_WARNINGS#include<locale.h> //设置本地化 #include"Snake.h"//游戏的测试逻辑 void test() {…

2024最佳项目管理软件整理

在项目管理领域&#xff0c;选择合适的任务管理软件对于提高团队效率和确保项目顺利进行至关重要。 以下是一些优秀的项目任务管理软件&#xff0c;以及它们的官网地址和特点介绍 序号软件名称官网地址特点介绍1zz-planhttps://zz-plan.com/一款在线甘特图协作软件&#xff0c…

c# - - - winform程序四个角添加圆角效果

winform 给窗体四个角添加圆角效果。 在窗体 Load 事件中添加如下代码&#xff1a; // 创建了一个圆角矩形的路径&#xff0c;并将其设置为控件的形状 System.Drawing.Drawing2D.GraphicsPath path new System.Drawing.Drawing2D.GraphicsPath(); int radius 30; path.AddAr…

ssm123基于java web的网上书城系统的设计与实现+vue

基于java web的网上书城系统的设计与实现vue 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff0c;商品交易当然也不能排除在外&#xff0c;随着商品交易管理的不断成熟&#xff0c;它彻底改变了…

基于FPGA的视频矩阵切换方案

一、单个显示设备的系统方案&#xff1a;会议室只有1个显示设备 会议室的信号源有很多&#xff0c;但是显示设备只有1个&#xff0c;这个时候最佳方案是使用切换器。 &#xff08;1&#xff09;切换器&#xff08;控制方式&#xff1a;遥控器、软件、机箱面板、中控&#xff…

DS高阶:B树系列

一、常见的搜索结构 1、顺序查找 时间复杂度&#xff1a;O(N) 2、二分查找 时间复杂度&#xff1a;O(logN) 要求&#xff1a;&#xff08;1&#xff09;有序 &#xff08;2&#xff09;支持下标的随机访问 3、二叉搜索树&#xff08;BS树&#xff09; 时间复杂…