Scratch 详解 流畅光线追踪(盲区)引擎:角度 + 一次函数 + 区域判断

news2024/11/24 16:21:29

【提示1】本文将全程使用原版积木实现这一功能,请不要在评论区发送扩展中有相应积木。若喜欢使用扩展,可以自行将本文介绍的方法用扩展积木替代。

【提示2】本文中代码里出现的所有最后带*号的变量,均为私有变量!


正文

        近日,我看见有些大佬的“光线追踪”类作品。这些个作品都是通过向周围的空间扫描墙壁,来确定盲点与可视点的边界。这意味着,程序的运行效率不会很高。并且,这种方法必须有实际的屏幕渲染才能使用,当一些使用情境不再依靠屏幕渲染时,这种方法就不能使用了。

        这个时候,我们就需要角度+函数+区域判断来完成这一目标。

        在接下来的文章中,我将会介绍两种使用场景下的不同程序效果的编写方式,其区别在于一种是区域判断,另一种是阴影覆盖;但两种方法都有相同的核心:用角度、一次函数进行区域划分。


目标效果图(无需任何线条,图中加上线条是为了让程序更加可观)

【先行概述】本运行效果需积木数量约300。运行原理为:通过玩家与墙角的位置,计算出划分“盲区”与“ 可视区 ”的角度界线;再由地图中的物体自行判断是否在盲区中:是则隐藏,否则显示。
【优点】没有任何屏幕渲染,全盲算,运用于隐式盲区的场景(如联机枪战的小地图)。【缺点】整个物体会以一个中心点判断是否隐藏;物体越多运行越卡。

步骤1 | 理清思路

        简单以一个正方形作为一个障碍物,这个障碍物会遮挡玩家的视线(如下图阴影部分)。

        那这个阴影部分,也就是盲区,和玩家的位置有什么联系呢?可以看见,这个障碍物有四个“墙角”(下图中标红点位置):

        不妨将玩家与墙角连接起来,我们发现:连接的线,恰好和“划分盲区与可视区的线”重合了。如下图:

        这下就好办了。“划分盲区与可视区的线”可以被很轻松地确定下来了!我们在图中标上几个点吧:

        不难发现规律:图中可以看见,物体1位于锐角∠EPF内部,是被遮挡的;而物体2位于锐角∠EPF外部,没有被遮挡。

那要如何判断是否位于角的内部呢?

        如下图,连接物体和玩家,即连PO。PO将∠APD分成了两个部分:∠APO和∠DPO。

        将∠APD记为θ,∠APO记为α,∠DPO记为β。当物体被遮挡,即在∠APD内部的时候,α+β=θ。

        仍然将∠APD记为θ,∠APO记为α,∠DPO记为β。如下图,当物体被遮挡,即在∠APD部的时候,α+βθ。

        这样一来,我们便有了判断物体是否被遮挡的方法。


步骤2 | 计算角度,判断物体是否可见

*提示:下文出现的“缓存列表”相关名称,是本人的代码习惯。缓存列表主要用于:存储临时变量、存储计算参数、自定义积木返回值 等功能。

