起动战士xpseedmod(海外new things |知识产权保护公司Digip完成320万欧元的种子融资,以实现商标知识产权服务民主化)
起动战士xpseedmod文章列表:
- 1、海外new things |知识产权保护公司Digip完成320万欧元的种子融资,以实现商标知识产权服务民主化
- 2、喜中签新能源新增指标后 我该买哪款车
- 3、如何用2 KB代码实现3D赛车游戏?2kPlus Jam大赛了解一下
- 4、华人小哥开发CG工坊,帮你快速入门计算机图形学 | GitHub热榜
- 5、飞桨上线万能转换小工具,教你玩转TensorFlow、Caffe等模型迁移
海外new things |知识产权保护公司Digip完成320万欧元的种子融资,以实现商标知识产权服务民主化
作者:来自投稿
2022年12月13日,一家总部位于瑞典的法律技术创业公司Digip宣布完成了320万欧元的种子轮融资。这笔资金将继续用于提供其经济且便捷的数字知识产权服务。
世界各地的公司每年在商标上花费1500亿欧元来保护他们的知识产权。这个流程缺乏透明度,且需要漫长的的等待时间和巨额的成本。
法律技术领域的领先初创企业Digip发现了这个问题,它可以为知识产权保护提供端到端的数字解决方案,按照客户的需要进行商标注册和保护服务,从而使得企业家和老牌企业有机会在数字化和用户友好的环境中在全球范围内保护和加强其品牌名称。
Digip是根据创始人的第一手经验创建的,旨在帮助企业家以律师事务所收取费用的一小部分来保护自己的品牌。
Digip于2020年推出,目前服务于全球42个市场的500家客户,从初创企业到收入6亿欧元的公司,追求帮助客户完成其对知识产权的完全控制。在过去的12个月中,Digip已经增加了三倍的收入,并扩展了客户的地理分布,目前,Digip已经在为20多个国家的客户提供服务。
该公司在今年早些时候的200万欧元融资基础上又获得了120万欧元的追加投资,用于其技术的开发,以确保能够在2023年扩大规模时为更多的客户提供服务,并保持其强劲的增长势头。种子轮由北欧风险资本投资公司Industrifonden和专注于欧洲的风险投资公司Seed X牵头,来自斯德哥尔摩的多个家族办公室和天使投资人也参与了此次融资。
Digip的首席执行官和联合创始人Viktor Johansson说道:“我们的使命是将全球150亿欧元的商标市场数字化。一家公司通常会花费4位数的费用进行商标搜索,然后等待数周才能得到结果。我们已经改变了用户的这种体验,他们报告说,通过使用该平台和服务,平均节省了80%的商标管理费,并减少了大量的工作量。”
“2023年对我们来说将是激动人心的一年。我们将在未来几个月内在平台上推出一些很酷的功能。这包括一个开放的API,合作伙伴可以集成和利用Digip的技术作为他们的工作流程中的一部分。我们也将从明年开始寻找一些新的市场,启动我们的业务。用户还可以在dig ip.com上看到商标搜索的显著改进。这对我们来说非常重要,因为贴近我们的用户将有助于提供卓越的客户体验。”
在谈到种子轮成功结束时,Industrifonden的高级投资总监Tore Tolke说:“法律行业一直在缓慢地接受技术,而Digip提供的服务已经显示出了巨大的潜力,该领域的需求也在不断增长。我们很高兴支持团队的使命,用尖端技术推动商标和知识产权行业向前发展。他们坚定地相信,个人的创造是极具价值的,创造者理应获得机会保护他们自己的知识产权,就像那些世界领先的大公司一样。”
编辑:王与桐
喜中签新能源新增指标后 我该买哪款车
[爱卡汽车 新能源频道原创]
本月初,北京市交通委发布消息,北京市将于8月1日起面向“无车家庭”,一次性增发2万个新能源小客车指标。比起普通小客车车牌摇号的遥遥无期,新能源车指标的“排队现状”显然更靠谱一些,而这新增的2万个新能源指标,更让那些还在苦苦排号的家庭看到了新的希望。那么问题又来了,如果真的得到了幸运女神的眷顾,那该去买什么新能源车呢?那么今天我们就先为大家推荐四款车型,价格区间从10万-30万元,好不容易得到一个车牌,一定要对自己好一点,选择一个自己称心如意的新能源车型。
车型:比亚迪汉EV
售价:22.98万元-27.95万元
我们首先向大家推荐的是比亚迪汉EV,为什么会是它?因为它的关注度真的非常高,作为比亚迪的最新的旗舰级轿车,它里里外外都透着一种时尚的气息。外形采用家族式最新的Dragon Face设计语言,内饰中15.6英寸的中控大屏也是非常夺人眼球。汉EV的尺寸方面也是非常突出,4960mm的车长搭配了2920mm的轴距,是一辆不折不扣的中大型车。汉EV入门的价格为22.98万元,顶配的四驱高性能版旗舰型售价27.85万元,百公里加速只需要3.9s,如此的价位配以如此的性能,同价位难找第二款车型了。
比亚迪 汉EV(询价模块,请勿手动编辑,如需删除,请在图片上右键删除询价)
外形设计上,汉脱胎于2019上海车展上亮相的e-SEED GT概念跑车,采用“Dragon face”家族式设计语言,纤细狭长的银色饰条被看做是“龙须”,横贯车头,扁平化的大灯则是“龙睛”。“汉”字车标采用类似于篆刻的视觉效果,展现出强烈的文化自信。
汉EV的车身尺寸为4960*1910*1495mm,轴距为2920mm。不难看出,汉EV采用了溜背式的线条设计,腰线流畅自然,完美的过渡到车尾,勾勒出一个紧致的尾部造型,营造出轿跑车特有的运动气质。
汉EV的尾部造型简洁,尾灯为贯穿式设计,内部借鉴了龙爪的灵感,我们在其他的比亚迪车型上已经见识了。
汉EV的轮圈尺寸全系都是19英寸,但顶配车型内部则十分运动,为了匹配它“高性能”的身份,刹车系统升级为Brembo品牌,刹车盘也采用了打孔工艺,可以快速的出去表面粉尘,提高刹车表现。
内饰方面,汉EV融入了中国古建元素,汲取了中式大殿建筑的对称样式,营造出环抱感十足的安心空间。同时通过内饰布局、细节造型、甚至材质、色彩、纹理的精雕细琢,传递出中式豪华独特的美感和韵味。颇有趣味的是副驾驶座对面的饰板,被设计成透光的龙鳞造型,颜色可随车内气氛和空气质量变幻,让人耳目一新。
比亚迪的自适应旋转中控屏在汉EV上得到延续,15.6英寸的尺寸能够容纳足够多的信息,功能方面与以往的比亚迪车型没有什么不同,开放性极强的车机系统支持自行安装APP,可以进一步扩展车机的使用场景。
为了符合家族旗舰的身份,汉EV这次着重加强了对于后排乘客的照顾。后排中央扶手配备了独立的空调控制面板,触控式的操作也非常高级。顶配车型的后排座椅还拥有通风/加热/按摩功能,这真的太奢华了。
汉EV提供单电机两驱和双电机四驱两套动力系统,其中单电机车型最大功率163kW(222马力),峰值扭矩330Nm。四驱版在此基础上增加了一台最大功率200kW(272马力),最大扭矩350Nm的后电机,采用前后双电机系统总功率达到363kW(494马力),峰值扭矩680Nm。电池方面,比亚迪汉EV是搭载“刀片电池”的首款产品,四驱版本NEDC续航里程为550km,前驱版本NEDC续航里程为605km。爱卡汽车对汉EV前驱版本的续航做了测试,最终测试成绩为601.5km,感兴趣的朋友可以点击链接。
编辑点评:比亚迪汉作为比亚迪品牌的旗舰车型,外形足够靓丽,内部空间也十分宽敞,舒适性和科技配置自然也会非常丰富,另外这次汉EV对于后排的乘客也十分照顾,拥有独立的空调系统和座椅加热/通风/按摩功能。性能方面,汉EV不但续航里程出色,同时百公里3.9s的加速之间也让同价位的其他车型望尘莫及,是一款综合实力很强的电动车型。
比亚迪汉EV
车型:广汽新能源Aion V
售价:15.96万元-23.96万元
说到中国消费者最喜欢什么车型,SUV应该是当仁不让的,所以第二款我们便向大家推荐Aion V,它的长度虽然只有4586mm,但轴距却达到了2830mm,所以它的后排空间表现会非常的惊人。这要感谢GEP 2.0纯电平台的使用,同时这也带来了三个版本——60标准续航版、70长续航版和80超长续航版,NEDC续航分别为400km、530km及600km。
广汽新能源 Aion V(埃安V)(询价模块,请勿手动编辑,如需删除,请在图片上右键删除询价)
在整体造型设计上,Aion V整体外观延续了家族风格,同时增加了多曲面、多层次的造型,营造出别具一格的运动感和科技感。Aion V的前脸设计非常战斗,官方称之为“机甲兽”,相信它肯定能给您留下深刻的印象。
Aion V的分体式大灯有很强的设计感,造型上相呼应,视觉上也很有科技含量。上方的灯组为近光灯和日间行车灯,下方则为远光灯和转向灯,两灯组为上下对称设计,日间行车灯和转向灯采用斜柱造型,整体档次感较高。
Aion V的侧面造型非常饱满,因为不用布置发动机,所以车头可以设计得较短,营造出短前悬的优美比例,当然这也能带来更大的内部空间。侧面的腰线非常锐利,最终和车窗处的镀铬饰条相融合。另外后面把手通过黑色饰条和尾灯相连,显得别具一格。
和Aion LX一样,Aion V也使用了隐藏式车门把手。当你揣着钥匙靠近车门时,车门把手会慢慢弹出,这会给你慢慢的仪式感,当然从功能上说,这也能起到降低风阻的作用。
Aion V的长宽高分别是4586/1920/1728mm,轴距却达到了2830mm,紧凑级SUV的长度,却可以放下一个中级SUV的轴距,这要感谢GEP 2.0纯电平台的运用,这也保证了宽敞的内部驾乘空间。
来到车内,Aion V的内饰基本延续了Aion LX的布局和设计,最吸引眼球的当属两块大尺寸液晶屏了。Aion V的内饰用料强调质感,搭配的颜色非常讨喜,按钮的布局也比较合理,大量软性材料和镀铬饰件的使用提升了内饰的档次感,总体来说比较精致。
中控屏的界面显示信息丰富但并不繁杂,UI设计简约美观,操作手感基本与手机一致,上手难度低。支持Carplay和第三方App,功能非常丰富。同时语音助手“baby”也非常强大,她能够帮助你完成几乎所有中控系统中的功能,并且足够智能,并不需要特定的词语才能够执行相对应的操作。
宽敞的空间是Aion V最大的优势,尤其是后排乘坐空间非常“惊人”,无论是腿部还是头部空间都非常宽敞。此外因为中间地板非常平整,所以即便乘坐三位乘客,都没有太多压力。车内的储物空间也非常丰富,其中中央扶手储物格盖子为对开方式,灵活度和实用性很强。
动力方面,Aion V全系使用一台最大功率为135kW(184马力)的永磁同步电机,峰值扭矩350Nm,这一数据与主流合资品牌的2.0T发动机参数相当。
变速箱方面则采用的是一台固定齿轮比的单速变速箱,挡把采用了旋钮设计,在熄火后还会降下隐藏起来,通电后则会缓缓升起,和某些捷豹路虎车型非常类似。
悬挂方面采用的是前麦弗逊后多连杆独立悬挂的组合,这也是同级车型常见的配置。由于采用的是GEP 2.0纯电专属平台,所以电池包并没有凸出在外,这样既能够让车辆的离地间隙不受电池包影响,增加了安全性。又能够避免凸出的电池包在行驶时产生较大的风噪而影响车型的静谧性。
编辑点评:Aion V作为一款尺寸上是紧凑级的SUV车型,轴距和内部空间却达到了中级SUV的表现,这点是最吸引人的地方,当然这要感谢GEP 2.0纯电平台的使用。此外Aion V在设计、配置和续航方面都表现出色,15.96万元的起售价也很有诚意。
广汽Aion V
车型:几何C
售价:12.98万元-18.28万元
接下来向大家推荐的是一款上周刚刚上市的车型——几何C。几何A在两年前问世的时候,优秀的设计和出色的质感给我们留下了深刻的印象,但不占优势的空间和出色用料带来的高成本,使得几何A并没有达到预期的销量。几何C的问世似乎让这些问题迎刃而解,定位于SUV的它,应该更容易被消费者们所接受。
几何汽车 几何C(询价模块,请勿手动编辑,如需删除,请在图片上右键删除询价)
几何C的外观有着明显的家族特征,车头设计非常简洁,楔形设计是它的标志性元素。传统意义上的前格栅被浑然一体的前脸所取代,两道线条的加入则带来了一些层次感。下面的格栅带有主动开闭功能,可以根据车辆的行驶的速度调节开合角度,有效降低风阻。
作为一款突出科技感的车型,几何C使用了全LED大灯,内置了80颗LED灯珠。条状日间行车灯兼具转向灯的功能,并且带有流水动态效果。
侧面来看,几何C整体线条非常流畅,并且采用了时下流行的悬浮式车顶设计。它的长宽高分别为4432×1833×1560mm,轴距为2700mm。在经过多处空气动力学优化之后,几何C的风阻系数仅为0.273Cd,这个成绩在SUV当中是非常不错的。
门把手采用了隐藏式设计,这也是目前越来越多电动车喜欢采用的设计,这也可以进一步降低风阻。
尾部的设计亮点莫过于当下流行的贯穿式LED尾灯组,不但科技感十足,夜晚点亮后更极具辨识度。
充电接口并不是左右对称的设计。交流慢充接口位于车身左后方,直流快充接口则位于车身右前翼子板处。
来到车内,几何C基本延续了几何A的设计风格,兼顾了简约和科技感。例如家族式的双辐式方向盘、4.2英寸液晶仪表盘,以及12.3英寸悬浮式中控液晶屏等,这些都进一步加强了车内的科技氛围。
几何C的中控屏使用一块12.3英寸的LED屏幕,系统集成了GKUI吉客智能交互系统,功能应该非常丰富。
一个明显的改变,几何C的空调控制面板集成到了空调出风口处,并且由几何A上的触摸按键更改为实体按键。实体按键使用起来会更加便捷一些。在下方则设计了一个放置手机的储物格,还带有无线充电功能。
换挡旋钮前方是一组极具特点的蜂巢网格状按键,这也堪称是几何车型最大的特点。在几何A上,这些网格是由灯光勾勒出来的,在强光下很难看清。几何C对此加以改进,进一步增强了易用性。
几何C搭载高效的三合一电驱动系统,把电机、电控、减速器集成到了一起,能够提升效率、减小体积和重量。其中它的电机是一台最大功率150kW(204马力)、最大扭矩310Nm的永磁同步电机,动力与几何A相比得到了明显的增强。另外,几何C还采用了热泵空调系统和电驱余热回收系统,在低温环境中能够提升不少续航里程。
几何C采用了旋钮式换挡机构。从几何A的表现来看,这个旋钮具有很不错的手感。
底盘方面几何C采用了前麦弗逊后扭力梁悬挂。电池组布置在前后轴之间,它配备有ITCS3.0电池智能温控系统,能够让电池始终工作在适宜温度范围中,确保了安全性,提升了电池寿命。几何C提供2种续航版本,其中优选续航版搭载53kWh电池,NEDC续航里程400km;甄选续航版搭载70kWh电池,NEDC续航里程达到了550km。
编辑点评:作为一款刚刚上市的紧凑型纯电SUV,几何C的竞争对手主要有比亚迪宋Pro EV、小鹏G3以及威马EX5等,它的最大优势来自于所配备的SEM智能能量管理系统,它可以带来更精准的实际续航里程,和冬季续航里程的提升。当然在编辑看来,几何C的内饰也很有特色,在原来几何A的基础上进行了升级,变得越来越实用了。
几何C
车型:欧拉白猫
售价:7.08万元-8.38万元
最后向大家推荐的可以说是一位“网红”,它最开始因为外形的乖萌而被大家熟知,后来因为可爱的名字而被大家所记住,它就是欧拉白猫。在编辑看来,这就是一款日本K-car设计风格主导的电动车,它的价格实惠,日常的城市代步应该是个不错的选择。
欧拉汽车 欧拉白猫(询价模块,请勿手动编辑,如需删除,请在图片上右键删除询价)
白猫的车头造型非常独特,似乎就是在一大块弧形面板上开了一堆散热口,然后加上了自己的Logo。白猫的极简主义设计非常彻底,不仅放弃了传统的进气格栅,就连折线、饰条也难觅踪影。喜欢的朋友应该会被它呆萌的造型所倾倒,不喜欢的朋友则会觉得难以接受。
大灯的造型和车身一样是方型与圆角的融合,卤素光源略符合其产品定位,灯头上的欧拉Logo也足以看出厂家在细节上的用心。
欧拉白猫的车身尺寸为3625mm×1660mm×1530mm,轴距则是2490mm。相比欧拉黑猫,白猫的长度增加了130mm,轴距增加了15mm。它的充电口位于左右前翼子板处,左边为直流快充接口,右边是交流慢充接口。
车尾同样采用极简设计,尾灯的位置有些靠下,好在它还配备了LED高位刹车灯。后车窗同时也是车辆的尾门,超大的面积为驾驶者带来了非常好的后方视野。
顶配车型配备了16英寸的铝合金轮圈,轮胎是朝阳的RADIAL RP18e系列,规格为175/55 R16。 中低配车型则使用15英寸铁制轮圈搭配轮毂罩,这和整车的体造型也非常协调。
欧拉白猫比黑猫略贵一点,除了体型上的增加,白猫在配置方面也有所提升。但黑猫内饰塑料感太强这个问题,在白猫车型上并没有得到改善。在设计上,白猫的内饰造型非常可爱,欧拉还为白猫准备了多达8种内饰配色,让年轻的消费者有更多个性化的选择。
欧拉白猫使用了当前最流行的双联屏设计。在10万元以下的车型中,这样的配置还是很显档次的。
动力方面,欧拉白猫的低配车型采用了一台最大功率35kW(48马力)、最大扭矩125Nm的永磁同步电机。中高配车型的电机更加强大一些,最大功率增加到45kW(61马力),最大扭矩则是130Nm。
欧拉白猫采用了旋钮式换挡机构,旋钮中间是一键启动按键,这里的设计和欧拉黑猫一致。
欧拉白猫采用了前麦弗逊后扭力梁悬挂。它拥有两种电池配置,低配车型搭载容量为34kWh的电池组,NEDC综合工况续航里程为360km;中高配车型搭载容量为38kWh的电池组,NEDC综合工况续航里程为401km。
编辑点评:欧拉白猫的问世,特立独行的外形就是它最大的亮点,喜欢它的人肯定会心甘情愿的为其买单。产品方面,欧拉本身也在不断进步,日常代步就是非常不错的选择,当然随着技术的不断成熟,我们也期待欧拉推出其他更个性的精致小车。
全文总结:这四款我们推荐的新能源车,都是最近刚刚上市不久的新车型,所以无论是技术,还是配置,在同级车中都是比较领先的,如果您还觉得电动车不靠谱,或者觉得续航里程难以满足要求,在这些车型上其实并不存在。而随着技术的发展,现在电动车已经有赶超燃油车的趋势,只要充电桩的建设完善起来,电动车的普及只是时间问题。
精彩内容回顾:
久等的“人民好电动” 大众ID.3全解析
爱卡e XRing续航测试 小鹏P7四驱高性能
解析宝马iX3 第五代eDrive技术是亮点
欧拉白猫
如何用2 KB代码实现3D赛车游戏?2kPlus Jam大赛了解一下
选自frankforce
作者:Frank
机器之心编译
参与:王子嘉、Geek AI
控制复杂度一直是软件开发的核心问题之一,一代代的计算机从业者纷纷贡献着自己的智慧,试图降低程序的计算复杂度。然而,将一款 3D 赛车游戏的代码压缩到 2KB 以内,听起来是不是太夸张了?本文作者 Frank 是一名资深游戏开发者,在本文中,他详细介绍了如何灵活运用代码压缩、编译、随机数生成、代码复用、设计模式等十八般武艺仅仅通过 2KB 的代码就能实现一款强大的 3D 赛车游戏。
几个月前,当我听说传奇 JS1K 游戏编程竞赛将不再举办时,当即把这件事告诉了其他开发者,最后我们决定在 itch 上搞一个 2KB 版的编程竞赛以弥补这一遗憾,我们将其称之为「2kPlus Jam」。这个竞赛的主要目标是制作一个只需要 2KB 压缩文件就可以容纳的游戏。如果你知道一个 3.5 英寸软盘可以存超过 700 个这样的游戏,你也就知道这有多小了。
我的作品(Hue Jumper)是对 80 年代赛车游戏渲染技术的致敬。这里的 3D 图像和物理引擎是我纯粹地使用 JavaScript 从零开始实现的,同时我还花了大量时间去调整游戏玩法和视觉效果。
游戏编程竞赛强调「变化」(shift),所以每当玩家通过关卡时,我就会改变整个世界的色调。我想在玩家通过关卡时,会感觉到像进入了一个色调不同的新的维度,这就是我为它取名为「Hue Jumper」的原因。
本文包含了这个游戏的完整 JavaScript 代码,所以可能会有点长。不过代码的注释很友好,所以我不打算一行一行解读,也不要求你现在就通读所有代码。我的目的是向你解释它的工作原理,还有为什么我要这样做,以及这个项目的整个结构。你也可以在 CodePen上找到这份代码(https://codepen.io/KilledByAPixel/pen/poJdLwB),并进行现场调试。
那么,系好安全带,坐稳,我们要开始啦!
灵感来源
我的灵感主要来源于 80 年代的经典赛车游戏,比如《Out Run》。使用相似的技术,他们能够在非常早期的硬件上实现实时三维图形。我最近也在玩一些现代的赛车游戏,比如《Distance》和《Lonely Mountains: Downhill》,这些游戏也对我的视觉设计和游戏体验有所帮助。
Jake Gordon 用 JavaScript 写的一个伪 3D 赛车的项目(https://GitHub.com/jakesgordon/javascript-racer/)帮我减轻了很多负担。他还为此写了一系列解释其工作原理的博文。尽管我的项目是从零开始的,但是他的代码助我解决了遇到的包括数学在内的一些问题。
我还看了 Chris Glover 开发的一款名为「Moto1kross」的 JS1k 游戏(https://js1k.com/2019-x/demo/4006)。这款简单的 1KB 赛车游戏给了我一个参考,让我知道什么是可能实现的。现在我有额外的 1KB 可用空间,因此我得远远超过它。
总体策略
由于游戏大小有严格的限制,程序的架构就显得尤为重要。我的总体策略是让一切尽可能的简单,以实现创造一款视觉感受和游戏体验都很棒的游戏的最终目标。
为了压缩代码,我用 Google Closure Compiler (https://closure-compiler.appspot.com/home) 来运行它。这个编译器会删掉所有空白,把变量重命名为 1 个字母的字符,并且做了一些简单的优化。你可以通过下面的链接使用这个编译器:https://clocompiler.appspot.com/home。
不过,这个编译器还做了一些无用的事,比如替换模板字符串、缺省参数和其它有助于节省空间的 ES6 特性。所以我需要手动撤销某些无用的工作,并预先准备一些更「冒险」的压缩技术,以节省每一个字节。但这并不是最主要的成功之处,大部分文件体积的压缩还是归功于代码本身的架构。
代码需要被压缩到 2KB 以内。如果你不想选用上一种方案,还有一个类似的、但功能较弱的工具——RegPack,它可以在严格遵守规定的情况下编译 JavaScript。无论哪种方式,策略都是一样的,尽可能使用重复的代码,然后在压缩的时候压缩它们。例如,某些字符串经常出现,因此它们的压缩比很大。「c.width」、「c.height」和「Math」就是一些很好的例子,但还有很多其它的小问题。因此,在阅读这段代码时,请记住,你经常会看到一些重复代码,这些重复是有目的的——便于压缩。
CodePen
下面我们将给出一款在 Codepen 上运行的游戏。你可以在 iframe 上玩这个游戏,但是为了获得最佳的游戏体验,我建议你使用链接(https://codepen.io/KilledByAPixel/pen/poJdLwB)打开它,这样你还可以编辑或是创建代码分支。
HTML
我的游戏使用到 html 的部分很少,因为它主要是基于 JavaScript 开发的。JavaScript 创建全屏画布的方法和与后面将画布大小设置为窗口内部大小的代码都是最节省空间的。我不能确定为什么 CodePen 中需要将「overflow:hidden」添加到「body」标签中,但是直接打开应该也可以正常工作。
最终的压缩版本使用了更小的设置——把 JavaScript 包在一个「onload」事件的「call」方法里()。但是,我不喜欢在开发的时候用这种压缩的设置,因为代码是以字符串形式存储的,这样编译器也就无法正常地强调语法。
常量
游戏中的很多东西都是由常量来控制的。当我们用类似 Google Closure 这样的工具来进行压缩时,这些常量就会被替换成类似于 C 中的「#define」的形式。将它们放在开头能够更快地调整游戏玩法。
// draw settingsconst context = c.getContext`2d`; // canvas contextconst drawDistance = 800; // how far ahead to drawconst cameraDepth = 1; // FOV of cameraconst segmentLength = 100; // length of each road segmentconst roadWidth = 500; // how wide is roadconst curbWidth = 150; // with of warning trackconst dashLineWidth = 9; // width of the dashed lineconst maxPlayerX = 2e3; // limit player offsetconst mountainCount = 30; // how many mountains are thereconst timeDelta = 1/60; // inverse frame rateconst PI = Math.PI; // shorthand for Math.PI
// player settingsconst height = 150; // high of player above groundconst maxSpeed = 300; // limit max player speedconst playerAccel = 1; // player forward accelerationconst playerBrake = -3; // player breaking accelerationconst turnControl = .2; // player turning rateconst jumpAccel = 25; // z speed added for jumpconst springConstant = .01; // spring players pitchconst collisionSlow = .1; // slow down from collisionsconst pitchLerp = .1; // rate camera pitch changesconst pitchSpringDamp = .9; // dampen the pitch springconst elasticity = 1.2; // bounce elasticityconst centrifugal = .002; // how much turns pull playerconst forwardDamp = .999; // dampen player z speedconst lateralDamp = .7; // dampen player x speedconst offRoadDamp = .98; // more damping when off roadconst gravity = -1; // gravity to apply in y axisconst cameraTurnScale = 2; // how much to rotate cameraconst worldRotateScale = .00005; // how much to rotate world // level settingsconst maxTime = 20; // time to startconst checkPointTime = 10; // add time at checkpointsconst checkPointDistance = 1e5; // how far between checkpointsconst maxDifficultySegment = 9e3; // how far until max difficultyconst roadEnd = 1e4; // how far until end of road
鼠标控制
输入系统是非常简单的,只用到了鼠标。使用下面这段代码,我们可以跟踪鼠标点击和水平光标位置,并将其表示为 -1 到 1 之间的值。双击是通过「mouseUpFrames」实现的。「mousePressed」变量只在玩家第一次点击开始游戏时使用一次。
mouseDown =mousePressed =mouseUpFrames =mouseX = 0; onm ouseup =e=> mouseDown = 0;onmousedown =e=> mousePressed ? mouseDown = 1 : mousePressed = 1;onmousemove =e=> mouseX = e.x/window.innerWidth*2 - 1;
数学函数
这个游戏使用了一些函数来简化代码并减少冗余。一些标准的数学函数可以用来对值进行限定(Clamp)并进行线性差值操作(Lerp)。「ClampAngle」函数就非常有用,因为很多游戏需要将将角度限制在 -PI 和 PI 之间,而这个函数就可以做到。
随机测试样例
R 函数的工作原理就像魔法——生成种子随机数。它先取当前随机种子的正弦值,乘以一个很大的数,然后小数部分就是最终的随机数。有很多方法可以做到这一点,但这是最节约空间的方法之一。我不建议使用这项技术来做赌博软件,但在我们的项目里,它的随机性已经足够了。我们将使用这个随机生成器在不需要保存任何数据的情况下创建各种程序。例如,山脉、岩石和树木的变化并不储存在内存里。但我们这里的目标不是减少内存,而是消除存储和检索数据所需的代码。
由于这是一个「真 3D」游戏,一个 3D 向量类就显得极为有用了,而且它还能让代码容量更小。该类只包含这个游戏所必需的基本要素——一个带有加法和乘法函数的构造函数,它的参数既可以是标量,也可以是向量。要确定是否传入了一个标量,只需检查它是否小于一个大数。使用「isNan」或是检查它的类型是否是「Vec3」当然更好,但它们需要更多的空间。
Clamp =(v, a, b) => Math.min(Math.max(v, a), b);ClampAngle=(a) => (a PI) % (2*PI) (a PILerp =(p, a, b) => a Clamp(p, 0, 1) * (b-a);R =(a=1, b=0) => Lerp((Math.sin( randSeed) 1)*1e5%1,a,b); class Vec3 // 3d vector class{ constructor(x=0, y=0, z=0) {this.x = x; this.y = y; this.z = z;} Add=(v)=>( v = v < 1e5 ? new Vec3(v,v,v) : v, new Vec3( this.x v.x, this.y v.y, this.z v.z )); Multiply=(v)=>( v = v < 1e5 ? new Vec3(v,v,v) : v, new Vec3( this.x * v.x, this.y * v.y, this.z * v.z ));}
渲染函数
LSHA 使用模板字符串生成一个标准的 HSLA(色相、饱和度、光度、透明度)颜色,并且刚刚重新排序,以便将更多经常使用的组件排列在前面。在关卡处发生的全局色调变化也是在这里发生的。
DrawPoly 可以绘制梯形,它也会被用于渲染场景中的所有东西。使用「|0」将 Y 分量转换为整数,以确保道路多边形完全连接。如果进行这项操作,在路段之间就会有一条细线。出于同样的原因,这种渲染技术必须在对角线图形的组件帮助下处理相机的滚动。
DrawText 则被用来渲染显示时间、距离和游戏标题的概述文本。
LSHA=(l,s=0,h=0,a=1)=>`hsl(${h hueShift},${s}%,${l}%,${a})`;
// draw a trapazoid shaped polyDrawPoly=(x1, y1, w1, x2, y2, w2, fillStyle)=>{ context.beginPath(context.fillStyle = fillStyle); context.lineTo(x1-w1, y1|0); context.lineTo(x1 w1, y1|0); context.lineTo(x2 w2, y2|0); context.lineTo(x2-w2, y2|0); context.fill();}
// draw outlined hud textDrawText=(text, posX)=>{ context.font = '9em impact'; // set font size context.fillStyle = LSHA(99,0,0,.5); // set font color context.fillText(text, posX, 129); // fill text context.lineWidth = 3; // line width context.strokeText(text, posX, 129); // outline text}
通过过程生成来构建轨道
在游戏开始之前,我们必须首先生成整个赛道,而每个游戏的赛道都不同。为此,我们构建一个路段列表,它存储了道路在轨道上每个点的位置和宽度。
轨道发生器很简单,它只是在不同频率、振幅和宽度的部分之间逐渐变细。赛道长度决定了这段赛道的难度。
这里的道路俯仰角是使用「atan2」函数计算出来的,它被用于用于物理效果和照明。
程序化的轨道生成器的示例结果。
roadGenLengthMax = // end of sectionroadGenLength = // distance leftroadGenTaper = // length of taperroadGenFreqX = // X wave frequencyroadGenFreqY = // Y wave frequencyroadGenScaleX = // X wave amplituderoadGenScaleY = 0; // Y wave amplituderoadGenWidth = roadWidth; // starting road widthstartRandSeed = randSeed = Date.now(); // set random seedroad = []; // clear road
// generate the roadfor( i = 0; i < roadEnd*2; i ) // build road past end{ if (roadGenLength > roadGenLengthMax) // is end of section? { // calculate difficulty percent d = Math.min(1, i/maxDifficultySegment); // randomize road settings roadGenWidth = roadWidth*R(1-d*.7,3-2*d); // road width roadGenFreqX = R(Lerp(d,.01,.02)); // X curves roadGenFreqY = R(Lerp(d,.01,.03)); // Y bumps roadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scale roadGenScaleY = R(Lerp(d,1e3,2e3)); // Y scale // apply taper and move back roadGenTaper = R(99, 1e3)|0; // random taper roadGenLengthMax = roadGenTaper R(99,1e3); // random length roadGenLength = 0; // reset length i -= roadGenTaper; // subtract taper } // make a wavy road x = Math.sin(i*roadGenFreqX) * roadGenScaleX; y = Math.sin(i*roadGenFreqY) * roadGenScaleY; road[i] = road[i]? road[i] : {x:x, y:y, w:roadGenWidth}; // apply taper from last section and lerp values p = Clamp(roadGenLength / roadGenTaper, 0, 1); road[i].x = Lerp(p, road[i].x, x); road[i].y = Lerp(p, road[i].y, y); road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth); // calculate road pitch angle road[i].a = road[i-1] ? Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;}
开始游戏
现在轨道有了,剩下的启动过程就很简单了。我们只需要初始化几个变量。
// reset everythingvelocity = new Vec3 ( pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0 ); position = new Vec3(0, height); // set player start posnextCheckPoint = checkPointDistance; // init next checkpointtime = maxTime; // set the start timeheading = randSeed; // random world heading
更新玩家状态
本节将介绍主要的更新函数,它可以处理游戏中所有内容的更新和渲染!通常,在代码写一个超大的函数并不是一个好习惯,我们需要将其分解成子函数。因此,为了方便理解,下面的叙述将其分为几部分。
首先我们需要了解玩家所在位置的道路信息。为了使物理效果和渲染感觉平滑,在当前路段和下一个路段之间进行了插值操作。
玩家的位置和速度是 3D 向量,并通过动力学进行更新以体现重力,阻尼和其他因素。如果玩家在道路下方,位置将被固定在地面上,并且速度会相对于法线反射。同样,在地面上时会施加加速度,并且越野行驶时相机会震动。经过游戏测试后,我决定允许玩家在空降时仍可以进行调整。
在此处理输入以控制加速,刹车,跳跃和转弯。通过「mouseUpFrames」也可以检测到双击。有一些代码可以跟踪玩家在空中停留了多少帧,以便在玩家仍然可以跳跃的时候有一个短暂的宽限期。
相机的俯仰角使用了一个简单的弹簧系统,在玩家加速、刹车和跳跃时给人一种动态的感觉。当玩家驾车翻越山丘以及跳跃时,摄像机也会根据道路角度倾斜。
Update=()=>{
// get player road segments = position.z / segmentLength | 0; // current road segmentp = position.z / segmentLength % 1; // percent along segment
// get lerped values between last and current road segmentroadX = Lerp(p, road[s].x, road[s 1].x);roadY = Lerp(p, road[s].y, road[s 1].y) height;roadA = Lerp(p, road[s].a, road[s 1].a);
// update player velocitylastVelocity = velocity.Add(0);velocity.y = gravity;velocity.x *= lateralDamp;velocity.z = Math.max(0, time?forwardDamp*velocity.z:0);
// add velocity to positionposition = position.Add(velocity); // limit player x position (how far off road)position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);
// check if on groundif (position.y < roadY){ position.y = roadY; // match y to ground plane airFrame = 0; // reset air frames // get the dot product of the ground normal and the velocity dp = Math.cos(roadA)*velocity.y Math.sin(roadA)*velocity.z; // bounce velocity against ground normal velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA)) .Multiply(-elasticity * dp).Add(velocity); // apply player brake and accel velocity.z = mouseDown? playerBrake : Lerp(velocity.z/maxSpeed, mousePressed*playerAccel, 0); // check if off road if (Math.abs(position.x) > road[s].w) { velocity.z *= offRoadDamp; // slow down pitchSpring = Math.sin(position.z/99)**4/99; // rumble }}
// update player turning and apply centrifugal forceturn = Lerp(velocity.z/maxSpeed, mouseX * turnControl, 0);velocity.x = velocity.z * turn - velocity.z ** 2 * centrifugal * roadX;
// update jumpif (airFrame && mouseDown && mouseUpFrames && mouseUpFrames{ velocity.y = jumpAccel; // apply jump velocity airFrame = 9; // prevent jumping again}mouseUpFrames = mouseDown? 0 : mouseUpFrames 1;
// pitch down with vertical velocity when in airairPercent = (position.y-roadY) / 99;pitchSpringSpeed = Lerp(airPercent, 0, velocity.y/4e4);
// update player pitch springpitchSpringSpeed = (velocity.z - lastVelocity.z)/2e3;pitchSpringSpeed -= pitchSpring * springConstant;pitchSpringSpeed *= pitchSpringDamp;pitchSpring = pitchSpringSpeed;pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent,-roadA,0));playerPitch = pitchSpring pitchRoad;
// update headingheading = ClampAngle(heading velocity.z*roadX*worldRotateScale);cameraHeading = turn * cameraTurnScale;
// was checkpoint crossed?if (position.z > nextCheckPoint){ time = checkPointTime; // add more time nextCheckPoint = checkPointDistance; // set next checkpoint hueShift = 36; // shift hue}
预渲染
在渲染之前,可以通过设置画布的宽度和高度来清除画布。这也适用于用画布填充窗口。
我们还计算了用于将世界点转换为画布空间的投影比例。「cameraDepth」值表示摄像机的视野(FOV),本游戏中其视野为 90 度。计算公式为「1/Math.tan((fovRadians/2))」,对于 90 度的 FOV 来说,其结果正好是 1。为了保持纵横比,投影按「c.width」进行缩放。
// clear the screen and set sizec.width = window.innerWidth, c.height = window.innerHeight;
// calculate projection scale, flip yprojectScale = (new Vec3(1,-1,1)).Multiply(c.width/2/cameraDepth);
画出天空、太阳和月亮
背景氛围是通过全屏线性渐变绘制的,它会根据太阳的方向更改颜色。
为了节省空间,我们使用具有透明度的全屏径向渐变在同一个 for 循环中绘制太阳和月亮。
线性和径向渐变相结合,构成了一个完全环绕场景的天空。
// get horizon, offset, and light amounthorizon = c.height/2 - Math.tan(playerPitch)*projectScale.y;backgroundOffset = Math.sin(cameraHeading)/2;light = Math.cos(heading);
// create linear gradient for skyg = context.createLinearGradient(0,horizon-c.height/2,0,horizon);g.addColorStop(0,LSHA(39 light*25,49 light*19,230-light*19));g.addColorStop(1,LSHA(5,79,250-light*9));
// draw sky as full screen polyDrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);
// draw sun and moon (0=sun, 1=moon)for( i = 2 ; i--; ){ // create radial gradient g = context.createRadialGradient( x = c.width*(.5 Lerp( (heading/PI/2 .5 i/2)%1, 4, -4)-backgroundOffset), y = horizon - c.width/5, c.width/25, x, y, i?c.width/23:c.width); g.addColorStop(0, LSHA(i?70:99)); g.addColorStop(1, LSHA(0,0,0,0)); // draw full screen poly DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);}
画出山和地平线
山是通过在地平线上绘制 50 个三角形来程序化地生成的。当我们面向太阳时,由于山处于阴影中,所以山的光线会更暗。此外,附近的山更暗,以模拟雾的效果。这里真正的诀窍是调整大小和颜色的随机值以获得良好的结果。
绘制背景的最后一部分是绘制地平线,并用纯绿色填充地平线的下方。
// set random seed for mountainsrandSeed = startRandSeed;
// draw mountainsfor( i = mountainCount; i--; ){ angle = ClampAngle(heading R(19)); light = Math.cos(angle-heading); DrawPoly( x = c.width*(.5 Lerp(angle/PI/2 .5,4,-4)-backgroundOffset), y = horizon, w = R(.2,.8)**2*c.width/2, x w*R(-.5,.5), y - R(.5,.8)*w, 0, LSHA(R(15,25) i/3-light*9, i/2 R(19), R(220,230)));}
// draw horizonDrawPoly( c.width/2, horizon, c.width/2, c.width/2, c.height, c.width/2, LSHA(25, 30, 95));
将路段投影到画布空间
在渲染道路之前,我们必须首先获取投影后的道路点。第一部分比较复杂,因为我们的道路的 x 值需要转换为世界空间位置。为了使道路看起来是弯曲的,我们将 x 值作为二阶导数。这就是奇怪的代码「x =w =」的作用。由于这种工作方式,路段并没有固定的世界空间位置,而是基于玩家的位置重新计算每一帧。
有了世界空间位置后,我们便能够用道路位置减去玩家位置以获得当前的摄像头空间位置。代码的其余部分实现了不同的变换,首先旋转航向、俯仰角,然后进行投影变换,使更远的东西看起来更小,最后将其映射到画布空间。
for( x = w = i = 0; i < drawDistance 1; ){ p = new Vec3(x =w =road[s i].x, // sum local road offsets road[s i].y, (s i)*segmentLength) // road y and z pos .Add(position.Multiply(-1)); // get local camera space
// apply camera heading p.x = p.x*Math.cos(cameraHeading) - p.z*Math.sin(cameraHeading); // tilt camera pitch and invert z z = 1/(p.z*Math.cos(playerPitch) - p.y*Math.sin(playerPitch)); p.y = p.y*Math.cos(playerPitch) - p.z*Math.sin(playerPitch); p.z = z; // project road segment to canvas space road[s i ].p = // projected road point p.Multiply(new Vec3(z, z, 1)) // projection .Multiply(projectScale) // scale .Add(new Vec3(c.width/2,c.height/2)); // center on canvas}
绘制路段
现在,我们有了每个路段的画布空间点,渲染就相当简单了。我们需要从后到前绘制每个路段,或者更具体地说,画出连接路段的梯形多边形。
为了创建道路,我们需要在每个路段上进行 4 层渲染:地面,条纹路缘,道路本身和虚线白线。根据道路线段的坡度和方向为每个阴影着色,并根据该图层的外观添加一些额外的逻辑。
我们需要检查路段是否在近/远剪辑范围中,以防止出现怪异的渲染伪像。此外,还有一个很好的优化方法,可以在道路变得很细时按距离缩小道路分辨率。这样就在没有明显的质量损失的情况下,将绘图次数减少了一半以上,从而获得了巨大的性能提升。
线框轮廓显示了每一个被渲染的多边形。
let segment2 = road[s drawDistance]; // store the last segmentfor( i = drawDistance; i--; ) // iterate in reverse{ // get projected road points segment1 = road[s i]; p1 = segment1.p; p2 = segment2.p; // random seed and lighting randSeed = startRandSeed s i; light = Math.sin(segment1.a) * Math.cos(heading) * 99; // check near and far clip if (p1.z < 1e5 && p1.z > 0) { // fade in road resolution over distance if (i % (Lerp(i/drawDistance,1,9)|0) == 0) { // ground DrawPoly(c.width/2, p1.y, c.width/2, c.width/2, p2.y, c.width/2, LSHA(25 light, 30, 95));
// curb if wide enough if (segment1.w > 400) DrawPoly(p1.x, p1.y, p1.z*(segment1.w curbWidth), p2.x, p2.y, p2.z*(segment2.w curbWidth), LSHA(((s i) // road and checkpoint marker DrawPoly(p1.x, p1.y, p1.z*segment1.w, p2.x, p2.y, p2.z*segment2.w, LSHA(((s i)*segmentLength%checkPointDistance < 300 ? 70 : 7) light)); // dashed lines if wide and close enough if ((segment1.w > 300) && (s i)%9==0 && i < drawDistance/3) DrawPoly(p1.x, p1.y, p1.z*dashLineWidth, p2.x, p2.y, p2.z*dashLineWidth, LSHA(70 light));
// save this segment segment2 = segment1; }
绘制道路上的树和石头
这个游戏只有两种不同类型的物体:树和石头,它们是被渲染在道路上的。首先,我们使用「R()」函数来确定是否存在对象。这是种子随机数厉害的地方之一。我们还将使用「R()」为对象添加随机形状和颜色变化。
一开始我想要其他的车辆,但如果不进行大幅裁剪,就不能满足空间限制,所以我使用风景作为障碍。这些风景的位置是随机的,而且倾向于接近道路,否则他们就会变得很稀疏,而且很容易通过。为了节省空间,对象的高度也决定了对象的类型。
在这里可以通过比较玩家和物体在 3D 空间中的位置来检查它们之间的碰撞。当一个物体被击中时,玩家会放慢速度,并将该物体标记为击中,这样它就可以安全地通过。
为了防止物体突然出现在地平线上,透明效果会随着距离的增加而减弱。由于我前面提到的神奇的种子随机函数,对象的形状和颜色使用了带有变化的梯形绘制函数。
if (R()29) // is there an object? { // player object collision check x = 2*roadWidth * R(10,-10) * R(9); // choose object pos const objectHeight = (R(2)|0) * 400; // choose tree or rock if (!segment1.h // dont hit same object && Math.abs(position.x-x) && Math.abs(position.z-(s i)*segmentLength) && position.y-height { // slow player and mark object as hit velocity = velocity.Multiply(segment1.h = collisionSlow); }
// draw road object const alpha = Lerp(i/drawDistance, 4, 0); // fade in object if (objectHeight) { // tree trunk DrawPoly(x = p1.x p1.z * x, p1.y, p1.z*29, x, p1.y-99*p1.z, p1.z*29, LSHA(5 R(9), 50 R(9), 29 R(9), alpha)); // tree leaves DrawPoly(x, p1.y-R(50,99)*p1.z, p1.z*R(199,250), x, p1.y-R(600,800)*p1.z, 0, LSHA(25 R(9), 80 R(9), 9 R(29), alpha)); } else { // rock DrawPoly(x = p1.x p1.z*x, p1.y, p1.z*R(200,250), x p1.z*(R(99,-99)), p1.y-R(200,250)*p1.z, p1.z*R(99), LSHA(50 R(19), 25 R(19), 209 R(9), alpha)); } } }}
绘制 HUD,更新时间,请求下一个更新
游戏的标题、时间和距离是通过一个非常简单的字体渲染系统显示的,该系统使用了我们之前设置的 DrawText 函数。在玩家点击鼠标之前,它会将标题显示在屏幕中央。这是我非常自豪的部分——能够显示游戏标题并使用粗体的「impact」字体。如果我面临的空间上的要求更紧一些,这些东西会是第一个被我删掉的。
按下鼠标后,游戏就会开始,HUD 显示剩余的时间和当前距离。在这个条件语句块中,时间也会被更新,因为它只在比赛开始后才会减少。
在这个庞大的更新函数的最后,它调用「requestAnimationFrame(Update)」来触发下一次更新。
if (mousePressed){ time = Clamp(time - timeDelta, 0, maxTime); // update time DrawText(Math.ceil(time), 9); // show time context.textAlign = 'right'; // right alignment DrawText(0|position.z/1e3, c.width-9); // show distance}else{ context.textAlign = 'center'; // center alignment DrawText('HUE JUMPER', c.width/2); // draw title text}
requestAnimationFrame(Update); // kick off next frame
} // end of update function
最后一点代码
我们需要调用一次上面巨大的更新函数,以启动更新循环。
此外,HTML 需要一个关闭脚本标签来让所有代码实际运行。
Update(); // kick off update loop
极致压缩
整个游戏的业务逻辑就是如此!以下是用彩色编码将其压缩以显示不同部分后的最终结果。完成所有这些工作之后,可以想象,在这样一小段代码中看到我的整个游戏是多么令人满足。之后的 zip 操作通过消除重复的代码将文件大小几乎又减少了一半。
HTML – Red
函数 – Orange
设置– Yellow
玩家更新 – Green
背景渲染 – Cyan
道路渲染 – Purple
对象渲染 – Pink
HUD 渲染 – Brown
说明
其他方法也可以实现同时提供性能和视觉效果的 3D 渲染。如果我有更多的空间,我更愿意使用像「three.js」这样的 WebGL API,我在我去年制作的游戏《Bogus Roads》中就用到了它。此外,因为它使用的是「requestAnimationFrame」,所以确实需要一些额外的代码来确保帧速率限制在 60 fps,我将它们添加到了增强版本中。我更喜欢使用「requestAnimationFrame」而不是「setInterval」,因为它的渲染结果更平滑,因为它将被垂直同步(让显卡的运算和显示器刷新率一致以稳定输出的画面质量)。这段代码的一个主要有点是它的兼容性非常好,可以在任何设备上运行,不过在我那台老旧的 iPhone 上运行速度有点慢。
结语
读完本文,希望大家有所收获。
这个游戏的代码在 GPL-3.0 开源协议下,在 GitHub 上已经开源了,你可以在自己的项目中随意使用它。该 repo 还包含 2k 版本,该版本在发布时仅为 2031 字节!你也可以加入一些额外的功能如音乐和音效来实现「增强」版本。(https://killedbyapixel.itch.io/hue-jumper)
GitHub地址:https://github.com/KilledByAPixel/HueJumper2k
原文链接:http://frankforce.com/?p=7427
华人小哥开发CG工坊,帮你快速入门计算机图形学 | GitHub热榜
子豪 发自 凹非寺
量子位 报道 | 公众号 QbitAI
CG新手们,你们的福音来了~
为了让初学者更好地学习计算机图形学基础知识,一位哈佛小哥创建了graphics-workshop,一周左右的时间,已经在Github上获得1K星。
其中包含5个子项目:被子块图案、过程纹理生成、栅格化和着色、风格化渲染,以及光线追踪。
用户需要用npm进行安装,通过运行下面的代码,安装依赖项和启动开发服务器。
$ npm install...added 16 packages from 57 contributors and audited 16 packages in 1.459s3 packages are looking for funding run `npm fund` for detailsfound 0 vulnerabilities$ npm run dev vite v2.1.5 dev server running at: > Local: http://localhost:3000/ > Network: http://10.250.14.217:3000/ ready in 555ms.
具体怎么用?一起来看。
被子块图案
首先,可以将制作被子块图案作为入门项目,它展示了在2D网格中渲染的过程。
作者在「shaders/quilt.frag.glsl」中给出了相应的代码,片段着色器遍历每一个像素,将像素编号传入gl_FragCoord.xy中,绘制2D网格。
新手们可以通过取消注释,来改变图形,包括绘制、翻转形状和改变颜色等。
比如,修改if语句,就可以改变图案的几何形状;
如果想生成更丰富的RGB颜色,可以通过修改变量c实现:
最后,利用gl_FragColor输出像素的颜色。
过程纹理生成
除了制作被子块图案,还可以创建类似「我的世界」中的场景:
为生成自然的外观,开发者使用了一种常见的图形基元,称为单纯形噪声。函数float snoise(vec2)用来接收向量,并在该位置输出一个平滑的标量噪声值。
由于不同位置的噪声值大致独立,改变屏幕右上方的seed ,就能够看到渲染后输出的新形状。
依次取消第一个代码块的注释,学习组合不同音高的噪声,用于改变纹理;取消第二个代码块的注释,学习使用阈值(特别是mix和smoothstep函数)来调整颜色。
此外,还可以添加参数,比如:利用temperature,从噪声图中独立采样来改变阴影等。
栅格化和着色
与大多数视频游戏所用的算法相同,采用栅格化方法渲染3D三角形网格,呈现更逼真的效果:
将3D表面分解为三角形,然后在屏幕上独立绘制每个三角形,并在它们之间插入变量。
图像被储存为三角形网格,片段着色器将对三角形的每个片段评估一次,而不是针对每个像素。
用户可以单击拖动来查看图形的不同角度,通过mesh查看除茶壶之外的其他形状,以及用kd改变对象的颜色。
利用illuminate()函数,可以表示光源的位置,以及光源对当前像素颜色的作用。
代码目前仅支持漫反射,用户也可以更新代码,添加Phong镜面反射组件。
风格化渲染
这一项目的代码和上面的项目非常相似。
但是在进行照明计算之后,不会立刻输出颜色,而是根据亮度强度阈值,进行离散化和不同风格的处理。
光线追踪
光线追踪是照片级真实感渲染中的黄金标准。
通过为每个像素拍摄射线,来用片段着色器进行几何计算,用trace()函数返回与给定射线相对应的颜色,来进行建模。
用intersect() 函数来计算空间中任何射线的第一个交点;illuminate()用于将两个点光源的作用相加,来计算给定点的光照。
在进行照明计算之前,添加条件语句以检查从点到光源的射线是否被遮挡。如果被遮挡,则应立即返回vec3(0.0)模拟阴影。
通过修改代码,还可以选择强度,在不同的位置添加第三个点光源。
作者简介
开发者Eric Zhang,目前是哈佛大学的硕士研究生,主要研究方向是机器学习和编程语言,曾在英伟达实习。
他获得过两届IOI金牌,还为高中学生写了一本物理书,并且提供免费的电子版。
不仅如此,小哥还擅长音乐,凭借中提琴演奏获得过不少奖项。
他经常在个人网站分享文章,也在Github中发布过多个项目,都有着不错的反响。
感兴趣的朋友们,可戳链接了解详情~
参考链接:
[1]https://github.com/ekzhang/graphics-workshop
[2]https://www.ekzhang.com/
[3]https://www.aapt.org/physicsteam/2020/pastexams.cfm
— 完 —
量子位 QbitAI · 头条号签约
关注我们,第一时间获知前沿科技动态
飞桨上线万能转换小工具,教你玩转TensorFlow、Caffe等模型迁移
本文作者:飞桨开发者说成员Charlotte
量子位 编辑 | 公众号 QbitAI
百度推出飞桨(PaddlePaddle)后,不少开发者开始转向国内的深度学习框架。但是从代码的转移谈何容易,之前的工作重写一遍不太现实,成千上万行代码的手工转换等于是在做一次二次开发。
现在,有个好消息:无论Caffe、TensorFlow、ONNX都可以轻松迁移到飞桨平台上。虽然目前还不直接迁移PyTorch模型,但PyTorch本身支持导出为ONNX模型,等于间接对该平台提供了支持。
然而,有人还对存在疑惑:不同框架之间的API有没有差异?整个迁移过程如何操作,步骤复杂吗?迁移后如何保证精度的损失在可接受的范围内?
大家会考虑很多问题,而问题再多,归纳一下,无外乎以下几点:
API差异:模型的实现方式如何迁移,不同框架之间的API有没有差异?如何避免这些差异带来的模型效果的差异?
模型文件差异:训练好的模型文件如何迁移?转换框架后如何保证精度的损失在可接受的范围内?
预测方式差异:转换后的模型如何预测?预测的效果与转换前的模型差异如何?
飞桨开发了一个新的功能模块,叫X2Paddle(Github见参考1),可以支持主流深度学习框架模型转换至飞桨,包括Caffe、Tensorflow、onnx等模型直接转换为Paddle Fluid可加载的预测模型,并且还提供了这三大主流框架间的API差异比较,方便我们在自己直接复现模型时对比API之间的差异,深入理解API的实现方式从而降低模型迁移带来的损失。
下面以TensorFlow转换成Paddle Fluid模型为例,详细讲讲如何实现模型的迁移。
TensorFlow-Fluid 的API差异
在深度学习入门过程中,大家常见的就是手写数字识别这个demo,下面是一份最简单的实现手写数字识别的代码:
from tensorflow.examples.tutorials.mnist import input_dataimport tensorflow as tfmnist = input_data.read_data_sets("MNIST_data/", one_hot=True)x = tf.placeholder(tf.float32, [None, 784]) W = tf.Variable(tf.zeros([784, 10]))b = tf.Variable(tf.zeros([10]))y = tf.nn.softmax(tf.matmul(x, W) b)y_ = tf.placeholder("float", [None, 10])cross_entropy = tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(logits = y,labels = y_))train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)init = tf.global_variables_initializer()sess = tf.Session()sess.run(init)for i in range(1, 1000): batch_xs, batch_ys = mnist.train.next_batch(100) sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))
大家看这段代码里,第一步是导入mnist数据集,然后设置了一个占位符x来表示输入的图片数据,再设置两个变量w和b,分别表示权重和偏置来计算,最后通过softmax计算得到输出的y值,而我们真实的label则是变量y_ 。
前向传播完成后,就可以计算预测值y与label y_之间的交叉熵。
再选择合适的优化函数,此处为梯度下降,最后启动一个Session,把数据按batch灌进去,计算acc即可得到准确率。
这是一段非常简单的代码,如果我们想把这段代码变成飞桨的代码,有人可能会认为非常麻烦,每一个实现的API还要一一去找对应的实现方式,但是这里,我可以告诉大家,不!用!这!么!麻!烦!因为在X2Paddle里有一份常用的Tensorflow对应Fluid的API表,(https://github.com/PaddlePaddle/X2Paddle/tree/master/tensorflow2fluid/doc),如下所示:
对于常用的TensorFlow的API,都有相应的飞桨接口,如果两者的功能没有差异,则会标注功能一致,如果实现方式或者支持的功能、参数等有差异,即会标注“差异对比”,并详细注明。
譬如,在上文这份非常简单的代码里,出现了这些TensorFlow的API:
在出现的这些api里,大部分的功能都是一致的,只有两个功能不同,分别是tf.placeholder和tf.nn.softmax_cross_entropy_with_logits ,分别对应 fluid.layers.data 和 fluid.layers.softmax_with_cross_entropy . 我们来看看具体差异:
tf.placeholder V.S fluid.layers.data
常用TensorFlow的同学对placeholder应该不陌生,中文翻译为占位符,什么意思呢?在TensorFlow 2.0以前,还是静态图的设计思想,整个设计理念是计算流图,在编写程序时,首先构筑整个系统的graph,代码并不会直接生效,这一点和python的其他数值计算库(如numpy等)不同,graph为静态的,在实际的运行时,启动一个session,程序才会真正的运行。这样做的好处就是:避免反复地切换底层程序实际运行的上下文,tensorflow帮你优化整个系统的代码。我们知道,很多python程序的底层为C语言或者其他语言,执行一行脚本,就要切换一次,是有成本的,tensorflow通过计算流图的方式,可以帮你优化整个session需要执行的代码。
在代码层面,每一个tensor值在graph上都是一个op,当我们将train数据分成一个个minibatch然后传入网络进行训练时,每一个minibatch都将是一个op,这样的话,一副graph上的op未免太多,也会产生巨大的开销;于是就有了tf.placeholder,我们每次可以将 一个minibatch传入到x = tf.placeholder(tf.float32,[None,32])上,下一次传入的x都替换掉上一次传入的x,这样就对于所有传入的minibatch x就只会产生一个op,不会产生其他多余的op,进而减少了graph的开销。
参数对比
tf.placeholder
tf.placeholder( dtype, shape=None, name=None )
paddle.fluid.layers.data
paddle.fluid.layers.data( name, shape, append_batch_size=True, dtype='float32', lod_level=0, type=VarType.LOD_TENSOR, stop_gradient=True)
从图中可以看到,飞桨的api参数更多,具体差异如下:
Batch维度处理
TensorFlow: 对于shape中的batch维度,需要用户使用None指定;
飞桨: 将第1维设置为-1表示batch维度;如若第1维为正数,则会默认在最前面插入batch维度,如若要避免batch维,可将参数append_batch_size设为False。
梯度是否回传
tensorflow和pytorch都支持对输入求梯度,在飞桨中直接设置stop_gradient = False即可。如果在某一层使用stop_gradient=True,那么这一层之前的层都会自动的stop_gradient=True,梯度不会参与回传,可以对某些不需要参与loss计算的信息设置为stop_gradient=True。对于含有BatchNormalization层的CNN网络,也可以对输入求梯度,如
layers.data( name="data", shape=[32, 3, 224, 224], dtype="int64", append_batch_size=False, stop_gradient=False)
tf.nn.softmax_cross_entropy_with_logits V.S fluid.layers.softmax_with_cross_entropy
参数对比
tf.nn.softmax_cross_entropy_with_logits( _sentinel=None, labels=None, logits=None, dim=-1, name=None)
paddle.fluid.layers.softmax_with_cross_entropy
paddle.fluid.layers.softmax_with_cross_entropy( logits, label, soft_label=False, ignore_index=-100, numeric_stable_mode=False, return_softmax=False)
功能差异
标签类型
TensorFlow:labels只能使用软标签,其shape为[batch, num_classes],表示样本在各个类别上的概率分布;
飞桨:通过设置soft_label,可以选择软标签或者硬标签。当使用硬标签时,label的shape为[batch, 1],dtype为int64;当使用软标签时,其shape为[batch, num_classes],dtype为int64。
返回值
TensorFlow:返回batch中各个样本的log loss;
飞桨:当return_softmax为False时,返回batch中各个样本的log loss;当return_softmax为True时,再额外返回logtis的归一化值。
疑问点?
硬标签,即 one-hot label, 每个样本仅可分到一个类别
软标签,每个样本可能被分配至多个类别中
numeric_stable_mode:这个参数是什么呢?标志位,指明是否使用一个具有更佳数学稳定性的算法。仅在 soft_label 为 False的GPU模式下生效. 若 soft_label 为 True 或者执行场所为CPU, 算法一直具有数学稳定性。注意使用稳定算法时速度可能会变慢。默认为 True。
return_softmax: 指明是否额外返回一个softmax值, 同时返回交叉熵计算结果。默认为False。
如果 return_softmax 为 False, 则返回交叉熵损失
如果 return_softmax 为 True,则返回元组 (loss, softmax) ,其中交叉熵损失为形为[N x 1]的二维张量,softmax为[N x K]的二维张量
代码示例
data = fluid.layers.data(name='data', shape=[128], dtype='float32')label = fluid.layers.data(name='label', shape=[1], dtype='int64')fc = fluid.layers.fc(input=data, size=100)out = fluid.layers.softmax_with_cross_entropy( logits=fc, label=label)
所以通过API对应表,我们可以直接转换把TensorFlow代码转换成Paddle Fluid代码。但是如果现在项目已经上线了,代码几千行甚至上万行,或者已经训练出可预测的模型了,如果想要直接转换API是一件非常耗时耗精力的事情,有没有一种方法可以直接把训练好的可预测模型直接转换成另一种框架写的,只要转换后的损失精度在可接受的范围内,就可以直接替换。下面就讲讲训练好的模型如何迁移。
模型迁移
VGG_16是CV领域的一个经典模型,我以tensorflow/models下的VGG_16为例,给大家展示如何将TensorFlow训练好的模型转换为飞桨模型。
下载预训练模型
import urllibimport sysdef schedule(a, b, c): per = 100.0 * a * b / c per = int(per) sys.stderr.write("rDownload percentage %.2f%%" % per) sys.stderr.flush()url = "http://download.tensorflow.org/models/vgg_16_2016_08_28.tar.gz"fetch = urllib.urlretrieve(url, "./vgg_16.tar.gz", schedule)
解压下载的压缩文件
import tarfilewith tarfile.open("./vgg_16.tar.gz", "r:gz") as f: file_names = f.getnames() for file_name in file_names: f.extract(file_name, "./")
保存模型为checkpoint格式
import tensorflow.contrib.slim as slimfrom tensorflow.contrib.slim.nets import vggimport tensorflow as tfimport numpywith tf.Session() as sess: inputs = tf.placeholder(dtype=tf.float32, shape=[None, 224, 224, 3], name="inputs") logits, endpoint = vgg.vgg_16(inputs, num_classes=1000, is_training=False) load_model = slim.assign_from_checkpoint_fn("vgg_16.ckpt", slim.get_model_variables("vgg_16")) load_model(sess) numpy.random.seed(13) data = numpy.random.rand(5, 224, 224, 3) input_tensor = sess.graph.get_tensor_by_name("inputs:0") output_tensor = sess.graph.get_tensor_by_name("vgg_16/fc8/squeezed:0") result = sess.run([output_tensor], {input_tensor:data}) numpy.save("tensorflow.npy", numpy.array(result)) saver = tf.train.Saver() saver.save(sess, "./checkpoint/model")
TensorFlow2fluid目前支持checkpoint格式的模型或者是将网络结构和参数序列化的pb格式模型,上面下载的vgg_16.ckpt仅仅存储了模型参数,因此我们需要重新加载参数,并将网络结构和参数一起保存为checkpoint模型
将模型转换为飞桨模型
import tf2fluid.convert as convertimport argparseparser = convert._get_parser()parser.meta_file = "checkpoint/model.meta"parser.ckpt_dir = "checkpoint"parser.in_nodes = ["inputs"]parser.input_shape = ["None,224,224,3"]parser.output_nodes = ["vgg_16/fc8/squeezed"]parser.use_cuda = "True"parser.input_format = "NHWC"parser.save_dir = "paddle_model"convert.run(parser)
注意:部分OP在转换时,需要将参数写入文件;或者是运行tensorflow模型进行infer,获取tensor值。两种情况下均会消耗一定的时间用于IO或计算,对于后一种情况,
打印输出log信息(截取部分)
INFO:root:Loading tensorflow model...INFO:tensorflow:Restoring parameters from checkpoint/modelINFO:tensorflow:Restoring parameters from checkpoint/modelINFO:root:Tensorflow model loaded!INFO:root:TotalNum:86,TraslatedNum:1,CurrentNode:inputsINFO:root:TotalNum:86,TraslatedNum:2,CurrentNode:vgg_16/conv1/conv1_1/weightsINFO:root:TotalNum:86,TraslatedNum:3,CurrentNode:vgg_16/conv1/conv1_1/biasesINFO:root:TotalNum:86,TraslatedNum:4,CurrentNode:vgg_16/conv1/conv1_2/weightsINFO:root:TotalNum:86,TraslatedNum:5,CurrentNode:vgg_16/conv1/conv1_2/biases...INFO:root:TotalNum:86,TraslatedNum:10,CurrentNode:vgg_16/conv3/conv3_1/weightsINFO:root:TotalNum:86,TraslatedNum:11,CurrentNode:vgg_16/conv3/conv3_1/biasesINFO:root:TotalNum:86,TraslatedNum:12,CurrentNode:vgg_16/conv3/conv3_2/weightsINFO:root:TotalNum:86,TraslatedNum:13,CurrentNode:vgg_16/conv3/conv3_2/biasesINFO:root:TotalNum:86,TraslatedNum:85,CurrentNode:vgg_16/fc8/BiasAddINFO:root:TotalNum:86,TraslatedNum:86,CurrentNode:vgg_16/fc8/squeezedINFO:root:Model translated!
到这一步,我们已经把tensorflow/models下的vgg16模型转换成了Paddle Fluid 模型,转换后的模型与原模型的精度有损失吗?如何预测呢?来看下面。
预测结果差异
加载转换后的飞桨模型,并进行预测
上一步转换后的模型目录命名为“paddle_model”,在这里我们通过ml.ModelLoader把模型加载进来,注意转换后的飞桨模型的输出格式由NHWC转换为NCHW,所以我们需要对输入数据做一个转置。处理好数据后,即可通过model.inference来进行预测了。具体代码如下:
import numpyimport tf2fluid.model_loader as mlmodel = ml.ModelLoader("paddle_model", use_cuda=False)numpy.random.seed(13)data = numpy.random.rand(5, 224, 224, 3).astype("float32")# NHWC -> NCHWdata = numpy.transpose(data, (0, 3, 1, 2))results = model.inference(feed_dict={model.inputs[0]:data})numpy.save("paddle.npy", numpy.array(results))
对比模型损失
转换模型有一个问题始终避免不了,就是损失,从Tesorflow的模型转换为Paddle Fluid模型,如果模型的精度损失过大,那么转换模型实际上是没有意义的,只有损失的精度在我们可接受的范围内,模型转换才能被实际应用。在这里可以通过把两个模型文件加载进来后,通过numpy.fabs来求两个模型结果的差异。
import numpypaddle_result = numpy.load("paddle.npy")tensorflow_result = numpy.load("tensorflow.npy")diff = numpy.fabs(paddle_result - tensorflow_result)print(numpy.max(diff))
打印输出
6.67572e-06
从结果中可以看到,两个模型文件的差异很小,为6.67572e-06 ,几乎可以忽略不计,所以这次转换的模型是可以直接应用的。
需要注意的点
转换后的模型需要注意输入格式,飞桨中输入格式需为NCHW格式。
此例中不涉及到输入中间层,如卷积层的输出,需要了解的是飞桨中的卷积层输出,卷积核的shape与TensorFlow有差异。
模型转换完后,检查转换前后模型的diff,需要测试得到的最大diff是否满足转换需求。
总结
X2Paddle提供了一个非常方便的转换方式,让大家可以直接将训练好的模型转换成Paddle Fluid版本。
转换模型原先需要直接通过API对照表来重新实现代码。但是在实际生产过程中这么操作是很麻烦的,甚至还要进行二次开发。
如果有新的框架能轻松转换模型,迅速运行调试,迭代出结果,何乐而不为呢?
虽然飞桨相比其他AI平台上线较晚,但是凭借X2Paddle小工具,能快速将AI开发者吸引到自己的平台上来,后续的优势将愈加明显。
除了本文提到的tensoflow2fluid,Paddle Fluid还支持caffe2fluid、onnx2fluid,大家可以根据自身的需求体验一下,有问题可以留言交流~
参考资料:
X2Paddle Github:https://github.com/PaddlePaddle/X2Paddle
tensorflow2fluid: https://github.com/PaddlePaddle/X2Paddle/tree/master/tensorflow2fluid
— 完 —
诚挚招聘
量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。
量子位 QbitAI · 头条号签约作者
վ'ᴗ' ի 追踪AI技术和产品新动态