如何计算α、β、θ?

        已知玩家位置、墙角位置、物体位置,我们就可以分别计算出:玩家面向[墙角]的角度r、玩家面向[逆时针相邻的墙角]的角度s、玩家面向[物体]的角度t。(角度r、s、t即Scratch的方向

        在上方的图中,可以看到:α=|t-r|、β=|s-t|、θ=|s-r|。

        玩家面向[墙角]的角度r、玩家面向[逆时针相邻的墙角]的角度s、玩家面向[物体]的角度t,都可以用下面这个积木算出来——[由 x1 y1 面向 x2 y2 的方向]:

        可以用反三角函数——atan计算。我使用了勾股+asin,可以少处理一些sc的角度问题。关于三角函数的使用以及正负转化,这里不多赘述,直接上代码:

由 x1 y1 面向 x2 y2 的方向

        那么,我们就可以写下这样的代码,来判断物体是否在盲区中:计算玩家面向一个墙角的方向,返回值保存在缓存1中;计算玩家面向相邻墙角的方向,返回值保存在缓存2中;计算玩家面向物体的方向,保存在缓存3中。然后分别将它们相减,算出α、β、θ的值,如下图:

计算α β θ

        接下来,如何判断α+β=θ?

        我们已经算出α、β、θ,不过值得一提的是,Scratch算数可不是那么准!好不容易算出的α、β、θ,他们的在精度上却很不幸地显出差异!本应相等的值,它们相差了0.00000000000001:

精度丢失

所以,我们应当避开这样的精度问题,做一点点小小的容错:将它们相减,得出它们相差的值,判断差是否很小,就如——

作差,进行精度容错

最后,把它作为物体是否可见的判断依据:

最后一块积木判断物体是否可见

Awesome!让我们来运行一下——(为了使程序更加直观,我连接了一些线段并延长,且显示出α、β、θ的大小(但是四舍五入了),实际的使用场景无需)

成功了!

        🎉🎉🎉恭喜,我们发现,程序已经可以根据位置来计算物体是否位于盲区中了!

        可是……糟了,Scratch方向角度是从-180到180的,

Scratch方向

        也就是说,会有一种特殊情况——当我们要算-170方向的线与170方向的线,本结果应是20,却算出340:

角度计算错误

        让我们来处理一下这种情况


步骤3 | 处理Scratch角度制的特殊情况

        之所以会把上面那个角算成340度,无非是因为Scratch的方向是从-180开始,180结束的。如果我们能让原本的-170变成190,即方向从0开始,360结束,那不就可以正确地计算出20°了吗?

        其实很简单,我们用:方向除以360的余数,就可以把所有奇奇怪怪的方向全都转换到0-360的区间:

        这样,我们就可以算出正确的20°:|170-190|=20。

        到了这里,可能有人就会问:“如果用你这个方法算20和-20(340)的角度差,本应该算出40,却又变成了|340-20|=320,不是会有一样的问题吗?”

        没错。所以不管是什么角度,我们都用两种方法各计算一遍,哪个算得的角度差小,我们就取哪个:

        用两种方法各计算一遍,哪个算得的角度差小,我们就取哪个

        这样一来,我们就成功解决了Scratch中角度差的计算问题。


步骤4 | 判断玩家与物体 是否在墙面的同侧

        等等!是不是有一块位于夹角内的地方,也是可以看得见的呢?

        如下图,像这样的情况:

位于夹角内,但在墙前方的物体,是可见的!


        判断一个点在夹角内之后,与玩家位于墙壁AD同侧的物体仍旧是可见的:

夹角内,同侧可见,异侧不可见

        要判断两点是否位于直线的同侧,使用一次函数是个不错的选择。

*提示:下方为推导过程,可直接空降到代码处

        设直线AD的解析式为:y=kx+b。A和D的坐标是已知的,我们就可以使用斜率公式:k=Δy/Δx,即 k = [ y(D) - y(A) ] / [ x(D) - x(A) ] 。现在还差常数项b未知,我们简单做个移项,把等式化为b=y-kx,然后把点A的坐标(D也可以)和刚刚算出的k代入b=y-kx,得到 b = y(A) - k*x(A)。现在,b和k都已经算出,AD的解析式就得到了。

        初中数学我们学过,将平面直角坐标系中任意一点Q(m,n)的横坐标x=m代入函数解析式,会得到一个新的、唯一的y1值。若:n<y1,则这个点Q在函数图像的下方,反之则在上方。如下图:

        延长墙面AD。只需要判断:玩家与物体是否同时位于 墙壁所在直线AD的上方 或 是否同时位于下方 即可。

延长AD(红色直线)

*提示:接下来的代码有简化方案,但为方便理解,这里不做简化处理

        设AD:y=kx+b,根据上面的公式 k=Δy/Δx 和 b = y - kx ,计算出AD的解析式:

计算墙解析式中的参数k和b

        值得注意的是,当这条直线垂直于x轴的时候,斜率 k 的值将会是正负 Infinity 。这意味着,这条直线的参数 k 和 b 在接下来的运算中会出错,得到NaN。

        所以在算出 k 后,如果是±Infinity,我们需要将它转换成一个非常非常大的数,这样就能在误差极其微小(以致于忽略不计)的情况下,保证接下来的计算不会出错,如下:

斜率更正

        直线解析式已经解决。判断玩家和物体是否位于直线同侧,我们将玩家的x坐标物体的x坐标分别代入解析式:y=kx+b

        得到直线上的y1和y2的实际意义如下图所示(以玩家和物体都在直线下方为例):

        (图以玩家和物体都在直线下方为例)此时:玩家y小于y1,物体y小于y2,二者皆为小于,位于同侧,物体可见。

        (若玩家和物体都在直线方)玩家y大于y1,物体y大于y2,二者皆为大于,位于同侧,物体可见。

        (若玩家和物体位于直线一上一下)玩家y小(大)于y1,物体y大(小)于y2,一个是大于,一个是小于,位于异侧,物体不可见。

        所以我们判断:<玩家y 小于 y1> 和 <物体y 小于 y2>

        ①若两个都是true,则表示玩家和物体同在直线下方(同侧);若两个都是false,则表示玩家和物体都在直线上方(也是同侧);若一个true一个false,则表示玩家和物体:一个在直线上方,一个在直线下方(异侧)。

        所以我们将这两个条件放在等号中,若它们同是true或同是false,等号就会返回true,表示同侧;若它们一个是true一个是false,等号就会返回false,表示异侧。即同或

        新建一个积木,名为判断同侧。将这些判断是否位于墙壁直线同侧的代码拼接起来,放在这个自制积木的定义中。

        最后,在判断物体是否可见的代码前,插入这个自制积木,然后在下方写入同侧可见异侧不可见:

        完美!把上面判断是否可见的代码段,放入克隆体的重复执行中。代码全览(部分自制积木已在上文出现或是本人习惯,下图未放出定义):

        克隆出两个物体看看效果如何(连线是额外加的):

        太棒了!

        和预想的效果一模一样,wonderful!


步骤5 | 添加、读取更多的墙

        这样的盲区肯定不能只有一面墙嘛!现在我们来添加更多的墙。

        创建两个列表,名为“墙角索引”“墙角详细信息”

        先来看列表“墙角索引”。这里需要存储的数据是:墙角的名称及顺序。因为要存储多组障碍物,我们需要在一个障碍物结束的位置加一个特殊标志(序号.1145)(序号是整数,第一个障碍物开始标识为0.1145,结束的标识为1.1145;同时1.1145也是第二个障碍物的开始标识)。

        墙角顺序的存储格式,可存储多组障碍物的墙角

        在另一个“墙角详细信息”的列表中,按照一定的格式,根据墙角名称,录入墙角位置:

存储方式

        现在,新建一个变量 i ,用于遍历 墙角索引:

        我们的目的是将每个墙角都遍历过去。当 i 指向列表中代表 墙角 的项时,正常计算是否可见;当 i 指向列表中代表 不同障碍物间标识符 的项(序号.1145)时,不进行任何操作。新建积木“物体判断是否可见”(!勾选“运行时不刷新屏幕”),在下方按照所需,接入遍历相关代码,如图:

        将克隆体循环中(除渲染部分)的代码,即判断是否可见的代码,放入“否则”分支里:

        通过遍历,我们需要改变的是当前墙角逆时针相邻墙角的坐标值:

        需要改变 当前墙角(墙角1) 和 逆时针相邻墙角(墙角2) 的坐标值

        当前墙角的值可以这样读取:

        当前墙角坐标的读取方式

        下一个墙角的名称可以这样获取:

        相邻墙角名称的获取

        但是——若是末尾的墙角,比如 wall1-4 ,这样会读取到障碍物标识符(序号.1145),怎么办呢?

        这时候, .1145 前面的序号就起作用了。创建一个积木“读取相邻墙角名称”,在这里面放入读取代码。(若下一项不是标识符,即不包含.1145,则正常返回下一项的内容;若下一项是标识符,则需找到该障碍物的第一个墙角,要返回上一个标识符 (序号-1).1145 的项+1的墙角名称内容)。

        需要注意的是,不能直接用1.1145减去1得到0.1145,因为你得到的可能是0.11450000000000005。所以为了准确,上图先向下取整,-1,再连接.1145。

        接着,将这个获取名称的代码放入刚刚改变墙角2数据的前面,再从墙角详细信息中读取出坐标:

        别忘了在判断是否可见的位置后面加上:判断到false的时候,就立即停止继续判断(防止接下来的墙面又判断为可见)。

        停止自制积木,在第一次判断到false时就终止判断

        现在,我们来运行一遍:

运行效果(可见的亮,不可见的暗)


完成啦ヾ(^∀^)ノ!本方法所有代码如下图~

所有代码

        以上就是“区域判断法”的所有内容啦!

        阴影覆盖法,真的是超流畅的光线追踪好嘛!

        其实阴影覆盖法差不多啦~就是在区域判断法的基础上,把物体自判断删掉,然后一面一面墙绘制四边形阴影盖住而已。可以自己试试呀

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C数据结构——无向图(邻接矩阵方式) 创建与基本使用

源码注释 // // Created by Lenovo on 2022-05-13-上午 9:06. // 作者&#xff1a;小象 // 版本&#xff1a;1.0 //#include <stdio.h> #include <malloc.h>#define MAXSIZE 1000 // BFS队列可能达到的最大长度 #define MAX_AMVNUMS 100 // 最大顶点数typedef enu…

用SpringBoot实现post和get请求(多图)

用SpringBoot实现post和get请求&#xff08;多图&#xff09; 用SpringBoot实现post和get请求创建SpringBoot工程创建controller验证FAQ创建项目后依赖报错Project org.springframework.boot:spring-boot-starter-parent:3.1.2.RELEASE not found more 用SpringBoot实现post和g…

AQS构建锁和同步器的框架

1.概述 AQS全称AbstractQueuedSynchronizer&#xff0c;此类在java.util.concurrent.locks包下面&#xff0c;是一个构建锁和同步器的框架&#xff0c;比如ReentrantLock就是基于AQS来实现的。 2.AQS实现原理 AQS内部有一个由volatile修饰(保证其可见性)的变量state&#xf…

PDF文件忘记密码,怎么办?

PDF文件设置密码分为打开密码和限制密码&#xff0c;忘记了密码分别如何解密PDF密码&#xff1f; 如果是限制编辑密码忘记了&#xff0c;我们可以试着将PDF文件转换成其他格式来避开限制编辑&#xff0c;然后重新将文件转换回PDF格式就可以了。 如果因为转换之后导致文件格式…

Tuxera NTFS2023Mac强大的Mac读写工具

Mac用户在使用NTFS格式移动硬盘时&#xff0c;会遇到无法写入硬盘的情况。要想解决无法写入的问题&#xff0c;很多人选择使用Mac读写软件。面对市面上“众多”的读写硬盘软件&#xff0c;用户应该怎么选择呢&#xff1f;初次接触移动硬盘的伙伴可能不知道移动硬盘怎么和电脑连…

RabbitMQ 教程 | 第6章 RabbitMQ 配置

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

科大讯飞 - 基于论文摘要的文本分类与关键词抽取挑战赛(DataWhale-Camp)

文章目录 1、赛题信息2、解决方案2.1 飞桨Baseline&#xff08;提供代码&#xff09;2.2 Bert和调参2.3 chatGLMlora大模型 3、关于DataWhale-NLP 1、赛题信息 提交地址&#xff1a;https://challenge.xfyun.cn/topic/info?typeabstract-of-the-paper&chymfk4uU 项目题目…

我的会议(会议通知)

前言: 我们在实现了发布会议功能&#xff0c;我的会议功能的基础上&#xff0c;继续来实现会议通知的功能。 4.1实现的特色功能&#xff1a; 当有会议要参加时&#xff0c;通过查询会议通知可以知道会议的内容&#xff0c;以及当前会议状态&#xff08;未读&#xff09; 4.2思路…

在Linux中怎么查找文件

2023年8月1日&#xff0c;周二上午 目录 Linux的四种搜索命令find简要说明举例说明拓展阅读locate 简要说明举例说明whereis简要说明举例说明which简要说明举例说明 Linux的四种搜索命令 findlocate&#xff08;不一定内置有&#xff0c;可能要下载mlocate包&#xff09;wher…

【概念理解】HAL库的滴答定时器HAL_Delay()函数的实现原理

来源&#xff1a;bilibili视频 这里写目录标题 概述一、寄存器部分1. 控制和状态寄存器(STK_CTRL)2. 加载值寄存器&#xff08;STK_LOAD&#xff09;3.当前值寄存器&#xff08;STK_VAL&#xff09; 二、代码部分hal_delay()1. hal_initTick()滴答定时器的初始化2. 将七万二传…

Redis 客户端有哪些?

文章目录 JedisLettuceRedisson最佳实践 - 到底用哪个&#xff1f; Redis 最常见的 Java 客户端有两个&#xff0c;Jedis 和 Lettuce&#xff0c;高级客户端有 Redisson&#xff0c;见下图&#xff08;图源 Clients | Redis&#xff09; Jedis Github地址&#xff1a;redis/j…

Windows下安装Hive(包安装成功)

Windows下安装Hive Hive与Hadoop的版本选择很关键&#xff0c;千万不能选错&#xff0c;否则各种报错。一、Hive下载1.1、官网下载Hive1.2、网盘下载Hive 二、解压安装包&#xff0c;配置Hive环境变量2.1、环境变量新增&#xff1a;HIVE_HOME2.2、修改Path环境变量&#xff0c;…

Oracle免费在线编程:Oracle APEX

前提&#xff1a; 注意&#xff1a;你要有个梯子才能更稳定的访问。 不需要安装Oracle&#xff0c;但是需要注册。&#xff08;还算方便的&#xff09; 注册&登录过程 进入Oracle APEX官网&#xff0c;我们选择免费的APEX工作区即可&#xff0c;点击“免费注册”。在注册…

基于H5或者微信小程序开发GIS地图实战全套代码

1 下面有一定基础的可以不看 (1)第一篇请看 微信小程序开发天地图 (2)第二篇请看 http://GeoServer+PostgreSQL+PostGIS+Tomcat+QGIS一整套相关 (3)第三篇请看 有国产化需求的 (4)第四篇请看 支持国家EPSG:4490 2 vue+openlayers实例代码

ThreadLocal原理

ThreadLocal原理 ThreadLocal对象new出来存放到堆中&#xff0c;ThreadLocal引用是存放在栈里 Thread 类有个 ThreadLocalMap 成员变量&#xff0c;Map的key是Threadlocal 对象&#xff0c;value是你要存放的线程局部变量。 public void set(T value) {//获取当前线程Thread&…

SpringBoot复习:(11)SpringApplication中的listeners成员变量是怎么初始化的?

initializers成员变量定义如下&#xff1a; 在构造方法中&#xff1a; setListeners代码如下&#xff1a; 给setListeners方法传递的是getSpringFactoriesInstances(ApplicationListener.class). getSpringFactoriesInstances代码如下&#xff1a; 调用的重载的getSpringFa…

redis 高级篇4 分布式锁

一 redis架构图 1.1 redis的架构图 1.2 分布式锁满足条件 1.独占性&#xff1b;2.高可用&#xff1b;3.防死锁&#xff1b;4.不乱抢&#xff1b;5.重入性 二 分布式锁的案例情况 2.1 分布式锁1:单机分布式部署 描述&#xff1a; 使用lock锁和synchronized&#xff0c;单机…

【shell】获取ping的时延数据并分析网络情况及常用命令学习

文章目录 获取ping的时延数据并分析网络情况|、||、&、&&辨析teetailkillall 获取ping的时延数据并分析网络情况 网络情况经常让我们头疼&#xff0c;每次都需要手动在终端ping太麻烦了&#xff0c;不如写个脚本ping并将数据带上时间戳存入文件&#xff0c;然后也…

iPhone 6透明屏是什么?原理、特点、优势

iPhone 6透明屏是一种特殊的屏幕技术&#xff0c;它能够使手机屏幕变得透明&#xff0c;让用户能够透过屏幕看到手机背后的物体。 这种技术在科幻电影中经常出现&#xff0c;给人一种未来科技的感觉。下面将介绍iPhone 6透明屏的原理、特点以及可能的应用。 iPhone 6透明屏的原…

【外卖系统】新增菜品

需求分析 在后台中&#xff0c;通过新增功能来添加一个新的菜品&#xff0c;在添加菜品时需要选择当前菜品所属的菜品分类&#xff0c;并且需要上传的菜品图片。 代码开发 需要添加的类和基本接口&#xff1a;实体类DishFlavor、Mapper接口DishFlavorMapper、业务层接口Dish…