Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 583 articles
Browse latest View live

使用 PMDK 实施面向持久内存的容错算法 - MapReduce 示例

$
0
0

获取示例代码

概述

在本文中,我将使用 libpmemobj 的 C++ 绑定(持久内存开发工具包 (PMDK)的核心库),展示面向持久内存 (PMEM) 的知名 MapReduce (MR) 算法的示例实施。本示例旨在展示 PMDK 如何促进持久内存感知型 MR 的实施,它强调使用多个线程和 PMEM 感知同步,通过事务和并发实现数据一致性。另外,我还将展示 PMEM 的自然容错能力,方法是在中途停止程序并从停止的位置重新启动,而不需要任何检查点/重启机制。最后,我将通过改变 map 和 reduce 工作线程的线程数量,展示灵敏度性能分析。

MapReduce 是什么?

MR 是Google* 在 2004 年推出的一种编程模型,它使用函数式编程概念,设计灵感来自于 Lisp* 等语言的mapreduce基元,可让程序员更轻松地在由数千台机器组成的群集上运行大规模并行计算。

由于所有函数的数据都是相互独立的,即所有输入数据都是按值传递的,这种编程模型已经成为一种处理数据一致性和同步问题的有效解决方案。并行化可以通过并行运行多个函数实例来自然实现。MR 模型可被描述为函数式编程模型的子集,在这个模型中,所有计算仅使用两个函数进行编码:Mapreduce

Map Reduce 流程图
图 1MapReduce 概述。此图是上面提到的 Google 文章的图 1 修订版。

关于 MR 工作原理的高度概述可以在图 1 中找到。输入由一组文件组成,这些文件被分为预定义大小的数据块(通常在 16-64 MB 之间)。每个数据块会被馈送到一个 map 任务,后者将创建被分组、排序并馈送到 reduce 任务的键值对。Reduce 任务可以直接将其结果写入至输出文件,或将它们传递给其他 reduce 任务以便进一步归约。

MR 计算的典型例子是字数统计。输入块分成几行。每行会被馈送到一个 map 任务,后者将为找到的每个词输出一个新的键值对,如下所示:{key : 'word', value : '1'}.Reduce 任务添加同一个键的所有值,并创建一个包含更新值的新键值对。如果我们最后只有一个 reduce 任务,那么输出文件的每个词将只包含一个键值条目,其值为总数。

使用 PMEM 的 MapReduce

MR 模型实现 FT 的方式是将其中间结果存储在驻留于本地文件系统的文件中。这一文件系统通常位于与节点(任务生成数据运行)相连的传统机械硬盘 (HDD) 或固态盘 (SSD) 的顶部。

这种方法的第一个问题在于,这些硬盘和易失性 RAM (VRAM) 内存之间的带宽存在多个数量级的差异。PMEM 技术能够以非常接近 VRAM 的速度运行,从而显著缩小这一差距。鉴于这一点,您可以将本地文件系统安装在 PMEM 设备上,以这种方法作为从机械硬盘或固态盘切换到 PMEM 的首个解决方案。虽然这肯定有帮助,但软件在设计时仍然需要考虑易失性内存和持久内存。例如,数据在持久存储(如逗号分隔值 (CSV) 文件和结构化查询语言 (SQL) 表)中的表现形式与 VRAM(二叉树和堆等)不同。在这里,使用 libpmemobj 库直接针对 PMEM 进行编程可以大幅简化开发!

通过直接针对 PMEM 进行编程,实现 FT 只需指定哪些数据结构应该是永久性的。为提高性能,仍然可以使用传统 VRAM,但需要以透明方式(相对于 VRAM 使用处理器缓存的方式相同)或作为临时缓冲区。另外,为了确保永久数据结构在写入操作过程发生故障时不会被损坏,必须建立事务机制。

设计决策

本节将介绍为使示例具有 PMEM 感知能力而作出的设计决策。

数据结构

这个特定示例专为仅在一个计算机节点上运行而设计,只有一台 PMEM 设备与其相连。工作线程被实施为线程。

我们首先需要一个允许我们将工作分配给 map 和 reduce 工作线程的数据结构。这可以通过任务来实现。将任务分配给工作线程的方式有两种:(1) 推送方式(从主信息推送到工作线程) (2) 拉动方式(工作线程从主信息获取)。在本例中我们选择第二种方式,以便使用持久任务列表简化实施并使用 PMEM 互斥体在工作线程之间进行协调。


图 2:根数据结构。

PMEM 池中的第一个对象始终是根。这一对象作为连接程序中创建的所有其他对象的主要锚点。就我而言,我有四个对象。前两个对象是 pmem::obj版本的 C++ 标准互斥体和条件变量。我们不能使用标准对象,因为 libpmemobj 需要在发生崩溃时重置它们(否则可能会发生永久性死锁)。如欲了解更多信息,请参见 以下关于通过 libpmemobj 进行同步的文章。第三个对象是输入数据,它被存储为一维 持久字符串。第四个对象是我们的任务列表。您可能已经注意到,变量 tlist没有被声明为持久指针。其原因在于,tlist在第一次创建后从未被修改(即被重写),所以在事务处理期间无需跟踪该内存范围。另一方面,map 和 reduce 任务的头变量被声明为持久指针,因为在程序执行期间,它们的值实际上会发生变化(通过添加新任务)。

接下来我们来看看 list_entry类:


图 3:ListEntry类。

  • 变量 next是链表中下一个条目的持久指针。
  • status标记可以取值 TASK_ST_NEW(任务为新任务,并且可以线程可以立即开始处理)、TASK_ST_BUSY(有些线程目前正在处理这一任务)、TASK_ST_REDUCED(这个任务有归约的结果,但尚未与其他归约任务相结合) 或 TASK_ST_DONE(任务最终完成)。
  • task_type标记可以取值 TASK_TYPE_NOTYPETASK_TYPE_MAPTASK_TYPE_REDUCE
  • start_byte在输入数据字符串中保存数据块的起始字节。仅与 map 任务相关。
  • n_lines保存数据块中的行数。仅与 map 任务相关。
  • kv是键值对列表的指针。这一列表仅与 reduce 任务相关。
  • kv_sizekv列表的大小(以元素为单位)。
  • 最后,alloc_byteskv列表的大小(以字节为单位)。

kv之所以是 char[]的持久指针,是出于性能方面的考虑。最初,我将这个列表实施为 kv_tuple 对的链表。然而,由于大量分配(有时每个任务每线程数十万个)非常小的对象(平均在 30-40 字节之间),并且由于这些分配由 libpmemobj 同步以保护元数据的完整性,所以我的代码无法扩展到 8 个线程之外。该变更允许每条线程在为单个任务存储所有键值对时仅执行一次大分配。

您可能也注意到,对于kv_tuple中的密钥,我没有使用前面提到的persistent_string类。其原因在于,persistent_string是针对可以随时间变化的持久字符串变量设计的,因此会为每个新字符串创建两个持久指针:一个用于对象本身,一个用于原始字符串。在这个特定示例中,不需要 persistent_string的功能。键值元组在建造过程中进行批量分配和设置,且在销毁之前不会更改。这会减少库在事务处理期间需要了解的 PMEM 对象数量,从而有效减少开销。

不过,以这种方式分配键值元组有点棘手。

struct kv_tuple { size_t value; char key[]; };

在创建键值元组的持久列表之前,我们需要计算它的大小(以字节为单位)。我们之所以可以这样,是因为主要的计算和排序工作都是先在 VRAM 上完成的,因此能够提前知道总大小。一旦我们完成了这一任务,我们便可以通过一次调用分配所有 PMEM:

void list_entry::allocate_kv (pmem::obj::pool_base &pop, size_t bytes) { pmem::obj::transaction::exec_tx (pop, [&] { kv = pmem::obj::make_persistent<char[]> (bytes); alloc_bytes = bytes; }); }

然后我们将数据复制到我们新创建的 PMEM 对象:

void list_entry::add_to_kv (pmem::obj::pool_base &pop, std::vector<std::string> &keys, std::vector<size_t> &values) { pmem::obj::transaction::exec_tx (pop, [&] { struct kv_tuple *kvt; size_t offset = 0; for (size_t i = 0; i < keys.size (); i++) { kvt = (struct kv_tuple *)&(kv[offset]); kvt->value = values[i]; strcpy (kvt->key, keys[i].c_str ()); offset += sizeof (struct kv_tuple) + strlen (kvt->key) + 1; } kv_size = keys.size (); }); }

除了强制池对象 pop(这个对象不能存储在持久内存中,因为它是在每个程序调用时新创建的)之外,该函数的输入都是两个易失性向量,包含由 map 或 reduce 任务生成的键值对。由于每个 kv_tuple的大小不是常量(取决于其密钥的长度),所以kv通过字节偏移(for loop 内的第一个和最后一个语句)迭代。

同步

以下伪代码表示工作线程的高级逻辑:

  1. 等到有新的任务可用。
    • map 工作线程每次只处理一个任务。
    • 如果可能,reduce 工作线程尝试处理两个任务(并将它们合并起来)。如果处理不了,那么只处理一个任务。
  2. 处理任务并设为TASK_ST_DONE(或者 TASK_ST_REDUCED,如果是处理单个任务的 reduce 工作线程)。
  3. 将结果存储在状态为 TASK_ST_NEW的新建任务中(最后一个任务有整个计算的结果,并直接创建为 TASK_ST_DONE)。
  4. 完成计算后(所有任务都为 TASK_ST_DONE),退出。
  5. 转至 (1)。


接下来我们来看一下 map 工作线程的步骤 (1):

void pm_mapreduce::ret_available_map_task (pmem::obj::persistent_ptr<list_entry> &tsk, bool &all_done) { auto proot = pop.get_root (); auto task_list = &(proot->tlist); /* LOCKED TRANSACTION */ pmem::obj::transaction::exec_tx ( pop, [&] { all_done = false; if ((task_list->ret_map (tsk)) != 0) { tsk = nullptr; all_done = task_list->all_map_done (); } else tsk->set_status (pop, TASK_ST_BUSY); }, proot->pmutex); }

这段代码中最重要的部分位于事务内部(在以pmem::obj::transaction::exec_tx开始的数据块内)。这一事务需要被锁定,因为每个任务只能由一个工作线程执行(意识到我在事务结束时使用来自根对象的持久互斥体)。调用task_list->ret_map()方法检查是否有新的 map 任务。如果有,我们将其状态设为 TASK_ST_BUSY,防止其被其他工作线程获取。如果没有新任务,则调用task_list->all_map_done()检查是否已完成所有 map 任务,在这种情况下线程将退出(在此代码片段中未显示)。

从这段代码中学到的另一个重要教训是,每次在锁定区域内修改数据结构时,该区域应与事务同时结束。如果线程更改了锁定区域内的数据结构,然后很快发生故障(但未完成事务),则在锁定时完成的所有更改都会回滚。与此同时,另一个线程可能已经获取该锁,并可能正在故障线程所作变更的基础上进行其他更改,从而最终破坏数据结构。

避免这种情况的一种方法是将持久互斥体传递给事务,从而锁定整个事务(如上面的代码片段所示)。然而,有些情况是不可行的(因为整个事务实际上是序列化的)。在这些情况下,我们可以通过将同步写入置于嵌套锁定事务中,将同步写入留到事务结束。尽管嵌套事务在默认情况下是扁平化的,这意味着我们最后拥有的仅仅是最外层的事务。嵌套事务的锁只从嵌套事务开始的位置锁定最外层的事务。这可以在以下代码片段中看到:

..... auto proot = pop.get_root (); auto task_list = &(proot->tlist); pmem::obj::transaction::exec_tx (pop, [&] { ....../* 这部分事务可以由所有线程同时执行*。*/ ...... pmem::obj::transaction::exec_tx ( pop, [&] { /* this nested transaction adds the lock to the outer one.* This part of the transaction is executed by only one * thread at a time */ task_list->insert (pop, new_red_tsk); proot->cond.notify_one (); tsk->set_status (pop, TASK_ST_DONE); }, proot->pmutex); /* end of nested transaction */ }); /* end of outer transaction */

对于 reduce 工作线程来说,情况 (1) 更复杂,所以我不会在这里将其全部重现。不过,有一部分值得讨论:

void pm_mapreduce::ret_available_red_task ( pmem::obj::persistent_ptr<list_entry> (&tsk)[2], bool &only_one_left, bool &all_done) { auto proot = pop.get_root (); auto task_list = &(proot->tlist); /* locked region */ std::unique_lock<pmem::obj::mutex> guard (proot->pmutex); proot->cond.wait ( proot->pmutex, [&] { /* conditional wait */ .....}); ..... guard.unlock ();

map 工作线程和 reduce 工作线程之间的主要区别在于,reduce 工作线程进行有条件等待。map 任务在计算开始之前立即创建。因此,map 工作线程无需等待创建新任务。另一方面,reduce 工作线程将进行有条件等待,直到其他工作线程创建新 reduce 任务。reduce 工作线程被唤醒时(另一个工作线程proot->cond.notify_one()在创建新任务并插入列表后运行),布尔函数(传递给 proot->cond.wait())将运行,以检查工作线程是否应继续。reduce 工作线程将在以下两种情况下继续:(a) 至少有一个任务可用 (b) 所有任务最终完成(线程将退出)。

容错

本文中介绍的示例代码可以从 GitHub*下载。这一代码将通过从一般 PMEM MapReduce 类继承并完成完成虚拟函数 map()reduce()来实现 PMEM 版本的 wordcount 程序:

class pm_wordcount : public pm_mapreduce { public: /* constructor */ pm_wordcount (int argc, char *argv[]) : pm_mapreduce (argc, argv) {} /* map */ virtual void map (const string line, vector<string> &keys, vector<size_t> &values) { size_t i = 0; while (true) { string buf; while (i < line.length () && (isalpha (line[i]) || isdigit (line[i]))) { buf += line[i++]; } if (buf.length () > 0) { keys.push_back (buf); values.push_back (1); } if (i == line.length ()) break; i++; } } /* reduce */ virtual void reduce (const string key, const vector<size_t> &valuesin, vector<size_t> &valuesout) { size_t total = 0; for (vector<size_t>::const_iterator it = valuesin.begin (); it != valuesin.end (); ++it) { total += *it; } valuesout.push_back (total); } };

构建指令

若要编译 mapreduce 代码示例,只需从 pmem/pmdk-examples GitHub 库的根目录键入make mapreduce即可。如欲了解更多信息,请阅读 mapreduce 示例README文件。

运行示例的说明

编译后,您可以运行无参数的程序来获取使用帮助:

$ ./wordcount USE: ./wordcount pmem-file <print | run | write -o=output_file | load -d=input_dir> [- m=num_map_workers] [-nr=num_reduce_workers] command help: print -> Prints mapreduce job progress run -> Runs mapreduce job load -> Loads input data for a new mapreduce job write -> Write job solution to output file command not valid

若要了解 FT 的工作方式,请使用一些示例数据运行代码。就我而言,我使用所有维基百科摘要 (文件大小为 5GB,因此可能需要很长时间才能加载到浏览器;您可以通过右击 > 另存为来下载)。运行 MR 之前的第一个步骤是将输入数据加载到 PMEM:

$ ./wordcount /mnt/mem/PMEMFILE load -d=/home/.../INPUT_WIKIABSTRACT/ Loading input data $

现在我们可以运行该程序(在这种情况下,我对 map 工作线程使用两个线程,对 reduce 工作线程使用两个线程)。取得一些进度之后,我们将按Ctrl-C,终止该任务:

$ ./wordcount /mnt/mem/PMEMFILE run -nm=2 -nr=2 Running job ^C% map 15% reduce $

我们可以用 print命令查看进度:

$ ./wordcount /mnt/mem/PMEMFILE print Printing job progress 16% map 15% reduce $

到目前为止,我们的进度已保存!如果我们再次使用 run命令,计算将从我们中断的位置重新开始(16% map 和 15% reduce):

$ ./wordcount /mnt/mem/PMEMFILE run -nm=2 -nr=2 Running job 16% map 15% reduce

计算完成后,我们可以将结果(命令 write)转储到常规文件并读取结果:

$ ./wordcount /mnt/mem/PMEMFILE write -o=outputfile.txt Writing results of finished job $ tail -n 10 outputfile.txt zzeddin 1 zzet 14 zzeti 1 zzettin 4 zzi 2 zziya 2 zzuli 1 zzy 1 zzz 2 zzzz 1 $

性能

所用系统拥有 28 核英特尔® 至强® 铂金 8180 处理器 CPU(224 个线程)和 768 GB 的英特尔® 双倍数据速率 4(英特尔® DDR 4)内存。若要模拟安装在 /mnt/mem 的 PMEM 设备,使用 512 GB 的内存。使用的操作系统是内核版本为 4.9.49 的 CentOS Linux* 7.3。所用的输入数据还是 所有维基百科摘要 (5 GB)。在实验中,我为 map 分配一半线程,为 reduce 任务分配一半线程。

图 4:使用我们的 PMEM-MR 示例对所有维基百科摘要 (5 GB) 中的词进行计数所用的时间。

可以看到,我们的示例可以一直扩展到 16 个线程(大约将完成时间减半)。在 32 个线程中仍然有所改进,但只有 25%。在 64 线程中,我们达到了这个特定示例的可扩展性限制。这是因为随着更多线程用于相同的数据,同步部分在总执行时间中占据较大比例。

总结

在本文中,我们使用 PMDK 库 libpmemobj 的 C++ 绑定展示了知名 MR 算法的示例实施。我展示了如何使用 PMEM 互斥体和条件变量,通过事务和并发来实现数据一致性。另外,我还展示了 PMDK 如何促进 FT 程序的创建,支持编程人员直接针对 PMEM 进行编码(也就是定义哪些数据结构应该被持久化)。最后,我进行了灵敏度性能分析,展示了将更多线程添加到执行时的可扩展性。

关于作者

Eduardo Berrocal 于 2017 年 7 月加入英特尔,担任云软件工程师。他拥有伊利诺斯州芝加哥伊利诺理工学院 (IIT) 的计算机科学博士学位。他的博士研究方向主要为(但不限于)数据分析和面向高性能计算的容错。他曾担任过贝尔实验室(诺基亚)的暑期实习生、阿贡国家实验室的研究助理,芝加哥大学的科学程序员和 web 开发人员以及西班牙 CESVIMA 实验室的实习生。

资源

  1. MapReduce: 简化大型集群数据处理,Jeffrey Dean 和 Sanjay Ghemawat,https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf
  2. 使用骨架函数进行并行编程,J. Darlington 等人,西澳大利亚大学计算机科学系http://pubs.doc.ic.ac.uk/parallel-skeleton/parallel-skeleton.pdf
  3. 持久内存编程,pmemobjfs - 基于 FUSE 的简单 libpmemobj, 2015 年 9 月 29 日, http://pmem.io/2015/09/29/pmemobjfs.html
  4. 持久内存编程libpmemobj 的 C++ 绑定(第 7 部分) - 同步原语, 2016 年 5 月 31 日, http://pmem.io/2016/05/31/cpp-08.html
  5. 持久内存编程使用 libpmemobj C ++ 绑定建模字符串,2017 年 1 月 23 日,http://pmem.io/2017/01/23/cpp-strings.html
  6. 链接到 GitHub 中的示例代码*
  7. https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-abstract.xml
  8. Pmem.io 持久内存编程如何模拟持久内存,2016 年 2 月 22 日,http://pmem.io/2016/02/22/pm-emulation.html

人工智能驱动型测试系统检测水中细菌

$
0
0

Hands in water

“净水、医疗、学校教育、食物、铁皮屋顶、水泥地面等各种要素都应属于每个人赖以生存的基本权利”。1

– Paul Farmer,美国医生、人类学家兼联合创始人,
Partners In Health

挑战

获取清洁水对于世界上的许多人来说都是一个难题。测试和确认清洁水源通常需要使用昂贵的测试设备和手动分析测试结果。对于获取清洁水仍然存在困难的地区,简单的测试方法可帮助有效预防疾病和拯救生命。

解决方案

为了将人工智能 (AI) 技术应用于评估水源的纯度,英特尔®软件创新者 Peter Ma 开发了一种有效的系统,帮助使用模式识别和机器学习识别细菌。通过将数字显微镜连接至运行 Ubuntu* 操作系统和英特尔® Movidius神经计算棒的笔记本电脑,可完成离线分析。完成分析后,在地图上实时标记污染点。

背景和历史

Peter Ma 为英特尔®人工智能研究院计划做出了重要贡献,经常参与黑客马拉松比赛,而且多次获奖。Peter 表示:“对于新鲜事物,人们一开始都会不太理解,而我对于新技术总是非常着迷”。

Peter 在 2010 年 Move Your App! Developer Challenge (TEDprize 主办的比赛)中赢得重要奖项,凭此获得了在 TEDGlobal 上发表演讲的机会。这一经历增强了 Peter 使用技术提高人类生活质量的意愿。参赛者需接受名厨和餐馆老板的挑战,高效跟踪儿童肥胖症的情况。

多年来,Peter 利用其设计和开发技能建立了稳固的咨询业务。Peter 表示:“我负责为从财富 500 强公司到小型初创企业的不同客户构建原型”。在咨询工作之余,我参加了许多黑客马拉松比赛,施展自己的丰富创意。我专为 World Virtual GovHack 构建了清洁水人工智能 (Clean Water AI) 作品,参加水安全和食品安全类别的角逐。

总部位于阿联酋迪拜的 GovTechPrize 每年都会将多个类别的奖项授予帮助应对全球迫切挑战的卓越技术解决方案。为鼓励广大学生和初创企业通过试验先进技术化解各种挑战,该机构推出了一项比赛,即全球虚拟黑客马拉松比赛 World Virtual GovHack。

为所有人创造 ∀I 的美好未来

Peter Ma

图 1.Peter Ma 演示清洁水测试系统。

“我们最初于 2017 年 12 月开始专为 World Virtual GovHack 设计该系统”。在此次比赛中,我荣膺第一名,并在 2018 年 2 月的颁奖典礼上从迪拜 Mansoor 王子手动接过 200,000 美元的奖金。这一荣誉将促进该项目的进一步发展。目前,我们处在原型设计阶段,正在设计原型的下一个迭代,确保它可部署在单个物联网设备中。在我看来,创新永无止境,我们需要不断完善上一个迭代”。

Peter 参加黑客马拉松比赛的高成功率令人印象深刻,激励着其他参赛者设计 Doctor Hazel、Vehicle Rear Vision 和 Anti-Snoozer 等出色项目。他表示:“我对于自己在多数黑客马拉松比赛中的表现感到满意,我的关注重点是技术如何改善人们的生活,而非技术有哪些绚酷功能”。

重要的项目里程碑

  • 2017 年 12 月启动开发工作。
  • 2018 年 2 月提交项目,在 World Virtual GovHack 上赢得一等奖,收获 200,000 美元的奖金,这笔资金将帮助推进 Clean Water AI 项目的下阶段工作。
  • 开始为新版测试系统设计原型,该系统可嵌入独立的物联网设备。
  • 通过为圣迭哥设计一种可消除水污染的供水系统,在 SAP Spatial Hackathon 中荣获第一名。
  • 将于 2018 年 4 月在纽约 O’Reilly 人工智能大会上进行演示。

Peter Ma receives the top GovTechPrize

图 2.Peter Ma 荣获 GovTechPrize 水安全和食品安全类别的大奖。

每分钟有一名新生儿因缺乏安全水和环境污染而夭折。2

– 世界卫生组织,2017

支持技术

Clean Water AI 项目受益于英特尔® AI DevCloud,这是一种面向英特尔人工智能研究院成员的免费托管平台。该平台基于英特尔®至强®可扩展处理器,为满足深度学习训练和推理计算需求而优化。Peter 利用英特尔 AI DevCloud 训练人工智能模型和英特尔 Movidius 神经计算棒实时执行水测试。该神经计算棒支持深度学习开发人员常用的 Caffe* 和 TensorFlow* 框架。

英特尔® Movidius软件开发套件具有重要的开发功能,提供了一种简化的机制分析、调整和部署神经计算棒上的卷积神经网络功能。由于 Clean Water AI 测试系统必须在没有云服务的情况下执行实时分析和识别污染物,因此神经计算棒中经人工智能优化的独立特性对于该测试系统的正常运行至关重要。神经计算棒是一种紧凑的无风扇设备,其大小与一般的 U 盘相当,具有快速 USB 3.0 的吞吐量,可用于在物联网边缘有效部署高效的深度学习功能。

“英特尔可满足人工智能从训练到部署的整个过程中的软硬件需求。对于初创企业而言,构建原型的成本相对较低。人工智能训练可通过英特尔 AI DevCloud 免费进行,任何人都可以注册。英特尔 Movidius 神经计算棒售价约为 79 美元,支持人工智能实时运行。”

– Peter Ma,英特尔人工智能研究院软件创新者

神经计算棒可充当推理加速器,其额外优势是无需互联网连接便可运行。神经网络需要的所有数据存储在本地,这使得快速、实时运行成为可能。任何需要访问远程服务器数据的测试系统都将受制于连接可用性(尤其在急需测试的农村地区)及潜在的服务中断和分析延迟。对于需要更高推理性能实施高强度应用的开发人员而言,可通过组合多达四个计算棒实施特定解决方案。

Clean Water AI 测试系统

Clean Water AI 测试系统包括简单的现成低成本组件:

  • 成本为 100 美元或更低且容易买到的数码显微镜
  • 运行 Ubuntu 操作系统的中档电脑
  • 实时运行机器学习和人工智能的英特尔 Movidius 神经计算棒

整个测试系统需要不到 500 美元的构建成本,是通常买不起传统昂贵测试系统的企业的实惠之选。

图 3 显示了基本的测试设置。

Microscope, laptop, and compute stick

图 3.显微镜、笔记本电脑和计算棒构成的基本测试系统需要不到 500 美元的装配成本。

作为该测试系统的核心,卷积神经网络可确定细菌的形状、颜色、密度和边缘。目前的识别范围限于大肠杆菌 (E. coli) 和导致霍乱的细菌,但由于不同类型的细菌具有独特的形状和物理特征,识别范围可扩展至许多不同类型。近期的项目目标包括区分无害微生物和有害细菌,检测矿物质等物质,满足不同地区的认证要求等。

为改进这一方法和提升识别精度,Peter 进行了持续训练。目前,测试的可信度高于 95%(最高为 99%),测试时需比较清洁水和污水,而且随着更多图像添加至该系统和训练持续加强,这一结果可能进一步改善。

在 Clean Water AI 测试系统的视频演示中,Peter 使用显微镜首先捕捉一幅清洁水图像,然后将其与污水样本进行比较。该系统可立即检测有害细菌,并在地图上标记污染情况。所有这些工作都可实时进行。

图 4 中渲染图所示的大肠杆菌通常出现在污水中,可被该系统根据形状和大小准确识别。

更多信息请访问 Peter Ma 的 Clean Water AI 项目

E. coli bacteria

图 4.大肠杆菌(最常见、最危险的水体污染物之一)的渲染图。

人工智能正在改变商业与科学的面貌

通过专用芯片的设计和开发、研究资助、教育推广与行业合作等,英特尔坚定致力于推动人工智能 (AI) 的发展,帮助化解医学、制造、农业、科学研究和其他行业的挑战。英特尔与政府机构和公司紧密合作,探索和完善可应对重大挑战的解决方案。

例如,我们与 NASA 的一项合作旨在筛查月球的大量图像,识别其不同特征,如陨石坑。通过使用人工智能和计算技术,NASA 在两周而非数年时间内实现项目目标。

英特尔人工智能产品组合包括:

Xeon inside

英特尔® 至强® 可扩展处理器:借助针对深度学习等广泛人工智能工作负载优化的计算架构,化解人工智能挑战。

Logos

框架优化:在强大的可扩展基础设施上更快速训练深度神经网络。

Movidius chip

英特尔®  Movidius Myriad™ 视觉处理单元 (VPU):创建和部署设备上神经网络和计算机视觉应用。

如欲获取更多信息,请访问 产品组合页面

Rome fountain

“英特尔树立了明确目标:改变计算面貌,提升人类的能力,并革新各行各业。目前,人工智能催生了大量工具,大幅提升了数据筛查的可扩展性,帮助人类增强了智能化水平。我们希望我们的机器能够提供个性化体验,改变和适应人们的购物方式及与他人互动的方式。

许多变化已经悄悄来临。英特尔推出了广泛的人工智能产品组合。我们首先发布了英特尔? 至强? 可扩展处理器,这一通用计算平台可为深度学习提供高效的推理功能。”

– Naveen Rao,英特尔副总裁兼人工智能产品事业部总经理

 

人工智能的美好未来

人类刚开始认识和发掘人工智能的无限可能性。英特尔人工智能研究院将与该领域的领导者及才能卓越的软件开发人员和系统架构密切合作,探索有望重塑当前人类生活的全新解决方案。我们诚邀志同道合的创新者加入这一非凡事业,为先进技术的发展指明新方向,共同造福全人类。

立即加入

Peter 表示:“人工智能将帮助政府和非政府机构推动未来发展,尤其是在水安全保障等资源监控方面。相比当前的系统,人工智能可降低成本,提供更准确的持续监控。水安全方面的人工智能设备采用光学读数而非化学方法,因此通常需要极少的维护工作”。

“我们能够为地球上的所有男人、女人和儿童提供清洁水,所缺乏的是全体人类实现这一目标的坚定意志。我们还在等什么?我们需要立即做出承诺”。3

– Jean-Michel Cousteau

资料来源

Clean Water AI
英特尔 DevMesh 中的 Clean Water Project
在英特尔 Movidius 神经计算棒上构建图像分类器
借助 Caffe* 深度学习框架发挥英特尔架构的最大价值
借助移动电子实施快速水传播病原体检测
英特尔 Movidius 神经计算棒
GovTech Prize
英特尔处理器支持深度学习训练

 

1 http://richiespicks.pbworks.com/w/page/65740422/MOUNTAINS%20BEYOND%20MOUNTAINS

2 http://www.who.int/mediacentre/factsheets/fs391/en/

3 http://www.architectsofpeace.org/architects-of-peace/jean-michel-cousteau?page=2

利用 Unreal Engine* 4 的粒子参数提升视觉效果

$
0
0

 

粒子参数是内置于 Unreal Engine* 的强大系统,支持在 Unreal Engine 4 的 Cascade 粒子编辑器外定制粒子系统。本教程将创建这样一个系统,并演示如何运用该系统来提高视觉逼真度。

为何使用粒子参数?

粒子参数是游戏利用粒子系统发挥最大潜能的必要条件,旨在使粒子系统动态响应周围的世界。

概述

在本教程中,我们将结合使用粒子参数和 CPU 微粒来根据游戏元素更改场景中的亮度,在本场景为篝火的燃料余量(见图 1)。随着燃量的减少,粒子系统产生的视觉效果以及系统中的火粒子所产生的火光也会相应地降低。燃料烧完后,我们开始重新填充燃料,直到篝火重新点燃。这样会形成一个展示整个粒子效果的良好循环。

digital campfire

digital campfire

图 1.采用粒子参数后的篝火。

将参数添加至 P_Fire

Particle parameters interface
图 2.粒子参数。

为了营造这种粒子效果,我们修改 Unreal Engine 入门内容中包含的 P_Fire 粒子系统。在图 2 中,紫色突显部分是我们修改的模块,橙色突显部分是我们添加的模块。

修改亮度

亮度是使用 CPU 粒子的主要优势之一,是这种效果的核心。

Settings interface
图 3.第一个发光体。

在“分布”下拉菜单中选择“参数分布”

在 P_Fire 粒子系统中的第一个发光体的细节面板中,从 Brightness Over Life Distribution下拉菜单中选择 Distribution Float Particle Parameter,如图 3 顶部所示。这样我们可以将发出的火光绑定至一个变量,在本示例中篝火的燃料余量。

设置名称

下一步是指定此分布将绑定至哪个粒子参数。我们使用名称 “FuelLeft”。将此名字输入至 Parameter Name字段,如图 3 所示。

设置映射模式

输入映射是粒子参数的一项重要特性。该特性支持我们指定可接受的最大和最小输入,并将这些值扩展至给定范围,以便单个输入参数功能无缝地用于多个不同的模块。这种功能使我们能够在不同的点降低不同部分的粒子效果。火花和火苗等效果仅在篝火开始变暗时改变一次,而且我们将通过设置其输入范围来反映这种变化。由于我们希望固定输入并将其扩展至特定范围,所以我们使用 DPM Normal来完成本教程中的所有分布。它位于 Param Mode下拉菜单中,如图 3 所示。

设置输入范围

接下来我们指定最小和最大输出。为了产生这种效果,我们设 0.0为最小输出,1.0为最大输出,如图 4 所示。这意味着这堆篝火所发出的火光可以在 0% 燃料(全暗)和 100% 燃料(旺火)之间变化。

Settings interface
图 4.设置输入范围。

设置输出范围

输出范围可以让我们指定这堆篝火所产生的最小亮度和最大亮度。值的设置范围如图 5 所示。

Settings interface
图 5.设置输出范围。

设置默认输入值

现在我们需要设置一个默认输入值,以应对没有给定效果值的情况。这一步可通过 Constant来完成(见图 6)。对于这种粒子系统,我们设为默认 1.0,或旺火。

Settings interface
图 6.设置默认值。

设置模块的其他部分

第二个发光体

为确保篝火所发出的火光与粒子系统中的粒子保持一致,我们修改第二个发光体的火光模块。修改第二个发光体中火光模块的 Brightness Over Life部分,以匹配图 7 所示的数值。如果我们不扩展光源,篝火在只剩火苗的时候仍然会发出旺火火光。

Settings interface
图 7.第二个发光体。

第一个和第二个发光体扩展

当前,篝火所产生的火光亮度将随着燃料的变化而变化,但火焰的大小不变。为了改变火焰的大小,我们将“Size Scale” 发光体添加至第一个和第二个发光体,如图 2 所示。其分布将变成“Vector Particle Parameter”,而非“Float Particle Parameter”。由于我们提供与“Float Particle Parameter”相同的参数名称,因此 Cascade 为我们的矢量拷贝所有三个字段的浮点值。对于这两个模块,我们希望图形在 0 - 100% 的燃料量中变化,因此我们只需修改字段 Parameter NameConstant。设置两个模块以匹配图 8 中所示的数值。

Settings interface
图 8.发光体缩放。

烟雾产生率

小火会产生少量的烟,我们可以通过修改粒子系统来反映这种情况。为此,我们在烟雾发射体上的产烟模块,设置速率部分的粒子参数。但与我们之前设置的粒子参数不同,我们只希望在燃料少于 40% 时产生烟雾。为此我们将 Max Input设为 0.4,而非 1。设置 Distribution以匹配图 9 中的数值。

Settings interface
图 9.烟雾产生率。

火苗产生率

火苗也随着火焰大小的变化而变化,但只会在火焰确实变小时变小。为了达到这种效果,我们在 50% (0.5) 时开始减小火苗。设置火苗发射体上的 Spawn Rate Distribution部分以匹配图 10 所示的数值。

Settings interface
图 10.火苗产生率。

变形产生率

火焰的变形需要用减小火焰的方式来更改。我们在 0-100% 燃料范围之间更改火焰,变形同样需要这样操作。设置变形发射体上的 Spawn Rate Distribution部分以匹配如图 11 所示的数值。

Settings interface
图 11.变形产生率。

设置蓝图

既然篝火效果可以随着燃料的变化而变化,那么我们需要设置蓝图来设置燃料量。在本教程中,通过慢慢减少燃料然后重新补充燃料来展示这种效果。为了创建有关此效果的蓝图,将粒子效果拖放至场景中,然后单击细节面板中的 Blueprint/Add Script

设置变量

为了达到这种效果,我们只需设置两个变量,如下图 12 所示:

FuelLeft:跟踪篝火中的燃料量的浮点,范围从 1(100% 燃料) 到 0(0% 燃料)。默认设为 1,因此篝火一开始为旺火。

FuelingRate:表示燃料减少或填充快慢的浮点。在本教程中,我们设默认值为 -0.1(每秒 -10%)。

创建完这两个变量后,蓝图中的变量部分将匹配图 12 中的数值。

Settings interface
图 12.火焰变量。

更改燃料余量

为了达到这种效果,需要更改每个 tick 的燃料余量,并将其应用于粒子系统。为此,我们让 Fueling Rate乘以 Delta Seconds,然后加上 Fuel Left。之后此数值设为 Fuel Left

为了将 Fuel Left应用于粒子系统,我们使用Set Float Parameter借点。为此,我们使用修改的 P_Fire 粒子系统组件,并将 Fuel Left用于 Param。参数名称必须为我们在粒子系统中所使用的名称,在本教程中为 FuelLeft

Settings interface
图 13.修改燃料余量。

设定燃料余量限值

最终篝火的燃料会耗尽。在本教程中,我们希望切换至为篝火增加燃料,而不是让燃料耗尽。为此,我们继续操作 tick 并查看新燃料值是否太低(小于或等于 -0.1)或太高(大于或等于 1.0)。因此我们将下限设为 -0.1,以便重新添加燃料之前篝火熄灭一会。这不会导致任何问题,因为我们设定了最小输入,所以传递至粒子系统所有低于 0 的值都将视为 0。

如果发现 Fuel Left超出限值,我们将用 Fueling Rate变量乘以 -1。如果 Fuel Left持续减少,会造成在之后的 tick 中增加,反之亦然。

Settings interface
图 14.设定燃料余量限值。

虚拟现实技术帮助医生和患者为重大手术做好充分准备

$
0
0

英特尔 iQ 执行编辑 Ken Kaplan 

沉浸式 3D 模拟帮助脑外科医生和患者为复杂的手术做准备,提供大脑 VR 之旅,最终减轻患者和家属对外科手术的恐惧。

虚拟现实 (VR) 技术让所有人都能探索宇宙,并从视觉上更好地了解宇宙。VR 还提供关于大脑内部结构的详细视图,帮助医生观察和探索大脑的运行方式,并在大脑出现问题时提供诊断和治疗计划。

英特尔销售与营销事业部医疗和生命科学部门总经理 Jennifer Esposito 表示,越来越多的神经外科医生和其他医疗专家使用 VR 为复杂的手术过程做准备。

她说:“在医疗领域,VR 技术拥有很多潜在的变革性机遇。”

为了做好脑部手术的准备,医生和患者使用俄亥俄州 Surgical Theater公司的 VR 技术,该公司能够通过大量 CT 和 MRI 扫描,创建患者脑部的 360 度 VR 模型。

3D 沉浸式技术不仅可以帮助外科医生进行手术训练,还在患者参与手术和提高手术满意度方面发挥着重要的作用。患者和家属可以直观地了解他们的医生如何计划治疗危及生命的脑部肿瘤或脑血管疾病。

Esposito 表示:“设想一下复杂的大脑结构,以及危及生命的诊断给患者所带来的种种焦虑,此时医生可以通过有关 CT 和 MRI 影像 VR 图像,在患者上手术台之前为他们详细解释大脑的情况,让他们调整好心态准备手术。”

促进医生和患者相互协作

2D MRI 和 CT 黑白图像解读起来非常困难。相反,栩栩如生的 3D 模拟可以从各个角度展示细节。医生可以放大和缩小图像,甚至还可佩戴 VR 头盔沉浸在画面之中,更好地观察每一个细节。

Jessica Morrow 的儿子 Kobe 曾患有严重的脑畸形,她说:“看到这种 3D 图像,确实可以帮助我们了解 Kobe 大脑的真实情况。”UCLA 小儿神经外科医生 Aria Fallah 博士在治疗 Kobe 的过程中使用了 Surgical Theater 的 VR 技术。

Morrow 说:“[我们]能够更清楚地知道所面临的情况,而且能更轻松地了解[医生]所采取的治疗方法。”

Morrow 一家观看 VR 图像,同时听 Aria Fallah 博士解释 Kobe 的手术计划。

Esposito 表示,VR 技术将成为一种重要的医疗工具,因为它能帮助提高医疗服务质量,以及患者的整体体验和满意度。

Esposito 说:“经证明,医生和患者之间共享决策过程可以降低长期护理成本,患者也可以更多地参与他们的术前和术后护理计划。

当医生和患者就护理问题相互配合时,整体医疗费用可降低 5.3%,二次住院频率会降低 12.5%。

患者和医生越来越青睐采用 VR 技术来帮助他们之间更好地合作。”她指出,根据 Accenture的调研,提供良好患者体验的医院,其盈利高 50%。

从模拟战斗机到挽救生命

Surgical Theater 创始人是以色列空军 Moty Avisar 和 Alon Geri,曾为 F-16 战斗机设计战斗机模拟器。在英特尔的帮助下,他们创建的 VR 技术在多家顶级研究医院试用,包括 UCLA、纽约大学、梅奥医学中心、西奈山医学院和斯坦福大学。

在加州纽波特海滩的 Hoag 医院,神经外科医生 Robert Louis 使用 360 度 VR 技术,找到了在动脉和视神经附近生长的肿瘤。Precision VR 让神经外科医生能够从多个角度观察患者的脑部,并练习他们的手术切入点。

Louis 说:“有很多其他州的患者说要来进行手术,我想用基于虚拟现实系统的 Surgical Theater 来完成手术。”

VR 可提供关于人脑内部的详细视图,包括手术切入点。

Hoag 医院是奥兰治县的一家非营利性的区域性医疗服务机构,每年接待近 30,000 名住院病人和 350,000 名门诊名人。使用 VR 技术 9 个月后,医院报告称,相比其他建议手术的医院,选择在 Hoag 进行手术的患者比例从 62% 增加到 84%。

Louis 说,第一次见到 VR 技术时就坚信它将是医疗领域下一次重大的技术飞跃。他认为,VR 是过去半个世纪以来神经外科领域最重要的进步之一。

他说:“其中一个[进步]是显微镜技术,另一个是[手术]导航系统。这些技术对保障手术安全发挥着极其重要的意义。”

英特尔商用 VR 解决方案总监 Kumar Chinnaswamy 清晰地记得第一次听说使用医疗 VR 给一个小男孩做手术的情景。男孩的父母不会说英语,也听不懂医生的话。

医生采用了 VR 技术,情形瞬间发生了变化。

Chinnaswamy 说:“他们将头盔戴在孩子头上,医生像孩子展示之后的手术过程。孩子立刻活跃起来,向父母解释将要发生的事情。

非常神奇。这就是技术改变生活的真实写照。”

编者注:Rob Kelton 制作了英特尔视频讲述这个故事。了解数据科学与技术如何帮助医疗服务机构治疗患者和挽救生命。

使用 Google Blocks* 在虚拟现实中设计素材原型

$
0
0

House render

本文介绍了如何使用 Google Blocks* 在虚拟现实中快速建模,以改进您的虚拟现实(VR)项目的工作流。深入研究软件的使用方法之前,我们来看一下典型的工作流。

工作流

虚拟现实开发的典型工作流

典型的虚拟现实项目是代码和素材的反复结合。概念交互和素材通常有一个预生产时期(通过草图)。但是,一旦生产开始后,您通常会多次启动与停止任务,因为代码开发方必须等待素材完成,反之亦然。

开发虚拟现实项目面临更严峻的情况,因为虚拟现实独特的视角引发了规模、布局和细节问题,这些问题存在于使用 2D 显示器展示的 3D 项目中。因此,您必须反复进行素材开发,才能得到正确的想法。在原型设计阶段,您还会常常看到低质量的演示,这将不利于您吸引资金和关注。

使用虚拟现实进行原型设计的优势

在虚拟现实中创建原型模型素材可为代码与素材开发人员创造更流畅的流程。相比仅使用草图,该方法支持代码开发人员更快速地设计与调整他们理想模型的粗略版本,获得可玩的对象并为素材开发人员提供参考材料。建模一般需要掌握使用各种工具和摄像头视角的特殊技术,才能利用 2D 界面创建 3D 对象,实现高精度和栩栩如生的细节,但是以繁重的工作负载为代价。

另一方面,在虚拟现实中建模使您在自然的 3D 环境中工作,实际上您是在房间大小的真实环境中移动和设计。通过采用该方法,3D 建模更像捏黏土,而不是调整建模应用中的顶点。创建包含少量细节的原型素材时,该方法不仅速度更快,而且更方便缺乏建模技术的用户快速入门。

Google Blocks 低面多边形模型的优势

Google Blocks 提供了一种“通过创建与修改基元进行构建”的简单的虚拟现实设计方法,还支持快速的立体“绘图”。通过将修改后的形状和颜色纯净的表面结合在一起,营造了一种简洁而低面数的唯美风格。这极大提升了性能,在原型设计阶段非常实用,因为该阶段的性能未经优化。简洁的外观甚至不需要修改,便可用作非常美观的演示。

面向虚拟现实开发的工作流修改

全新的生产工作流转变了不断启动与停止的“瀑布式”开发,支持代码团队在原型设计阶段根据他们的需要提供原型,无需等待素材团队。通过该方法,素材团队可以使用原型模型和预生产草图开发完善的素材,并将这些素材轻松替换为代码团队已经实施的工作原型。

使用 Unity* 软件等开发工具中的基元完成所有必要的原型“构建”,想法很简单,但是缺乏真实的粗略模型将会增加开发正确交互和测试的困难。当您试图使用立方体、球体、圆柱体构建对象时,通常会发现进展缓慢。借助新的工作流,您可以快速获取更接近成品素材的形状,并且利用交互开发中学到的知识进一步指导最终素材的开发。

工具概述

我们来列举开发中使用的工具。除 HTC vive* 之外,所有工具均是免费的。如上所述,我们将使用 Google Blocks 应用和 HTC vive 构建虚拟现实模型。接下来,我们将 Google Poly* 云服务使用导出与共享模型。然后,我们将使用 Unity 软件中的模型进行实际开发。最后,我们将借助 Blender* 中的模型进行最终的素材开发。除 Blender 之外,您也可以使用 Maya* 或任何您喜爱的 3D 建模应用,或者将 Unity 替换为 Unreal Engine*(也是免费的)。

使用 Google Blocks

由于我们使用 HTC vive 作为虚拟现实头盔显示器(HMD),首先下载 Steam*,然后访问以下链接安装 Blocks:
http://store.steampowered.com/app/533970/Blocks_by_Google/

首次启动 Blocks 时,您将获得一个指导您使用所有选件制作冰淇淋甜筒的教程:
https://www.youtube.com/watch?v=kTCcM5sRz74&feature=youtu.be

本教程很好地介绍了 Blocks 基本功能的使用方法。

  • 移动您的对象
  • 创建与缩放形状
  • 绘制对象
  • 选择与复制
  • 擦除

本教程为您提供了简单的接口,便于您随意使用更多工具与选件。以下是所有工具与选件的概述:
https://youtu.be/41IljbEcGzQ

Tools list

工具列表

  • 形状:圆锥体、球体、立方体、圆柱体、环面
  • 笔画:三面或更多的形状
  • 画笔:调色板背面的颜色选项、绘画对象或面
  • 手柄:选择、抓取、缩放、复制、翻转、分组/取消分组
  • 修改:修改形状、细分、拉伸
  • 擦除器:对象、面

调色板控制

  • 教程、网格、保存/发布、新场景、颜色

其他控制

  • 单个 Grip 按钮用于移动场景,两个按钮  用于缩放/旋转
  • 左触控板的左/右用于撤销/重做
  • 左触发器用于创建对称
  • 右触发器用于放置物体

文件(左控制器菜单按钮)

  • 您的模型、精选、已赞、环境

您还需要使用鼠标启动一个选项,以导入放置在场景中的参考图像(或图像)。这是一种从预生产草图过渡至原型设计的理想方式,无需依赖内存。单击台式机屏幕顶部中心的 Add reference image按钮,以导入图像。

To import an image

Google Poly

在我们进一步讨论如何在 Blocks 中建模之前,我们先了解一下 Poly,它是谷歌推出的一款在线“仓库”,包含大量已发布的模型:https://youtu.be/qnY4foiOEgc

Poly 汇集了来自 Tilt Brush 和 Blocks 的已发布作品,但是您可以选择侧边栏的 Blocks,以专门浏览任意一个来源。抽时间进行浏览与搜索,看看您能使用各种形状和颜色制造哪些物体,无需任何纹理。浏览时,请记得单击 Like,以便您在 Blocks 中轻松找到它们。

Like inside Block

请务必注意哪些模型要求获得共享或编辑许可。您只能对标记为可合成的模型进行修改或发布修改。目前,任何标记为可合成的模型均要求您注明原作者,因此,如果您将可合成的模型用作实际项目中某对象的基础,请务必注明原作者。

Remix content

现在,我们看一下可作为构建内容的基础使用的特定 Blocks 模型:https://poly.google.com/view/bvLXsDt9mww

Render

点赞后,为了从 Blocks 中轻松加载模型,单击左控制器上的 menu按钮,然后单击 heart选项,便可查看您点赞过的模型。然后选择房屋,并按下 Insert

Blocks

房屋随即就会像透视模型一样被放大。如欲继续放大,请用抓取工具(手柄)抓取房屋,然后按住右触控板上的 Up(+)不放,以进行放大。一旦大小符合了您的要求,您可以使用控制器手柄移动与旋转房屋。

由于模型的复杂性,渲染性能可能受计算机的影响,但是这向您展示了复杂的场景是如何构建的,并且支持您根据自身的需求进行修改。一旦我们开始使用 Unity 软件,将使用我修改过的预制件版本,以降低复杂性,添加碰撞盒与节省时间。现在,我们来设置实施了虚拟现实的 Unity 软件,这样我们便有了放置创建对象的空间。

Unity* 软件项目设置

导入插件

首先创建一个新项目(将它保存在您喜欢的位置)。接下来需要导入 SteamVR* 插件,以支持 vive:https://assetstore.unity.com/search?q=steamvr

SteamVR

您可能需要根据它的建议调整您的项目设置。首次操作需单击 Accept all。在此之后,它可能会再次询问您,除非您已修改了项目设置。稍后直接关闭该对话。

接下来,我们将获取 VRTK,它是一款能够轻松进行移动与对象交互的卓越工具套件:https://assetstore.unity.com/search/?q=vrtk

本教程不会提供关于如何使用 VRTK 插件的详细信息,但是我们将使用一个示例场景来进行某些操作,如通过触控板来控制移动。VRTK 插件非常适合将快速交互应用于您在 Blocks 中创建的对象。有关 VRTK 使用方法的更多信息,请观看本视频:

VRTK virtual reality toolkit

我们会将示例场景“017_CameraRig_TouchpadWalking”用作快速启动场景,因此,您需要加载该场景并删除除以下部分之外的所有内容:

  • 平行光
  • 地面
  • [VRTK_SDKManager]
  • [VRTK_Scripts]

Sample scene

接下来,扩大地面的 X/Z,您将拥有更大的空间来移动和放置物品。

scale the X/Z

导入预制件

抓取我们之前展示的房屋模型预制件,并将其导入您的场景:https://www.dropbox.com/s/87l8k23pc3h4a40/house.unitypackage?dl=0

请确保房屋移动后与地面齐平:

move the house

然后,您应该能够运行项目,戴上您的 HMD 并使用触控笔在场景内移动。如果通过触控板控制移动引起了您的不适,您也可以将该项目导入使用不同运动形式的其他示例场景(所有示例场景均被标记),如远距传动。

如果您想跳过这些设置步骤,直接导入房屋,可以从此处下载经过完备设置的 Unity 软件项目:https://www.dropbox.com/s/cehr2wxhi6nmh6c/blocks.zip?dl=0

目前,在碰撞之外,您经过场景时不能与任何物体交互。如欲查看来自 Poly 的原始 FBX,打开 Test Objects文件夹中的 model 1。现在,您拥有了一小片虚拟现实场景,可以开始添加对象了。

构建对象

现在场景已设置完成,我们需要决定为它添加何种原型。根据我们想要添加的对象,分为两种方法:从零创建或从 Poly 合成。出于演示目的,我们将使用两种方法。

假设我们想把房屋转变为烹饪和培训模拟器,需要更多的互动道具,它的形状要比基元提供的形状略微复杂。首先,我们将两个组件进行简单的合成,以创建一个带盖子的煎锅。

合成对象

我们将以下两个可合成模型合成为一个,请记得对它们点赞:

我们开始合成:https://youtu.be/SbjSs_rcFbk

您可能会问,为什么不把两个对象放入 Unity 软件,直接在软件上合成对象?这突出了从 Poly 获取的两个下载选项- OBJ 和 FBX -的重要性。如果您下载了 OBJ,整个模型是一个网格;如果您下载了 FBX,模型是所有单个形状块组成的集合。

这改变了 Unity 软件中的模型使用方式。在打开网格碰撞器以及设置对象以通过 VRTK 交互时,将整个网格用作单个对象非常有用。我们合成的两个模型仅用作 OBJ 文件,因此,我们不能在 Unity 软件中修改单个部件。但是,我们的新模型可用作两种文件(有时 OBJ 在下载时显示需要一些时间)。

现在,我们来下载 FBX,使用拖放功能将其导入 Unity 软件(和其他文件的导入方式一样),然后将对象拖入场景。

测试场景

将对象放置于场景后,单击 play直接进入场景以检查比例。如果比例不正确,选择整个模型组进行调整。现在,您拥有了一个简单的对象,可用于借助 VRTK 的交互原型设计,相比使用若干个未经修改的基元,使用该对象更加有效。您也获得了一个完善模型或添加纹理等额外细节的绝佳起点。

更妙的是,您可以首先将房屋对象插入 Blocks 中,对锅(或其他任何配件)进行建模。同时参考整座房屋,然后在保存与发布前删除单个房屋对象,甚至无需进入 Unity 软件。

创建模型

现在,我们看一下如何从零创建一个快速、简单的模型,如奶酪刨丝器。该模型不是一个简单的基元,但是也不是过于复杂。我们将构建一块奶酪,当您切开它时,会露出酥脆的边缘。您会发现我使用了右侧的 menu按钮对对象进行分组和取消分组,以简化操作。我还在两个边和面中使用了 modify工具。观看视频:

由于该模型可以作为 FBX 导入 Unity 软件,您可以以单个导入的形式轻松实现奶酪和刨丝器的分离,以进行各种交互。

如果相比 Unity 软件,您更倾向于使用 Unreal Engine,首先参阅该引擎的 FBX 工作流:https://docs.unrealengine.com/latest/INT/Engine/Content/FBX/index.html

使用建模程序

导入 blender

如果您拥有了 FBX 或 OBJ 文件,您或建模师想在完整的建模软件包中处理,您可以将它们导入免费的程序,如 Blender。

Blender

Blender

Blender

您可以在此处编辑模型、制作动画或执行其他操作。

面向 Poly 导出

如果您希望能够再次使用 Poly 共享模型,可以将其导出为 OBJ(包括 MTL 材料文件)。

model using Poly

接下来,单击 Poly 网站上的 upload OBJ按钮。

button on the Poly site

最后,将 .obj 和 .mtl 文件拖放到页面,然后单击 Publish发布模型。

drag and drop the .obj

但是缺点在于模型为单一网格 OBJ,您无法在 Blocks 中使用它进行合成或查看,它主要用于快速共享完整模型。但是建模师可以借助它出色地向您展示正在进行中的任务(还支持您下载 OBJ,以便在 Unity 软件中执行测试)。请记住,通过此方式上传的文件不会在 Blocks 中显示,即使您点赞了它们。因此,注意您点赞的对象是否标有“上传的 OBJ 文件”,如果有的话将无法在 Blocks 中使用。

回顾

我们回顾一下本文的主要内容。

  • 拥有预生产参考图像后,将它们导入 Blocks。
  • 在 Blocks 中快速“绘制”可用模型。
    • 在合适的时候,将可合成模型作为起点。
    • 你可以导入其他模型,以供参考或缩放。
  • 向 Poly 发布您的模型(您可以选择不公开模型)。
  • 根据您的需要,下载 OBJ 或 FBX。
  • 将模型导入 Unity 软件进行原型设计。
  • 与建模师分享 Poly 页面,以便他们修改 OBJ 和 FBX 或将其用作参考,为建模师提供 Unity 软件场景的预生产草图,甚至屏幕截图(或可玩版本),然后开始在 Blender 等工具中开发最终素材。
  • 建模师也可以使用 Poly 为您提供可下载和插入场景中的快速预览。
  • 改善与重复,以快速构建您的商业或游戏项目!

未来,Google Blocks 可能会集成动画(请参阅本文:https://vrscout.com/news/animating-vr-characters-google-blocks/),以使您的工作流更加出众,让我们拭目以待!

代码示例:使用 Direct3D* 12 进行并行处理

$
0
0

文件:

下载
许可:英特尔示例源代码许可协议
优化了…… 
操作系统:Microsoft* Windows® 10(64 位)
硬件:需要 GPU
软件:
(编程语言、工具、IDE、框架)
Microsoft Visual Studio* 2017、Direct3D* 12、C++
前提条件:熟悉 Visual Studio、Direct3D API、3D 图形、并行处理。
教程:使用 DirectX 3D* 12 进行并行处理

简介

本项目的理念是展示如何使用 Direct3D 12 在游戏中进行并行处理。本项目扩展了《使用 OpenGL* 和 Oculus* VR 中的可视化对比英特尔® 酷睿™ i5 处理器和英特尔® 酷睿™ i7 处理器》文章(参见参考部分)的结果和代码,以添加 Direct3D 12 渲染器。它还将之前的粒子系统重新实施为 Direct3D 12 计算着色器。

  1. 修改代码,以添加 CPU Direct3D 12 渲染器
  2. 迁移至 GPU
  3. 深入了解 CPU 和 GPU 的区别

入门指南

修改代码,以添加 CPU Direct3D* 12 渲染器

首先需要将 Direct 3D 12 “渲染器”添加至英特尔酷睿 i5 对比英特尔酷睿 i7 文章中使用的粒子系统。由于软件设计包含了渲染概念,因此可以轻松完成这项操作。第一步是定义连接渲染器的接口,然后编写一个事件循环。为了提升性能,我编写了一个自定义上传堆。接下来,查看计算着色器和实际的 Direct3D 12 渲染代码,然后讨论关于顶点缓冲区视图的问题。

迁移至 GPU

我们可以通过将渲染器从 CPU 迁移至 GPU 来提升性能。相比在各个线程之间分解处理,最好是在多个处理器:CPU 和 GPU 之间分解。

我努力确保 CPU 部分每个结构的每个字段的正确性与一致性,然后首先考虑到 GPU 计算问题可能使任务量增加两到三倍。我当即决定查找某种“助手”框架才是正确的选择,因此,我选择了 MiniEngine:DirectX 12 引擎入门套件。我将介绍我如何在本项目中安装与自定义 MiniEngine。借助 MiniEngine,之前需要使用 CPU 渲染的 500 多行代码减少为面向 GPU 的约 38 行设置代码和 31 行渲染代码(总共 69 行),我的努力终于有了回报。

GPU 渲染器包含设置和渲染代码。设置包含配置根签名、顶点输入和获取用于颜色和深度的格式。最后,我配置了图形 PSO 和视图与生产矩阵。渲染代码分为获取环境、描述过渡、使用新值更新矩阵之前清除颜色和深度以及绘制帧。

深入了解 CPU 和 GPU 的区别

为了实现最佳性能,我使用了两个粒子数据缓冲区,对其中一个进行了渲染,另一个则使用 GPU 计算更新为下一帧。我简要介绍了这一点,然后深入研究了在 GPU 上实施粒子渲染系统所需的变动,尤其是算法的区别。

参考资料

John Stone,Integrated Computing Solutions 公司,《使用 OpenGL* 和 Oculus* VR 中的可视化对比英特尔® 酷睿™ i5 处理器和英特尔® 酷睿™ i7 处理器》https://software.intel.com/zh-cn/articles/compare-intel-core-i5-and-i7-processors-using-custom-visualization-and-vr-benchmark,2017 年。

John Stone,Integrated Computing Solutions 公司,《使用 Direct3D 12 进行并行处理》, https://software.intel.com/zh-cn/articles/parallel-processing-with-directx-3d-12,2017 年

更新日志

创建于 2018 年 3 月 20 日

Unreal Engine 4 并行处理鱼群

$
0
0

Nikolay Lazarev

Integrated Computer Solutions, Inc.

关于群集算法的一般性描述

实施群集算法模拟鱼群的行为。该算法包含四种基本行为:

  • 聚集:鱼搜寻定义为聚集半径范围内的同伴。对所有同伴的当前位置进行求和,求和结果除以同伴数量,得到同伴的质量中心,这是鱼努力聚集的点。求和的结果减去鱼的当前位置,得出的矢量结果执行标准化,从而确定鱼的游动方向。
  • 分离:鱼搜寻定义为分离半径范围内的同伴。如需计算处于某一鱼群特定分离方向的鱼的游动矢量,对同伴位置和鱼自身位置的差值进行求和。结果除以同伴数量,然后执行标准化并乘以 -1,从而改变鱼的初始方向,使其游向同伴的相对位置。
  • 对齐:鱼搜寻定义为对齐半径范围内的同伴。对所有同伴的当前速度进行求和,然后除以同伴数量,对得出的矢量执行标准化。
  • 反转:所有的鱼只能在边界明确的特定空间内游动。必须识别出鱼跨越边界的时刻。如果某条鱼碰到边界,它的方向将变成相对矢量(从而确保鱼在指定的空间内游动)。

结合使用关于鱼群中每条鱼的这四种基本行为准则,计算出每条鱼的总位置值、游速和加速度。拟定算法引入了权重系数概念,以增加或降低这三种行为模式(聚集、分离和对齐)所产生的影响。鱼的反转行为中不使用权重系数,因为鱼不许游出指定的边界。因此,反转的优先级最高。而且,为该算法提供了最大速度和加速度。

根据上述算法,计算每条鱼的参数(位置、游速和加速度)。为每一帧计算这些参数。

群集算法的源代码及注释

为了计算鱼群中鱼的状态,使用双缓冲。鱼的状态保存在大小为 N x 2 的阵列中,其中 N 代表鱼的数量,2 代表状态的副本数量。

使用两个嵌套循环实施该算法。在内部嵌套循环中,计算三种行为(聚集、分离和对齐)的方向矢量。在外部嵌套循环中,根据内部嵌套循环的计算结果计算鱼的新状态。这些计算同样以每种行为的权重系数值以及游速和加速度的最大值为基础。

内部循环:在每次循环迭代中,计算每条鱼的新位置值。作为 lambda 函数的参数,引用传递至:

agents鱼状态阵列
currentStatesIndex保存每条鱼当前状态的阵列索引
previousStatesIndex保存每条鱼之前状态的阵列索引
kCohfor 聚集 行为的权重因子
kSepfor 分离 行为的权重因子
kAlignfor 对齐 行为的权重因子
rCohesion为了聚集而搜寻同伴的半径
rSeparation为了分离而搜寻同伴的半径
rAlignment为了对齐而搜寻同伴的半径
maxAccel鱼的最大加速度
maxVel鱼的最大游速
mapSz允许鱼游动的区域的边界
DeltaTime距离上次计算所花费的时间
isSingleThread指示循环以哪种模式运行的参数

ParllelFor 可用于两种模式,取决于 isSingleThread Boolean 变量的状态:

     ParallelFor(cnt, [&agents, currentStatesIndex, previousStatesIndex, kCoh, kSep, kAlign, rCohesion, rSeparation,
            rAlignment, maxAccel, maxVel, mapSz, DeltaTime, isSingleThread](int32 fishNum) {

初始化包含零矢量的方向,分别计算三种行为:

     FVector cohesion(FVector::ZeroVector), separation(FVector::ZeroVector), alignment(FVector::ZeroVector);

为每种行为初始化同伴计数器:

     int32 cohesionCnt = 0, separationCnt = 0, alignmentCnt = 0;

内部嵌套循环。计算三种行为的方向矢量:

     for (int i = 0; i < cnt; i++) {

每条鱼应忽略(不计算)自己:

     if (i != fishNum) {

计算当前鱼的位置与阵列中其他鱼的位置之间的距离:

     float distance = FVector::Distance(agents[i][previousStatesIndex].position, agents[fishNum][previousStatesIndex].position);

如果距离小于聚集半径:

     if (distance < rCohesion) {

将同伴位置添加至聚集矢量:

     cohesion += agents[i][previousStatesIndex].position;

增加同伴计数器的值:

     cohesionCnt++;
     }

如果距离小于分离半径:

     if (distance < rSeparation) {

将同伴与当前鱼之间位置的差值添加至分离矢量:

     separation += agents[i][previousStatesIndex].position - agents[fishNum][previousStatesIndex].position;

增加同伴计数器的值:

     separationCnt++;
     }

如果距离小于对齐半径:

     if (distance < rAlignment) {

将同伴的游速添加至对齐矢量:

     alignment += agents[i][previousStatesIndex].velocity;

增加同伴计数器的值:

     alignmentCnt++;
                      }
             }

如果发现聚集的同伴:

     if (cohesionCnt != 0) {

聚集矢量除以同伴数量,并减去自己的位置:

     cohesion /= cohesionCnt;
     cohesion -= agents[fishNum][previousStatesIndex].position;

聚集矢量执行标准化:

     cohesion.Normalize();
     }

如果发现分离的同伴:

     if (separationCnt != 0) {

分离矢量除以同伴数,并乘以 -1 以改变方向:

            separation /= separationCnt;
            separation *= -1.f;

分离矢量执行标准化:

              separation.Normalize();
     }

如果发现对齐的同伴:

     if (alignmentCnt != 0) {

对齐矢量除以同伴数:

            alignment /= alignmentCnt;

对齐矢量执行标准化:

            alignment.Normalize();
     }

根据每种可能行为的权重系数,确定新的加速矢量,不超过最大加速值:

agents[fishNum][currentStatesIndex].acceleration = (cohesion * kCoh + separation * kSep + alignment * kAlign).GetClampedToMaxSize(maxAccel);

沿 Z 轴限定加速矢量:

   agents[fishNum][currentStatesIndex].acceleration.Z = 0;

新游速矢量和自上次计算所花的时间相乘,结果与鱼之前的位置相加:

     agents[fishNum][currentStatesIndex].velocity += agents[fishNum][currentStatesIndex].acceleration * DeltaTime;

游速矢量不超过最大值:

     agents[fishNum][currentStatesIndex].velocity =
                 agents[fishNum][currentStatesIndex].velocity.GetClampedToMaxSize(maxVel);

新游速矢量和自上次计算以来所花的时间相乘,结果与鱼之前的位置相加:

     agents[fishNum][currentStatesIndex].position += agents[fishNum][currentStatesIndex].velocity * DeltaTime;

检查当前鱼是否在指定边界内。如果是,保存计算的游速和位置值。如果鱼沿某条轴游出了边界,将沿该轴的游速矢量值乘以 -1 以改变鱼的游动方向:

agents[fishNum][currentStatesIndex].velocity = checkMapRange(mapSz,
               agents[fishNum][currentStatesIndex].position, agents[fishNum][currentStatesIndex].velocity);
               }, isSingleThread);

对每条鱼来说,应用新状态之前,应检测鱼与真实静态物体(比如水下岩石)的碰撞:

     for (int i = 0; i < cnt; i++) {

检测鱼与静态物体之间的碰撞:

            FHitResult hit(ForceInit);
            if (collisionDetected(agents[i][previousStatesIndex].position, agents[i][currentStatesIndex].position, hit)) {

检测到碰撞后,撤销之前计算的位置。游速矢量应变成相对方向和计算的方向:

                   agents[i][currentStatesIndex].position -= agents[i]  [currentStatesIndex].velocity * DeltaTime;
                   agents[i][currentStatesIndex].velocity *= -1.0;
                   agents[i][currentStatesIndex].position += agents[i][currentStatesIndex].velocity * DeltaTime;
            }
     }

计算所有鱼的新状态后,运用这些更新的状态,所有鱼都将向新的方向游去:

for (int i = 0; i < cnt; i++) {
           FTransform transform;
            m_instancedStaticMeshComponent->GetInstanceTransform(agents[i][0]->instanceId, transform);

设置鱼实例的新位置:

     transform.SetLocation(agents[i][0]->position);

让鱼的头部转向移动方向:

     FVector direction = agents[i][0].velocity;
     direction.Normalize();
     transform.SetRotation(FRotationMatrix::MakeFromX(direction).Rotator().Add(0.f, -90.f, 0.f).Quaternion());

更新实例转换:

            m_instancedStaticMeshComponent->UpdateInstanceTransform(agents[i][0].instanceId, transform, false, false);
     }

重新绘制所有鱼:

     m_instancedStaticMeshComponent->ReleasePerInstanceRenderData();

     m_instancedStaticMeshComponent->MarkRenderStateDirty();

交换索引的鱼状态:

      swapFishStatesIndexes();

算法的复杂程度:如何提高鱼影响工作效率的值

假设参与算法的鱼的数量为 N。为了确定每条鱼的新状态,必须计算其他所有鱼的距离(不算上为了确定三种行为的方向矢量的其他操作)。该算法最初的复杂程度为 O(N2)。例如,1,000 条鱼要求 1,000,000 次操作。

Figure 1

图 1:计算某场景中所有鱼的位置的运算操作。

计算着色器及注释

描述每条鱼的状态的结构:

     struct TInfo{
              int instanceId;
              float3 position;
              float3 velocity;
              float3 acceleration;
     };

用于计算两个矢量之间的距离的函数:

     float getDistance(float3 v1, float3 v2) {
              return sqrt((v2[0]-v1[0])*(v2[0]-v1[0]) + (v2[1]-v1[1])*(v2[1]-v1[1]) + (v2[2]-v1[2])*(v2[2]-v1[2]));
     }

     RWStructuredBuffer<TInfo> data;

     [numthreads(1, 128, 1)]
     void VS_test(uint3 ThreadId :SV_DispatchThreadID)
     {

鱼的总数:

     int fishCount = constants.fishCount;

该变量用 C++ 语言创建和初始化,用于确定每个图形处理单元 (GPU) 线程所计算的鱼数量(默认为 1):

     int calculationsPerThread = constants.calculationsPerThread;

用于计算必须在该线程中计算的鱼状态的循环:

     for (int iteration = 0; iteration < calculationsPerThread; iteration++) {

线程标识符。对应状态阵列中的鱼索引:

     int currentThreadId = calculationsPerThread * ThreadId.y + iteration;

检查当前索引,确保其不超出鱼的总数(如果启动的线程数超过鱼数,可能会发生这种情况):

     if (currentThreadId >= fishCount)
            return;

如欲计算鱼的状态,可使用单个双长度阵列。阵列的第一个 N 要素为待计算的鱼的新状态;第二个 N 要素为之前计算的鱼的旧状态。

当前鱼索引:

    int currentId = fishCount + currentThreadId;

鱼的当前状态结构副本:

     TInfo currentState = data[currentThreadId + fishCount];

鱼的新状态结构副本:

     TInfo newState = data[currentThreadId];

初始化三种行为的方向矢量:

     float3 steerCohesion = {0.0f, 0.0f, 0.0f};
     float3 steerSeparation = {0.0f, 0.0f, 0.0f};
     float3 steerAlignment = {0.0f, 0.0f, 0.0f};

初始化每种行为的同伴计数器:

     float steerCohesionCnt = 0.0f;
     float steerSeparationCnt = 0.0f;
     float steerAlignmentCnt = 0.0f;

根据每条鱼的当前状态,分别计算三种行为的方向矢量。循环从输入阵列的中间开始,它位于保存旧状态的位置:

     for (int i = fishCount; i < 2 * fishCount; i++) {

每条鱼应忽略(不计算)自己:

     if (i != currentId) {

计算当前鱼的位置与阵列中其他鱼的位置之间的距离:

     float d = getDistance(data[i].position, currentState.position);

如果距离小于聚集半径:

     if (d < constants.radiusCohesion) {

将同伴位置添加至聚集矢量:

     steerCohesion += data[i].position;

增加聚集同伴的计数器:

            steerCohesionCnt++;
     }

如果距离小于分离半径:

     if (d < constants.radiusSeparation) {

同伴与当前鱼的位置差值与分离矢量相加:

     steerSeparation += data[i].position - currentState.position;

增加分离同伴计数器:

            steerSeparationCnt++;
     }

如果距离小于对齐半径:

     if (d < constants.radiusAlignment) {

将同伴的游速添加至对齐矢量:

     steerAlignment += data[i].velocity;

The counter of the number of neighbors for alignment increases:

                          steerAlignmentCnt++;
                   }
            }
     }

如果发现聚集的同伴:

   if (steerCohesionCnt != 0) {

聚集矢量除以同伴数量,并减去自己的位置:

     steerCohesion = (steerCohesion / steerCohesionCnt - currentState.position);

聚集矢量执行标准化:

            steerCohesion = normalize(steerCohesion);
     }

如果发现分离的同伴:

     if (steerSeparationCnt != 0) {

分离矢量除以同伴数,并乘以 -1 以改变方向:

     steerSeparation = -1.f * (steerSeparation / steerSeparationCnt);

分离矢量执行标准化:

            steerSeparation = normalize(steerSeparation);
     }

如果发现对齐的同伴:

     if (steerAlignmentCnt != 0) {

对齐矢量除以同伴数:

     steerAlignment /= steerAlignmentCnt;

对齐矢量执行标准化:

           steerAlignment = normalize(steerAlignment);
     }

根据三种可能行为的权重系数,确定新的加速矢量,不超过最大加速值:

     newState.acceleration = (steerCohesion * constants.kCohesion + steerSeparation * constants.kSeparation
            + steerAlignment * constants.kAlignment);
     newState.acceleration = clamp(newState.acceleration, -1.0f * constants.maxAcceleration,
            constants.maxAcceleration);

沿 Z 轴限定加速矢量:

     newState.acceleration[2] = 0.0f;

新加速矢量和自上次计算以来所花的时间相乘,结果与之前的游速矢量相加:

游速矢量不超过最大值:

     newState.velocity += newState.acceleration * variables.DeltaTime;
     newState.velocity = clamp(newState.velocity, -1.0f * constants.maxVelocity, constants.maxVelocity);

新游速矢量和自上次计算所花的时间相乘,结果与鱼之前的位置相加:

     newState.position += newState.velocity * variables.DeltaTime;

检查当前鱼是否在指定边界内。如果是,保存计算的游速和位置值。如果鱼沿某条轴游出了边界,将沿该轴的游速矢量值乘以 -1 以改变鱼的游动方向:

                   float3 newVelocity = newState.velocity;
                   if (newState.position[0] > constants.mapRangeX || newState.position[0] < -constants.mapRangeX) {
                          newVelocity[0] *= -1.f;
                   }

                   if (newState.position[1] > constants.mapRangeY || newState.position[1] < -constants.mapRangeY) {
                          newVelocity[1] *= -1.f;
                   }
                   if (newState.position[2] > constants.mapRangeZ || newState.position[2] < -3000.f) {
                          newVelocity[2] *= -1.f;
                   }
                   newState.velocity = newVelocity;

                   data[currentThreadId] = newState;
            }
     }         

表 1:算法比较。

算法 (FPS)

运算操作

CPU SINGLE

CPU MULTI

GPU MULTI

100

62

62

62

10000

500

62

62

62

250000

1000

62

62

62

1000000

1500

49

61

62

2250000

2000

28

55

62

4000000

2500

18

42

62

6250000

3000

14

30

62

9000000

3500

10

23

56

12250000

4000

8

20

53

16000000

4500

6

17

50

20250000

5000

5

14

47

25000000

5500

4

12

35

30250000

6000

3

10

31

36000000

6500

2

8

30

42250000

7000

2

7

29

49000000

7500

1

7

27

56250000

8000

1

6

24

64000000

8500

0

5

21

72250000

9000

0

5

20

81000000

9500

0

4

19

90250000

10000

0

3

18

100000000

10500

0

3

17

110250000

11000

0

2

15

121000000

11500

0

2

15

132250000

12000

0

1

14

144000000

13000

0

0

12

169000000

14000

0

0

11

196000000

15000

0

0

10

225000000

16000

0

0

9

256000000

17000

0

0

8

289000000

18000

0

0

3

324000000

19000

0

0

2

361000000

20000

0

0

1

400000000

Figure 2

图 2:算法比较。

笔记本电脑硬件
CPU – 智能英特尔®酷睿 i7-3632QM 处理器 2.2 Ghz,睿频加速可达 3.2 GHz
GPU - NVIDIA GeForce* GT 730M
RAM - 8 GB DDR3*

角色建模

$
0
0

modeled character

简介

角色建模指在计算机项目的 3D 空间中创建角色的过程。角色建模技巧对电影、动画、游戏和 VR 培训项目中的第三和第一人称体验至关重要。本文将介绍如何进行目标明确的设计、如何使设计成为模型就绪型设计,以及创建模型的流程。在后续的课程中,我将使用重新拓扑技巧继续完成模型。

characters within the 3D space

设计和图纸绘制

设计角色的第一步是了解角色在应用或场景中的用途和目标。例如,如果该角色为第一人称培训项目而创建,可能只需要构建浮动的双手模型。它可以指如何为培训应用设计角色。

此外,对电影、游戏和 VR 来说,角色的设计非常关键。设计必须符合场景需要,并且在视觉上展现其人物个性。如果眼睛大而宽,可能比较卡通和可爱。如果穿的袜子一高一低,可能比较古怪和紧张。让他们的设计讲述关于他们这一类人的故事。

下面我列举一个角色设计示例,我将在本文中为其建模。与此同时我还提供一个设计明细,解释设计如何影响大家对角色的感知。

简要设计明细

  • 圆形表示角色善意友好;你希望观众喜欢这个角色。
  • 大眼睛展现青春活力,角色非常可爱;也很有表现力。
  • 像螺旋桨帽子和条纹 T 恤等细节表明他很有趣幼稚。

模型就绪:静态与动画

design of the character model

设计完成后,最重要的是区分它是静态还是动画角色。这将决定您如何为自己的角色模型创建蓝图。这些蓝图称作正投影图。正投影图包括模型的正面图、侧面图和顶视图。您可以将这类图视作 2D 动画或概念艺术。不过,3D 角色模型的正投影图不同。下面我来介绍静态和动画正投影图的不同要求。

动画

动画模型必须合理设置以便绑定。如要绑定角色,以下要求必不可少:

  • 必须以 T 或 A 的姿势完成图纸
  • 他们的膝盖和手臂必须稍微弯曲
  • 手指和腿部必须张开
  • 他们必须毫无表情

如果跳过这些步骤,将难以获得便于绑定且活力十足的清晰结果。下面是我将要建模的角色的 T 姿势的正投影图示例。

T-pose drawings of the character

静态

静态模型,比如雕像或玩具人物,要保持相同的姿势。因此,不需要绑定。只需要以设计要求的姿势和表情进行建模。关于正投影图的唯一要求是图纸必须从所有的正投影图角度,表现出角色模型成品的姿势和表情。

static model

注意,对于动画和静态图,身体的侧面和正面图必须相应地排列起来。只有这样才能确保,当这些蓝图指导您进行建模过程时,模型的比例正确。接下来将每张正投影图保存为 .jpg 或 .png 文件。

orthographic view
正投影图

现在,您已准备好进入建模部分!由于头部建模往往难度更大,因此我决定用较大的篇幅来重点介绍头部建模。不过我相信您只要了解如何为头部建模,角色其他部分的创建将会非常简单。另外,相同的技巧也适用,我将继续通过分步流程和图片指导您完成建模。

建模

设置

正投影图纸完成后,可以将它们放入您选择的 3D 项目中。为此,您可以图像平面的形式打开它们。看下图,图像平面上的图纸一张张有序地排列着。这一步至关重要。有一点差异也无妨,但如果距离较远,图像平面会使角色模型的比例不协调。准备好平面后,现在我们正式开始建模。

image planes

开始前须知

角色建模的三要素分别为对称性、简洁性和自由切换。

  • 对称性:我们必须确保身体每一部分的对称性,以维持动画的相应功能。
  • 简洁性:不要从密集网格开始。从低多边形开始,这样您可以轻松地调整网格的形状。例如,在视频中我从由深、宽、高三个部分组成的立方体开始。
  • 自由切换:必须能够在网格磨平功能开关之间自由切换。通常,网格磨平后,凌乱的几何图形会变得非常整洁。 

我会在头部建模视频中完成这三个过程。观看视频可帮助您了解如何将这些技巧融入到工作流程之中。现在我们开始吧!

为头部建模

为头部建模时,我们要经历四个阶段。这些阶段适用于创建头部和身体其他部分的模型。

  1. 低多边形阶段:将低多边形原始对象(例如立方体)调整为待创建的身体某一部分的形状。
  2. 预规划阶段:增加多边形的数量,并继续调整网格的形状。
  3. 规划阶段:规划某一空间的细节,比如头部模型的面部特征。
  4. 润色阶段:相应地微调并增加拓扑结构,使其符合设计要求。

the four stages for modeling the head

阶段一和阶段二

首先,将低多边形立方体调整为角色头部的形状。您观看视频时会发现,我使用 Translate 工具为头部塑形,还使用 Insert Edgeloop 工具和 Smooth 按钮进一步增加头部细节。特定区域需要更多拓扑结构时,我喜欢使用插入边缘循环。另一方面,使用 Smooth 按钮可以帮助增加整个网格的拓扑结构,同时保持光滑的网格形状。

阶段三和阶段四

现在有了更多的拓扑结构,我们可以开始规划眼睛、鼻子和嘴巴。这里您要使用正投影图指导每一张图纸的放置。另外,您需要观看视频了解这一过程。从这里开始,接下来的步骤是:

  1. 规划/塑造面部特征网格的顶点。
  2. 进行挤压以创建出眼窝、嘴巴和鼻子的空间。
  3. 在不增加拓扑结构的情况下继续塑形
  4. 慢慢添加或挤压多边形。
  5. 尽可能使用雕刻工具或软选择来匹配网格和正投影图。
  6. 多次重复步骤三和步骤四,直到拓扑结构和图纸相符。

planning the eyes, nose, and mouth

在这个阶段,我喜欢使用 Edgeslide 工具,这样我在平移顶点时,头部形状不会改变。接下来进入润色阶段。面部特征润色完成后,您可以开始为眼睛建模。

为眼睛建模

接下来是制作与头部相适应的眼球以及眼窝。

具体过程如下所示:

  1. 制作球体。
  2. 移动并均匀缩放该球体,使其大体适合眼窝。
  3. 将该球体旋转 90 度,使极点位置朝外。
  4. 根据需要调整眼窝,使其位于眼球上。
  5. 调整虹膜、瞳孔和角膜的形状,如下所示。
  6. 选择新组,并在 x 轴上缩放组 1。

按照以下图像的指导进行操作。

modeling the eyes
图 1.均匀缩放,并将球体平移到眼窝内。
然后将球体旋转 90 度,使球体的极点朝外。
图 2.调整眼窝使其与眼睛相适应。

复制眼睛。我们不编辑的“眼睛”网格将作为角膜。

creating the iris and pupil
图 3.选择一个球体,如图所示选择边缘并缩放。
图 4.将边缘平移回角膜内。现在我们创建了虹膜。
5.选择内表面,并向内挤压以创建瞳孔。

为眼睛部分创建一个组。为该组重新命名,然后继续复制。

group and scale eye piece
图 6.选择新组并在 x 轴上缩放组 1。

现在头部模型只剩下眼皮、耳朵和脖子。不过我们在完成模型重新拓扑之后再开始做这些。至于头发和眉毛,我通常喜欢创建低多边形的简单形状。

修补错误

如果这是您第一次制作模型,整个过程中可能会遇到一些困难。下面我针对一些可能会遇到的问题提供相应的解决方法。

  • 对称工具无法正常运行。
    • 这样会出现不对称现象。请执行以下步骤解决问题。
    1. 检查并删除多余的顶点和面。
    2. 复制并隐藏或移动原来的网格。接下来需要删除一半复制的面,确保切割网格中间位置的顶点与对称轴保持一致,然后在对称轴上使用镜像工具。
    3. 删除对象的历史记录。
  • 网格不对称。
    • 如果忘记重新打开对称性,移动顶点时会出现这种情况。
    1. 复制并隐藏或移动原来的网格。接下来需要删除一半复制的面,确保切割网格中间位置的顶点与对称轴保持一致,然后在对称轴上使用镜像工具。
  • 角色的眼睛与眼窝和头部不相适应。
    • 如果眼球为椭圆形球体或球体距离较远,可能会发生这种情况。对于这些情况,您可能需要在几何体上使用晶格变形工具。生成纹理贴图动画也可以解决问题。
  • 对网格进行分组和镜像时,无法完成镜像。

手臂和边缘循环放置

接下来,我来制作几何体中另一个复杂部分:手臂。介绍制作过程之前,必须了解边缘循环放置的重要性。边缘循环不仅可以让您添加拓扑结构,还可在安装网格时让网格弯曲。关节、肘部、肩膀和膝盖等节点处至少需要三个边缘循环。

另外,是否还记得绘制角色手臂图纸时要保持双臂稍微弯曲?您需要为这种弯曲建模。安装角色时,节点要沿弯曲位置放置,这样您可以知道 IK 节点是如何弯曲的。但如果节点放在直线上,关节可能向后弯曲,从而导致角色手臂或腿部断裂。

为手臂建模

在为手臂建模的过程中,从手指开始依次向后。我发现这样做可以使端部网格更整洁。按照这个顺序,我将过程简化为四个阶段:

  1. 手指阶段:为所有手指和拇指建模。
  2. 手掌阶段:为手掌建模。
  3. 连接阶段:将手指和拇指与手部连接起来。
  4. 手臂阶段:挤压和手臂塑形。

现在您对我们的目标有了基本的了解,下面用图片展示整个建模过程的详细步骤。

阶段一

add edge loops to model fingers

图 1.制作用于手指建模的低多边形立方体,切换视图以便该立方体与图纸相符。

add edge loops to model fingers

图 2.在关节处添加边缘循环并进行润色。

duplicate and adjust mesh across fingers

图 3.复制、调整和平移手指模型以创建其他手指。

model the thumb

图 4.通过低多边形立方体为拇指建模。对拇指进行润色。切换视图检查它们的位置;然后将手指和拇指合并为一个网格。

阶段二

steps to create and shape the palm
图 6.创建包含相应数量的细分部分的立方体,以连接手指。
图 7.删除其他所有边缘循环(为简单起见)并塑造手掌形状。

注:用这种方法可以轻松地在较低的细分区域推动手掌的形状,并确保在增加拓扑结构时有足够的几何体来连接手指。

阶段三

steps to shape the palm
图 8.添加回手掌的拓扑结构。
图 9.合并手掌和手指网格。
图 10.连接手指。

注:我更喜欢使用 Target Weld 工具连接手掌和手指。

usage of Target Weld tool
图 11.整理几何图形。

阶段四

extrude the arm
图 12.挤压手臂。

extrude the arm
图 13.添加边缘循环,并确保网格是中空的。

手臂制作完成后,我们可以像处理眼球一样复制手臂。我们来回顾一下步骤:

  1. 复制并创建手臂组。
  2. 在 x 轴上缩放组 1,并取消手臂组。

为身体建模

到这里,您已经学到了大部分完成角色建模所需的技巧!身体其他部位的建模步骤与头部和手臂建模类似。如果您观看视频,按照这些步骤,将能很好地完成建模。

  1. 问问自己:“哪种原始网格最适合?
    • 例如,圆柱体适用于裤腿,而立方体适用于鞋子。
  2. 制定一个大致的计划。
    • 例如:“我要用圆柱体创建裤腿,先创建左裤腿,然后使用镜像工具完成整个模型。”
  3. 移动、缩放和编辑低多边形原始网格,使其与正投影图相符。
  4. 慢慢添加或挤压多边形。
  5. 尽可能使用雕刻工具或软选择来匹配网格和正投影图。
  6. 多次重复步骤 5 和 6,直到拓扑结构和图纸完全相符。
  7. 如有必要,对模型进行镜像处理!

以下是我提供的有关身体其他部位的图片示例。

shirt
衬衫

shorts
短裤

legs

shoes

非常好!您的模型已经制作完成!不过,继续下一阶段之前,您需要仔细检查这些部位,确保您已准备好进入重新拓扑阶段。

  • 模型是否对称?
  • 膝盖和手臂是否弯曲?(仅适用于角色要被绑定的情况)
  • 角色的所有部位是否都已建模?
  • 角色与图纸是否相符?

如果这些问题不存在任何疑问,那么您已经准备好阅读后续文章了解角色重新拓扑。

资源

感谢您阅读此篇文文章并观看其中的视频。鉴于最好通过多个渠道广泛学习,这里我提供一些资源供您参考学习,这些资源为我和同事的答疑解惑之旅提供了重要帮助。下面列出其中一部分。

有关 3D 的实用 YouTube* 频道:

  • Maya* 学习频道
  • Pixologic 学习频道
  • Blender* Guru
  • Blender
  • James Taylor (MethodJTV*)

其他角色建模资源:

  • Linda.com*
  • Pluralsight*
  • AnimSchool*

其他绑定资源:

  • Rapid Rig* 和 Mixamo*(自动绑定)
  • Pluralsight*
  • AnimSchool

数字绘画:快节奏绘画流程

$
0
0

splash art for Final Star Dynasties - Space court
图 1.Star Dynasties* splash art 定稿。

在概念艺术中,将抽象概念转化为具体视觉效果,这之间所花的时间越短越好。它适用于所有涉及概念艺术的行业,但在视频游戏行业的快节奏开发流程方面,速度尤为重要。这里我将通过我为独立游戏 Star Dynasties*制作的数字插图,简要介绍概念化、参考收集、草图绘制和定稿等步骤,其中重点介绍效率和速度。

随附的视频介绍了关于如何使用 3D 元素、照片纹理和 Photoshop* 工具的几种具体方法。每位美术师都有一套符合自己的风格和特定项目需求的技巧。我希望通过本文为大家提供一些想法,为大家的工具包添砖加瓦。

设计流程

第 1 步:规划和概念化

创建 Star Dynasties插画的第一步是明确图像的使用目的。图像需要实现哪些目标?这里,它是游戏的主要 splash art,可能是潜在玩家第一眼看到的效果。因此它代表着游戏的基调和类型,从而吸引对这类游戏感兴趣的玩家。基调和类型可以通过颜色、框架/构图、主题和风格来确立。本质上来说就是通过图像来讲述故事,这就是大多数概念艺术和插图的目的所在。

Star Dynasties创建者 Glen Pawley 表示,这款游戏是“以未来暗黑时代为背景的角色模拟游戏,你可以扮演跨越多个朝代的空间殖民地的集团首领。目标是让自己的领地幸存下来,并不断发展壮大。”它是类似中世纪政治和个人关系(而非军事策略)的战略游戏,因此它所需要的封面艺术与第一人称射击游戏完全不同。确定图像的使用目的后,可以开始收集一些参考资料。

在项目开始甚至是绘制草图之前,收集强大的参考资料库非常有用。它可帮助项目参与者,包括美术师、客户、艺术团队和艺术总监,相互之间建立理解。参考可以用作注意事项指南,节省大量时间和精力。从同类游戏中收集参考资料可以帮助拓宽游戏创意的视野,或者避免出现已经完成(或过多)的视觉效果。下笔之前,关于游戏基调、风格和内容的资料越详细,美术师、客户、艺术总监等项目参与者之间的重复工作就越少。

有时收集少量图像就足够了,但如果是涉及多位美术师的团队项目,可能需要制定更为详细和周密的风格指南。然而,更完整、更全面的风格指南通常只有在项目主要艺术创作完成之后才能制定出来。参考除了有利于初步的创意构思之外,在整个数字绘画过程中都是非常实用的,尤其是对于写实或半写实风格。使用工具,比如 Handy 艺术参考工具,进行网络搜索可以轻松获取参考资料,或者朝着镜子中的你做鬼脸。就是以最快的方式收集到最佳的结果,因此应该充分利用能够轻松获得的参考资料。

第 2 步:构图和缩略图

确定一些初步的创意、目标和参考资料之后,接下来就是缩略草图。选择图像定稿方向之前,有时一张重要的插图(比如封面)有数十张缩略图和粗略图。这时,真正有用的只有少数几张,因为基本方向是事先已经确定的:前景是统治者或王室,背景是空间殖民地,吸引观众想象自己的人物角色。

Final Star Dynasties grayscale rough sketches
图 2.Star Dynasties* 粗略图。

这些最初的粗略图并不反映行动或暴力,而是展示或暗示以下关键要素:

  • 游戏的多代家族王朝视角。
  • 太空殖民地的工业风格,最初是为了收集资源而建立,但现在用作永久栖息地。
  • 军用船舶,通过改装后用作货船或勘探船。
  • 提示人际关系,右边的人物可能参与了朝廷阴谋。
  • 盾徽、守卫的矛型武器、最中间的人物所穿的皇室式样着装(更不用说之后添加的金色和蓝色等皇室专用颜色)均凸显了中世纪主题。
  • 低角度、中心位置,以及调查殖民地/船舶的姿态,确定了主角的权威性。

大家可能会注意到,这些草图使用了 3D 元素。如果只需绘制粗略图,在缩略图过程中使用 3D(本示例中使用的是 SketchUp*)可以增加更多时间。不过,这些粗略图中的基本宇宙飞船模型为之前的 Star Dynasties插图制作过,因此相比重新绘制,找到这些模型并截图速度更快。在缩略图过程中使用 3D 的另外一个原因是,它有助于更有条理地了解视角和构图成选项。

在 3D 空间中缩放和平移可以创造新的机会来形成有趣的构图,即使是房间、大楼或其他物体的基本粗略版也可如此。例如,城堡可以像一堆 3D 盒子和圆柱体那样简单,而且仍然可以用来探索其他可能的构图。找到最佳构图是缩略图过程的一个重要部分。最佳构图不仅可以创建引人注目的图像,还能帮助传达故事、创意和基调。

在上图 2 中的粗略图中,我们选择第 1 张图(座上)为最终的插图。其他的粗略图存在以下一些问题:

  • 第 2 张图:窗户妨碍眼睛自由地审视这张图片。其他图片中是皇室成员俯视自己的殖民地王国,而这张是让他们抬头看,削弱了他们的权力感。
  • 第 3 张挺好,但右上角区域在视觉方面的吸引力不够。
  • 第 4 张过于对称,导致图片乏味无聊。它过于居中,而且宇宙飞船的大小和形状太接近。在构图中,大小和形状必须更加多样化。

以下是第 1 张图在构图方面的优势:对称性首先将视线吸引到中间的人物,然后是他们所注视的对象。飞船呈对角线移动,并与横幅垂直,有助于将视线吸引到图片的其他位置,落在次要的兴趣点上,比如左边的守卫、右边正在说话(或密谋)的贵族,以及介绍游戏环境的背景要素。

第 3 步:详细的插图

该流程的最后一部分是为图片增加所需的细节。有些概念可以是粗略的草图或速写画,但作为插图来说,必须对图片进行润色。首先使用 SketchUp 通过缩略图充实部分 3D 场景。有些概念美术师几乎完全使用 3D;而有些什么都不用。一般来说,在 30 分钟的时间里,使用几支 Photoshop 笔刷创造出的场景或角色比在 3D 项目中更加真实。但如果要求高细节级别和/或真实性,例如电影色调的艺术,3D 渲染器非常有用,有时甚至是不可或缺的基本工具。在这张插图中,我们主要使用 SketchUp 确定视角,并作为数字绘画的指南。

有时我通过一些 Photoshop 着色技巧(视频中有介绍)直接在 3D 渲染器上面绘制。但这通常涉及 3D 美术师的模型成品,而非这些我自己粗略绘制的盒状 SketchUp 创意。SketchUp 并不是最高级的 3D 工具,它主要用于人造形状,而不是有机形状。但它是免费的(专业版相对比较便宜),也比较直观,因此在 3D 方面没有经验,和/或不使用 3D 证明其他项目更昂贵的美术师比较喜欢使用这种工具。

3D scene in SketchUp and final splash
图 3.SketchUp* 中的 3D 场景,以及 Star Dynasties* splash art 定稿。

根据新截图充实 3D 场景和更新缩略图之后,接下来是增色。让人物、支柱、地板、建筑物、飞船和行星等主要元素在不同的层次更有帮助。Photoshop 有多种为灰度图像着色的方法。我在插图中使用的工具包括渐变贴图、色彩和覆盖层,以及 Blend If 特性,这些在视频中都有介绍。渐变贴图特别适合在区域的阴影部分施加不饱和的冷色调色彩,而在高亮部分施加更鲜艳明亮的色彩。粗略图初始着色后继续增加细节和进行绘制,但渐变贴图可以在一开始节约大量时间和精力。

确定色彩之后,就是应用所有的细节、姿势、照明、表情等等。头部和手部参考应用 Handy 艺术参考工具特别有用,因为它包含简单易用的定位和照明工具。在本插图中,岩石景观、布艺横幅以及金属支柱都包含照片纹理,既可以用作覆盖层,也可以用作基础层。飞船上的一些细节是自定义形状。查看 Long Pham 的 Gumroad*,了解工业和科幻自定义形状包。对于可见的脸部,我使用 Handy art 参考工具帮助获得自己想要的角度和光亮。最右边人物的手其实我的手的照片,我只是在上面进行了一些色彩调整和绘制。

这就是这张插图背后的整个创作流程。快速处理涉及一些工具和捷径的使用,但每一步所花的时间同样重要,尤其是与客户或团队合作的时候。收集参考资料、寻找最佳构图、设计和风格等前期步骤可以大大减少后续耗时的修订工作。如果你有兴趣练习快速绘图,可以加入 Daily Spitpaint等 Facebook 小组,不仅可以练习,还能了解如何在紧迫的时间里克服数字绘画方面的其他挑战。

在Windows上的Caffe实战:fine-tune猫狗大战

$
0
0

作者:钱彩红

文档目的

上一篇文章介绍了在Windows上如何利用Caffe对自己的图片进行训练,在实际训练中我们常常会发现由于样本数量太少,导致训练结果差,而且中间可能会消耗大量的时间去进行调参但效果却仍不尽如人意。本文仍以猫狗大战为例,介绍如何使用前人训练好的网络和模型,在自己的数据集上进行fine-tuning(微调),以达到快速取得较好的训练结果的目的。

环境介绍

本文所述的工具和命令适用Windows+BVLC Caffe的CPU或GPU版本 (需要提前在机器上安装BVLC Caffe并成功编译); 以及Windows+ clCaffe的版本, 但clCaffe是基于Intel Skylake及以后的处理器核显做硬件加速的修改版,使用时要注意。

准备的资料

  1. 准备自己的图片数据。这里仍然使用kaggle的dogsvscats(猫狗大战)的图片,下载地址:https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data
  2. 下载预训练好的模型文件包

BVLC提供的Model Zoo里有很多训练好的经典模型,这里我们选择使用imagenet的一个1000分类模型,这是caffe团队用imagenet图片进行训练,迭代30多万次,训练出来的一个model,这个model将图片分为1000类。模型的下载地址:http://dl.caffe.berkeleyvision.org/bvlc_reference_caffenet.caffemodel

将最后下载到的内容都解压放在自己的目录下的\bvlc子目录下:

可以看到里面的内容非常全面,有solver文件,deploy文件,caffemodel文件以及其他一些需要的文件,这都是我们接下来需要用到的。

数据集预处理

在准备好了自己的图片数据集,并且下载了预训练的模型相关文件后,我们就可以开始fine- tune了。首先需要将自己的图片数据分成train和val的数据集,并且转换成LMDB格式,并生成均值文件。因为我们fine-tuning 需要基于我们自己数据的LMDB和均值文件来进行。关于数据处理的详细步骤仍请参考这篇文章

这是我们处理后得到的LMDB和均值文件:

修改预训练模型参数

接下来就是将下载下来imagenet的模型参数文件进行修改。

修改solver文件(\bvlc\solver.prototxt)

  1. 修改网络文件的路径和文件名
    net:后面改为实际使用的网络文件的路径和名称
  2. 修改test_iter
    原来的test_iter为1000,因为我们目前的测试数据比较少,总共只有5000个图片,batch_size是50,所以我们将这个值改为100
  3. 将base_lr从0.01降为0.001。微调时的base_lr不要太大
  4. max_iter也改小一点,因为我们没有那么多数据,我们先改为10000看看效果
  5. stepsize改小一点,我们改为5000。一方面是max_iter现在是10000,stepsize比它大就没有意义了。另一方面是因为我们在实际训练中希望学习率下降的快一点,所以在达到stepsize次iteration达到后learning rate可以变得更小
  6. display改成100,每100次迭代打印一次
  7. snapshot_prefix改成我们需要的路径和名字
  8. 其他参数不变

以下是修改前后的solver文件对比

修改前:

#imagenet
net: "models/bvlc_reference_caffenet/train_val.prototxt"
test_iter: 1000
test_interval: 1000
base_lr: 0.01
lr_policy: "step"
gamma: 0.1
stepsize: 100000
display: 20
max_iter: 450000
momentum: 0.9
weight_decay: 0.0005
snapshot: 10000
snapshot_prefix: "models/bvlc_reference_caffenet/caffenet_train"
solver_mode: GPU

修改后:

#dogsvscats
net: "train_val.prototxt"
test_iter: 100
test_interval: 500
base_lr: 0.001
lr_policy: "step"
gamma: 0.1
stepsize: 5000
display: 100
max_iter: 10000
momentum: 0.9
weight_decay: 0.0005
snapshot: 5000
snapshot_prefix: "models/finetune"
solver_mode: GPU

修改网络文件train_val.prototxt

  1. 修改data层的data source和mean file信息,改成我们自己的LMDB和meanfile的路径和文件名
  2. Crop_size 改为208。因为我们的数据在前面生成meanfile时已经被resize则为208*208,所以不改的话会因为data size不匹配而报错,如下图。
  3. 最后一层的输出分类num_output从1000改为2. 因为例子中是1000个分类的问题,而我们这个是2分类的问题。在Fine-tune自己的数据时,这一层通常是需要修改的。
  4. 修改最后一个全连接层的层名,改为”fc8-dogcat”,这样的话会因为已训练好的模型中没有这个层的层名,就会以新的随机值初始化这一层,这样也就达到了我们适应新任务的目的。注意这里只要用到最后一层名字的地方都要修改,可以批量替换一下。
  5. 加快最后一层的学习速率,这样做的目的是让新修改的这一层用新的data重新学习,因此需要更快的学习速率,因此我们将,weight和bias的学习速率加快10倍。

这是修改前后的train_val.prototxt对比:

修改前:

#imagenet
name: "CaffeNet"
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  transform_param {
    mirror: true
    crop_size: 227
    mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
  }
# mean pixel / channel-wise mean instead of mean image
#  transform_param {
#    crop_size: 227
#    mean_value: 104
#    mean_value: 117
#    mean_value: 123
#    mirror: true
#  }
  data_param {
    source: "examples/imagenet/ilsvrc12_train_lmdb"
    batch_size: 256
    backend: LMDB
  }
}
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TEST
  }
  transform_param {
    mirror: false
    crop_size: 227
    mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
  }
# mean pixel / channel-wise mean instead of mean image
#  transform_param {
#    crop_size: 227
#    mean_value: 104
#    mean_value: 117
#    mean_value: 123
#    mirror: false
#  }
  data_param {
    source: "examples/imagenet/ilsvrc12_val_lmdb"
    batch_size: 50
    backend: LMDB
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 96
    kernel_size: 11
    stride: 4
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "conv1"
  top: "conv1"
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "norm1"
  type: "LRN"
  bottom: "pool1"
  top: "norm1"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "norm1"
  top: "conv2"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    pad: 2
    kernel_size: 5
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu2"
  type: "ReLU"
  bottom: "conv2"
  top: "conv2"
}
layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "norm2"
  type: "LRN"
  bottom: "pool2"
  top: "norm2"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv3"
  type: "Convolution"
  bottom: "norm2"
  top: "conv3"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "relu3"
  type: "ReLU"
  bottom: "conv3"
  top: "conv3"
}
layer {
  name: "conv4"
  type: "Convolution"
  bottom: "conv3"
  top: "conv4"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu4"
  type: "ReLU"
  bottom: "conv4"
  top: "conv4"
}
layer {
  name: "conv5"
  type: "Convolution"
  bottom: "conv4"
  top: "conv5"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    pad: 1
    kernel_size: 3
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu5"
  type: "ReLU"
  bottom: "conv5"
  top: "conv5"
}
layer {
  name: "pool5"
  type: "Pooling"
  bottom: "conv5"
  top: "pool5"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "fc6"
  type: "InnerProduct"
  bottom: "pool5"
  top: "fc6"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 4096
    weight_filler {
      type: "gaussian"
      std: 0.005
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu6"
  type: "ReLU"
  bottom: "fc6"
  top: "fc6"
}
layer {
  name: "drop6"
  type: "Dropout"
  bottom: "fc6"
  top: "fc6"
  dropout_param {
    dropout_ratio: 0.5
  }
}
layer {
  name: "fc7"
  type: "InnerProduct"
  bottom: "fc6"
  top: "fc7"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 4096
    weight_filler {
      type: "gaussian"
      std: 0.005
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu7"
  type: "ReLU"
  bottom: "fc7"
  top: "fc7"
}
layer {
  name: "drop7"
  type: "Dropout"
  bottom: "fc7"
  top: "fc7"
  dropout_param {
    dropout_ratio: 0.5
  }
}
layer {
  name: "fc8"
  type: "InnerProduct"
  bottom: "fc7"
  top: "fc8"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 1000
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "fc8"
  bottom: "label"
  top: "accuracy"
  include {
    phase: TEST
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "fc8"
  bottom: "label"
  top: "loss"
}

修改后:

#dogsvscats
name: "DogCatNet"
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  transform_param {
    mirror: true
    crop_size: 208
    mean_file: "dogsvscats.mean.binaryproto"
  }
# mean pixel / channel-wise mean instead of mean image
#  transform_param {
#    crop_size: 227
#    mean_value: 104
#    mean_value: 117
#    mean_value: 123
#    mirror: true
#  }
  data_param {
    source: "train_imgSet.lmdb"
    batch_size: 128
    backend: LMDB
  }
}
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TEST
  }
  transform_param {
    mirror: false
    crop_size: 208
    mean_file: "dogsvscats.mean.binaryproto"
  }
# mean pixel / channel-wise mean instead of mean image
#  transform_param {
#    crop_size: 227
#    mean_value: 104
#    mean_value: 117
#    mean_value: 123
#    mirror: false
#  }
  data_param {
    source: "val_imgSet.lmdb"
    batch_size: 50
    backend: LMDB
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 96
    kernel_size: 11
    stride: 4
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "conv1"
  top: "conv1"
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "norm1"
  type: "LRN"
  bottom: "pool1"
  top: "norm1"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "norm1"
  top: "conv2"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    pad: 2
    kernel_size: 5
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu2"
  type: "ReLU"
  bottom: "conv2"
  top: "conv2"
}
layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "norm2"
  type: "LRN"
  bottom: "pool2"
  top: "norm2"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv3"
  type: "Convolution"
  bottom: "norm2"
  top: "conv3"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "relu3"
  type: "ReLU"
  bottom: "conv3"
  top: "conv3"
}
layer {
  name: "conv4"
  type: "Convolution"
  bottom: "conv3"
  top: "conv4"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu4"
  type: "ReLU"
  bottom: "conv4"
  top: "conv4"
}
layer {
  name: "conv5"
  type: "Convolution"
  bottom: "conv4"
  top: "conv5"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    pad: 1
    kernel_size: 3
    group: 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu5"
  type: "ReLU"
  bottom: "conv5"
  top: "conv5"
}
layer {
  name: "pool5"
  type: "Pooling"
  bottom: "conv5"
  top: "pool5"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "fc6"
  type: "InnerProduct"
  bottom: "pool5"
  top: "fc6"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 4096
    weight_filler {
      type: "gaussian"
      std: 0.005
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu6"
  type: "ReLU"
  bottom: "fc6"
  top: "fc6"
}
layer {
  name: "drop6"
  type: "Dropout"
  bottom: "fc6"
  top: "fc6"
  dropout_param {
    dropout_ratio: 0.5
  }
}
layer {
  name: "fc7"
  type: "InnerProduct"
  bottom: "fc6"
  top: "fc7"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 4096
    weight_filler {
      type: "gaussian"
      std: 0.005
    }
    bias_filler {
      type: "constant"
      value: 1
    }
  }
}
layer {
  name: "relu7"
  type: "ReLU"
  bottom: "fc7"
  top: "fc7"
}
layer {
  name: "drop7"
  type: "Dropout"
  bottom: "fc7"
  top: "fc7"
  dropout_param {
    dropout_ratio: 0.5
  }
}
layer {
  name: "fc8-dogcat"		#Change this layer name
  type: "InnerProduct"
  bottom: "fc7"
  top: "fc8-dogcat"
  param {
    lr_mult: 10
    decay_mult: 1
  }
  param {
    lr_mult: 20
    decay_mult: 0
  }
  inner_product_param {
    num_output: 2		#change to 2
    weight_filler {
      type: "gaussian"
      std: 0.01
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "fc8-dogcat"
  bottom: "label"
  top: "accuracy"
  include {
    phase: TEST
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "fc8-dogcat"
  bottom: "label"
  top: "loss"
}

运行命令开始训练

最后执行训练命令:

C:\Projects\caffe\build\tools\Release\caffe.exe train --solver=solver.prototxt --weights bvlc/bvlc_reference_caffenet.caffemodel

最后发现在2000次迭代时准确率已经达到了0.9658了。

可见,通过这种方式可以快速取得较好的结果。这样的话,本例中max_iter甚至可以设的再小一点就可以取得不错的训练结果。

做完以上步骤后,再对比一下直接训练的命令:

C:\Projects\caffe\build\tools\Release\caffe.exe train --solver=solver.prototxt

就会发现用预训练模型fine-tune的过程和直接训练的过程其实是很类似的。区别只是初始化的时候命令里是否带了weights参数。
a. 不带参数直接训练的话是按照网络定义指定的方式初始化(如constant,gaussian)
b. 已有模型fine-tuning是读取你已经有模型的参数文件来作为初始值

Fine tune的要点和注意事项

  1. 运行训练命令时,提供预训练的weights给新的caffe dataset来训练,这样预训练的权重就会载入模型中,并且通过名字来匹配每一层。 
  2. Base lr学习率不要设置的太大,因为学习率过大的话原来这个模型里的权重会存在更新过快的问题,这个值一般设定不超过0.001
  3. 因为新的任务和原模型中一般是不一样的,如本例中预训练模型是1000分类,而实际任务是二分类的,所以通常需要修改模型中的最后一层,把prototxt最后一层的层名改为一个新的名字。这样这个新名字所在的层将从随机权重开始训练
  4. 同理,如果想指定某几层从随机权重开始训练,那么可以修改对应的层为新的名字即可,被修改的层都会从随机权重开始训练。
  5. 减少solver prototxt中的总体学习率base_lr,但是增加新引进层的lr_mult。主要原因是想让新数据在新加的层学习很快,而其他的层学习变慢慢
  6. 将solver中stepsize设置为比train from scratch(从0开始训练)更低的值,因为我们实际训练可能需要很长一段时间,这样stepsize变小的话后面的学习率减少得快一些。我们也可以通过将lr_mult设置为0来完全防止对最后一层以外的所有层进行微调。
  7. 并不是所有的数据集都适合拿来fine-tuning, 新的数据集和预训练数据集特征比较相似(如本例)的情况下会比较适合拿来做fine-tuning

模型的推理实施

训练完成以后,我们会得到自己的.caffemodel, 可以用来进行自己项目的部署、推理和实施。这里特别提一下,如果想在某些低性能、低功耗或可移动设备上运行深度神经网络(DNN – Deep Neural Network),可以使用Intel Movidius神经计算棒(NCS – Neural Computing Stick)来进行增强和实施。在本例中,我们将最后训练出来的模型通过Movidius对给定的一张图片进行预测:

这个结果的意思是该图片为猫(值为0)的概率为0.55,为狗(值为1)的概率为0.45。

如果对如何使用Movidius神经计算棒相关内容感兴趣的话,可以参见https://software.intel.com/zh-cn/articles/using-movidius-ncs-to-run-caffe-image-classification-model

参考资料

在Windows上的Caffe实战 – 猫狗大战:https://software.intel.com/zh-cn/articles/the-caffe-practice-on-windows-the-war-between-cat-and-dog

使用Movidius神经计算棒(NCS)运行Caffe图片分类模型:https://software.intel.com/zh-cn/articles/using-movidius-ncs-to-run-caffe-image-classification-model

NCS SDK与Caffe的集成:https://software.intel.com/zh-cn/articles/how-to-deploy-tensorflow-and-caffe-for-intel-hardware-platform-into-movidius-ncs-sdk

BVLC关于微调的官方例子和说明: http://caffe.berkeleyvision.org/gathered/examples/finetune_flickr_style.html

BVLC的model zoo: https://github.com/BVLC/caffe/wiki/Model-Zoo

imagenet的一系列模型:https://github.com/BVLC/caffe/tree/master/models

迁移学习和fine-tune的说明:http://cs231n.github.io/transfer-learning/

关于作者

钱彩红是英特尔软件与服务事业部的一名应用软件工程师,专注于在英特尔平台上与开发者的合作和业务拓展。力求将英特尔卓越的软硬件平台与合作开发者的软硬件产品完美结合,提供最优客户体验。

面向游戏和虚拟现实素材的模块化概念

$
0
0

In-game environment

模块化已成为游戏行业的热点话题和重要趋势,模块化是将素材组整理为可重复使用的内部关联模块的流程,旨在形成更大的结构与环境。核心理念是尽可能多地重复使用工作,以节约成本、改进加载速度和简化生产。但是,这些方法需要克服一些缺点。在表面上创建变体非常重要,它使模块的重复变得不明显,观众会沉浸在真实的环境中。

实时环境的一个最大问题便是我们无法在引擎内完成所有创建。作为艺术家,在最终产品到达引擎时,我们依赖于将海量程序全部整合至一个工作流。由于整个场景拥有一致、相同的细节非常重要,而不一致的微小细节将耗费更多时间,这为我们提出了挑战。这需要多次快速迭代和早期测试。借助最新的下一代工具和引擎,通过使用高级材质/着色器提升整个场景的视觉质量和终止重复,在最终场景中添加细节成为了可能。

优势

  • 快速构建大型环境
  • 节省内存

劣势

  • 开始时需要更多的规划时间
  • 看起来重复、方形或机械对齐;枯燥

在本文中,我分享了创建面向虚拟现实(VR)游戏的环境艺术的经验,这些经验适用于其他 3D 应用或体验。

如何从设计师的角度考虑:基本知识

创建模块化素材时,了解架构元素如何组合成细节和有趣的空间与了解关卡设计策略如何向用户传达关键内容一样重要。在设计和硬件的限制内,了解如何将视觉效果简化为可信的可复用预制件是一种平衡行为,您可以通过实践与学习提升自己的熟练水平。此外,这影响了我们艺术家如何看待参考以及如何决定首先创建哪些素材。强烈建议您在开始创建场景时,以最大限度降低工作量和提升可复用性为目标。这样的迭代策略在不扩大预算的情况下改善了整体质量,同时报告了后续步骤。

游戏对比商业

游戏和商业应用之间的最大区别在于游戏中的艺术专为玩家打造,而商业应用中的艺术服务于消费者。鉴于玩家也是一名消费者,游戏的规则和机制必须在空间布局和设计中突出体现,以强调游戏的趣味性。低水平设计会为用户带来较差的体验。单从消费者的角度来看,我们必须注重为他们提供舒适和美观的场景。在虚拟现实中,不管您创建哪种应用,无论是游戏还是体验,这些界线变得比较模糊。游戏通常需要艺术家、设计师和编程人员之间更多的横向思考和交流;尽早获得反馈至关重要,以避免后续问题的出现。

构建联锁素材的过程中,当我们尝试将多个部件结合成简单的结构时,必须经常查看引擎中的组件,以检查简单的非纹理或纹理模型如何对齐以及是否未对齐。测试素材时,需要检查的关键包括:

易用性:素材是否与其他素材对齐无误?网格轴点是否位于正确位置,并对齐到网格?总体而言,您是否可以轻松使用?

重复性:我们是否需要使用新部件来分解过小的套件?如果观众可以轻易看到每个部件和预制件架构,我们将难以使用户沉浸在空间中。

形式与形状: 所有物体是否均有独特的剪影,并且与引擎中的照明相互映衬?或者平面是否可以很好地分解?

合成:所有组件是否配合默契?是否能构成有趣的图像?

可读性、颜色/细节、规模与前两秒:这是为场景添加纹理与基础照明后的后期检查。请确保路径可见或重要细节可立即传达。为了实现这一目标,我们可能需要更多的素材,有时需要更少的素材。这将维护视觉平衡,从粗糙几何体与纹理快速迭代为最终素材。

了解如何从设计师的角度思考问题以及您的工作如何影响整个开发流程和观众体验最终支持您作出主观的决策,以改进设计或减少交通的混乱。了解现实世界为什么有特定的外观或感觉以及外形如何体现对象的使用是成为艺术家或设计师的关键。我遇到过许多技术故障,但是通过查阅参考了解和学习这些基础知识将使您变得更好、更快。技术只是一种工具。模块化是工具箱里的一套扳手。如果您知道自己在做什么,可以毫不费力地构建飞机。

规划的重要性

在规划模块集和评估其需求时,了解设计元素能够帮助您制定决策。如前所述,模块化要求在生产前进行充分规划。您应该从参考着手。首先分解图像,从真实空间中提取素材与观点。出色的参考以自然的方式展示多样性。沉浸感隐藏在细节中。

在这一阶段,我使用的工具是 Pinterest*,它也是我用来查找参考的终极工具。搭配使用 PureRef(一款免费工具)来查看参考,您将拥有一个非常强大的组合。Trello* 可以帮助您管理基于图像的任务并显示进度。这些工具可以帮助减少创新冲突或犹豫不决,尤其是将屏幕截图发布到 Trello 可记录今天完成的工作,这样您就知道明天从哪里重新开始了。假以时日,它将成为个人项目的得力助手,显示您已完成的进度,推动项目的发展。

与客户合作时,获取尽可能多的参考,包括图片、360 度视频、相似的空间和任何重要的细节。在某些情况下,最好提前绘制分镜图,这样在工作时,我们便可以朝着既定目标进行横向思考,客户也容易与我们达成共识。可以是某个空间的照片,包括佩戴头显的绿屏人物,以显示每个场景分别是什么。随后进入草图或粗略 3D 布局,然后是最终外观通道。考虑使用 TurboSquid*、Cubebrush*、Unity* 素材商店等网站也很重要,它们可以帮助减少素材创建的生产时间和成本。购买 Quixel Megascans* 和 Substance Source 可真正帮助您尽早获取优质的材质。由于购买的素材通常纹理质量较差,它的作用不容小觑。

一般流程概述

尝试不同的工作流和采用他人的工作流,以查看该流程是否提升或降低了您的速度,这点非常重要。作为艺术家,我领悟到追求卓越没有什么捷径可走,也没有什么秘诀可遵循。每个人都会根据各自的工作方式、工作对象和工作地点,采用不同的流程或技巧。您可以使用以下基本流程大致了解完整的个人环境包含什么。您需要采取灵活、迭代的方式。通过保持任务和文件的整洁有序,了解何时会产生令您满意的情形,并最大限度地减少创新冲突。提前为您的素材制定命名规则。

  • 收集参考:了解您的空间。在脑海中筹划设计。它感觉如何?
  • 按照任务划分参考:素材、材质、模块化组件。
  • 检查 1:尝试基本 3D 布局或基本几何体草图。比例感、空间和素材的互联很重要。
  • 测试早期照明:调整元素,以适应理想的组合方式。
  • 如果您的网格能够出色协调,场景也已完成,改进从开始到游戏决赛的网格细节,并开始解包。
  • 检查 2:尽早应用纹理,专注于基本反射率、基本法线图和支持临时照明的整体可读性。请注意查看您的反射是否过暗。
  • 开始执行您的生产计划,并继续完善细节。高多边形创建、最终的纹理通道;首先关注更大或使用更频繁的素材,并将其作为时刻检查所有部件是否紧密结合的基准。
  • 创建支持的道具,在场景中填充需要的细节,营造一种丰盈的角色感、故事感和比例感。
  • 最终的照明通道:截图,对比外部图片浏览器的变化,调整为理想的外观。

    在我看来,应用模块化的最大优势在于您将始终知晓您需要持续更新的素材数量。在专业设置中,如果需要对布局或优化进行修改,该方法帮助非常大。

纹理类型

开始查看参考之前,我们需要捕捉环境、空间和协调性。但是,分析参考的各个部分、查看每个材质的用途同样重要,这样才能注意到相同的材质应用在何处。目标是尽可能减少材质,以限制可管理范围内的工作,并减少在运行时消耗大量内存的纹理的总体负载。为了掌握模块化概念,我们需要了解我们将要使用的 3 种纹理类型。

可重复:沿 4 个方向或仅水平或垂直无限拼贴的纹理,也被称作装饰板。可重复纹理是您的前线步兵,凭借高效性和可复用性占据了场景的绝大部分。我们通常在这里着手,直接从这些纹理中建模,以获得首批构建模块。我们将在下文讨论这个话题。

tilable texture for 3d object
可重复纹理

拼贴说明与技巧:

  • Substance Designer 功能超凡,几乎能够制作可以想象的任何平铺纹理或材质。它非常擅长实现超现实主义风格,无需使用扫描数据,并且完全无损。基于节点的系统支持打开参数以用于引擎,使用随机种子进行快速迭代,可动态调整的无限定制和意外发现。
  • 对于基于物理的渲染材质,高度和法线是重中之重,然后是粗糙度和反射率。通过这种方式,首次读取显示正确,细节已经对齐,但是我们也可以使用/创建活动对象(AO)、曲度和法线图颜色通道来创建来自高度数据的遮罩,以创建粗糙度和反射率。
  • 决定如何拼贴纹理,注意重复!我的岩石纹理是明显拼贴的,但是本研究示例旨在创建更独特的部分,并混合相同岩石材质不明显的重复版本。这被称作低噪声;供应其他区域,但是通过细节吸引观众。
  • 使用直方图!为了使反射率适合任何照明条件并且看起来自然,您的反射率应该在直方图中呈现漂亮的曲线,中间部分的平均亮度约为 128。该数值可能会更低,自然变暗的表面会导致材质的反射率信息发生变化,反之亦然。此外,广泛的颜色值或更平缓的曲线更自然。请访问 Textures.com查看纹理值曲线,以获取正确的值和颜色。值得一提的是,中灰的值为 186 RGB。我还建议您在调试早期照明测试时,将该值应用于单个场景的所有素材中,以查看几何体/高度图如何增加了光影细节。
  • 反射率和粗糙度是就像一枚硬币的两面。它们均会对光线进行解析,以生成数值。反射率就像一层底漆,如果一面墙上带有高光分散,您将注意到反射率高的区域更亮,继承了浅色,而暗淡的区域更暗,更多地保持了原始反射颜色。
  • 了解如何在 Substance Designer 中创建生成器,以加速您的工作流,重复使用图表来创建出色的底层。它们可以是形状和模式、边缘缺损、裂缝、受潮损坏等。请信任您在弗兰肯斯坦图中使用的由其他艺术家提供的生成器和图表。

装饰板:

trim sheet texture

asset with texture variants
使用 Substance Smart 材质创建可互换变体。

high poly trim sheet
我们的装饰板高多边形网格:

装饰说明与技巧:

  • 根据模版的轮廓,创建一个 1 米 x 1 米的平面。保存平面,以导出为低多边形和高多边形网格,以确保元素之间没有空白。
  • 将元素对齐到网格!转换为相关的场景比例,确保纹素密度的准确性!
  • 为了在烘焙过程中拼贴装饰,几何体需要经过 1 x 1 平面。
  •  请勿解包!它被烘焙成平面,因此,我们不需要担心几何体,也无需解包这些对象。基准面已被解包为 0-1,我们已经准备好开始了。
  • 浮动物体。我们可以创建 3D 对象,将它们用作固定在或漂浮于其他元素之上的细节,如底部角落的小螺栓。由于纹理被烘焙成平面,所以烘焙无法识别深度的变化,这是因为法线应该看起来无缝混合。我们不需要将细节建模至复杂的网格,只需重复使用螺丝、螺栓或其他凹形/凸形形状,将它们放置在我们想要的地方,这节省了大量的时间。如果我们不满意烘焙结果,这种操作也不会损坏细节。
  • 节省小型、可复用细节的空间。如果浮动物体非常小,而且没有足够的像素为法线提供支持的话,有时效果会不理想。通过将细节放在底部,我们可以在游戏网格重复上重复使用细节,这需要将小平面用作浮动物体,然后将细节映射至平面。
  • 法线图中的 45 度斜角有助于磨平边缘,这个角度非常适合它们的重复使用。Sunset Overdrive提供了关于该技术的出色剖析。我并不是说每条边都需要它;它主要用于硬边的低多边形物体。
  • 简单的往往是更好的。拥有一些独特的元素,但是相比包含多个浮动物体的装饰,细节较少的部分更容易重复使用,而且更不明显。因此,在两者之间保持良好的平衡。
  • 一块装饰板上可以包含不同的材质,如金属装饰、木制线饰或橡胶条。烘焙材质 ID 遮罩可提升装饰板的多功能性,有助于节省内存。此时,完善的规划提供了极大的帮助。
  • 也可以在 Substance Designer 和 Quixel* NDO Painter 中创建不包含几何体的装饰。您可以使用 Substance Painter 中的 alpha 遮罩将细节添加至装饰几何体。

更多创新示例:

example of a texture
Image of texture
该示例面向移动增强现实项目。该方法缩小了纹理尺寸,增加了细节。

 

如果在网格上创建了装饰板,可以通过将环边添加至平面或分解组件,轻松地交替使用装饰板。该方法可快速执行设计原型,因为为了确保拼贴,UV 将是一对一的。如果规划合理,您也可以交换纹理,前提是纹理拥有相同的网格空间。更多详情敬请访问 Jacob Norris 的 分析

此处,使用一个装饰和一个可重复纹理将元素组合成基本的走廊。

独特:专注于一种素材的纹理,类似于道具。它使用来自高多边形模型的烘焙贴图生成独特的非拼贴细节。这些应该是场景的收尾工作。在理想的情况下,总是同时使用的相似道具位于单个纹理图集上。

中心道具是创建独特素材的一个例外,它是对整个场景至关重要的一类独特素材。在这种情况下,可以优先考虑获得基本或最终的独特素材。


国际象棋的所有棋子均使用同一个纹理。每个棋子从高多边形烘焙为低多边形,每个表面均拥有独特的细节。

混合:在相同的 0-1 纹理上使用拼贴元素和独特元素。也可以指更大型素材上的材质混合,使用独特的法线和遮罩传输拼贴材质,如装饰石墙或大石块。

该混合示例使用独特的法线图运行 Substance Painter 中的磨损生成器,以获得混合锈迹与金属的遮罩。红线还表示裁切现有网格以形成新几何体的位置。没错,回收利用!

这些元素组合成旧时科幻小说中的仓库设备。一个装饰(两个颜色变量)、一个混合和两个拼贴纹理。

纹素密度与差异

早期阶段需要考虑的最后一个问题是纹素密度,即我们的素材每单元将使用多少个像素。对于新手,这个问题可能会耗费很长时间,向您强烈推荐 Leonardo Lezzi 编写的这篇 文章。对于第一人称体验,我们需要每米 1000 或 1024 x 1024 个纹理,或者每厘米 10.24 个像素的密度。我们主要想使用可重复纹理,以在更大的表面上最大限度地添加视觉细节。这些规则的例外包括与玩家距离较近的可交互物体,如枪。我想在解包时使用面向 Maya* 的 Nightshade UV。该工具内置了纹素密度特性,用于设置或追踪每单元的像素。以下示例是一面 3 米 x 3 米的墙,纹素密度为 1K/米。

虚拟现实至少需要 1K/米,但是随着头显的发展,该值可能会提升至 2K/米。这为虚拟现实艺术家带来了很多挑战,因为计算机硬件仍会限制许多用户的纹理大小。虚拟现实头显已经在更广阔的视野上渲染了两个图像,这增加了提升帧速率和美化细节的难度。为了解决这个难题,可以在 Unreal Engine* 4(UE4)和 Unity* 中创建着色器,以使用分辨率较低的细节图创造接近 8K 的错觉。您可以在 Youtube 上观看该技术的简短演示:细节纹理:UE4 & Megascans 的快速教程

这些着色器设置对添加变体和分解表面拼贴至关重要,它们使用基于遮罩的材质混合顶点颜色来混合材质或颜色。由于着色器技术可以通过多种不同方式实现变异,这个话题有些空洞。面向 Unity 的 Amplify Shader Editor 是一款支持此类 AAA 艺术开发的卓越工具。此外,资深环境艺术家 Yannick Gombart 编写的分析解密了部分技术。

网格

我记得,最初,模块化是一个难以理解的概念。我们来看一下俄罗斯方块*。所有俄罗斯方块都是一种叠加游戏,组件模块组合成新的联锁形状,并落入网格

根据规定的脚注或规模,网格为我们的模块化蓝图提供指导。脚注报告关于模块化构建的基本信息,通常取决于我们开发的游戏类型。如果我们在《龙&与地下城》中,模块化走廊的底座拼贴尺寸是 5 英尺还是 10 英尺?这对玩家的移动和空间感有何影响?在俄罗斯方块中,该尺寸为底座立方体尺寸。

鉴于我们正在讨论虚拟现实,我们的 侧重点是第一人称游戏和应用,好的脚注应为 3 米 x 3 米或 4 米 x 4 米。建模时务必以厘米为单位,所以是 300 x 300。该度量系统容易被整除,使得模块可以轻松分解为游戏引擎默认使用的圆形单元。决定采用何种脚注时,需要注意,第一人称虚拟现实往往会使物体看起来比实际要小,因此,夸张的形状会使物体看起来更真实、更清晰。首先我们需要改变建模应用的网格,以模拟引擎的网格,这样它们便可以无缝集成。

如何在 Maya* 中设置我们的尺寸

首先,我们需要确保 Maya 中使用的单位为厘米。
前往 Window > Setting/Preferences > Preferences

单击 Settings,并在 Working Units下检查长度被设置为厘米。

现在我们来设置网格。

前往 Display > Grid Options

在 Size 下设置以下值。

  • 长度/宽度:1,000 个单元。这是我们透视图的整体网格尺寸,而不是每个网格单元的尺寸。
  • 网格间隔:10 个单元(就像在 Unity 或 UE4 中一样控制网格单元线;如果您可以将该值设置为 5、10、50、100,它将匹配 UE4 网格对齐。)对于 Unity,我使用了一款可以镜像更强大网格的 ProGrids 插件,类似于 Unreal 中的插件)。改变该值将反映素材如何在引擎内相互对齐。
  • 再分割:1.改变了每单元包含的网格线数量。为 1 时和为 2 时,分别表示每 10 个单元和每 5 个单元的网格线。相比每次在网格间隔字段中输入独特的数值,滑动输入是一种更快速地将网格划分为不同对齐值的方法。
    最后,创建一个 100 x 100 x 100 的立方体,并导出为 .FBX 文件,以查看您的素材是否匹配引擎中的默认立方体尺寸(通常为 1 米 x 1 米)。
  • 实用的热键:
    • 通过长按 X,您可以在转换时将物体对齐网格。
    • 长按 V将选项对齐顶点。
    • 按下 InsD键支持您移动物体轴点。如果您同时长按 XV,将获得网格上的轴点。
    • 长按 J将对齐旋转。

再次提醒,无论您使用哪款建模软件,目标都是使网格匹配引擎的网格。

以下是一篇出色的步骤详解,提供了面向网格设置的图像,它也是一个支持高水平设计和环境艺术实践的实用网站。

将一切完美融合—草图

现在我们已经了解了指标和需要查找的纹理,我们将面临两个选择。一是创建快速纹理集,通常为装饰板或拼贴纹理,并以此创建模块。二是直接进入建模程序。无论采用何种方式,将参考分解为工作单元和材质能够帮助我们充分了解空间和生产范围,从而尽快减少创新冲突。

例如,我们可以通过以下几种方式查看参考,以进行规划:

某些建模程序包含用于测量单元的距离工具。我在此处测量了基准人物,创建场景时它将被用作比例参考。

现在,我可以覆盖这个人物参考,并相应缩放他的比例,以获得参考单元。我还对某些纹理插图进行着色,突出显示了用于规划生产计划的要点。

再举一个从照片参考中创建的例子。我使用公园长椅估算建筑规模,将它分割为模块,并突出显示了装饰。

返回房间图像。考虑到单元,我现在可以在 Maya 中使用简单的平面快速绘制草图,以得到我的规模。由此建立了脚注,可用作所有高多边形网格和游戏素材的基本和根参考。

我可以从这里开始创建我的材质和素材集,例如:

如前所述,我们需要不断检查引擎内部,以确定我们的模块是否按预期对齐。处理 5、10、50、100 厘米的单元。注意,以米为单位确保了素材之间的完美纹理拼贴(如果我们的纹素密度为 1k/每米)。轴点对齐网格、正面面向 Z 方向对于导出模块非常重要。制定命名规则也是一个不错的选择,便于您在引擎中轻松查找每个模型。

在 Unreal 中,我使用基本套件装配本项目的平面。从这里开始,通过创建材质、高多边形网格和解包进行向上改进。该场景是一个由多种参考组成的自定义空间。我绘制了一个自上而下的基本地图作为平面图,然后开始创建木梁和栏杆,填充其他细节以为它们提供支持。

在 Trello 中跟踪进度类似于此:

左侧是我的参考图像,它帮助我们了解空间、道具和材质。然后我获得了显示每个阶段的进度。最后,为素材目录创建不同的列,以跟踪每个素材。每张卡片包含一份清单和我需要记住的任何图像或注释。颜色代码报告每张卡片的材质类型(拼贴、装饰、混合、独特)。现在基本只剩下改进了。

继续处理—Look Dev

需要注意的是,开发游戏时,最好能尽快完成。知道如何使用模块化概念节省时间不意味着您的最终版能够符合您的预期。您会反复修改很多遍,为了消除风险,迭代非常关键。尽早获得最终版并且执行以下 look dev 测试:

该图像绝非最终的环境。我想修改很多地方,还想更换材质。为了进入这一阶段,我使用了基础基元、某些预制素材,混合使用了我在 Substance Designer 中制作的材质和从 Substance Source 中下载的材质。目标是在整个平面营造一种逼真的气氛。该场景在一周多的时间内组装完成,这对一名艺术家来说已经很快了!

在使用模块化进行一般关卡设计时,我很早就意识到如果您一直呆在网格上,场景将变得非常枯燥。您想要保持密切、有机的构成和网格提供的易用性之间的平衡。

为了保持该平衡,我使用 Oculus Medium* 打开草图。基于体素的雕塑程序可以使虚拟现实游戏开发变得无比自由,艺术家可以轻松地预先展示整个场景。借助 PureRef 和 Pinterest,我们可以将能够加载至 Medium 的大型图像用作参考,实现了早期创建与灵感之间的无缝连接。此外,进行虚拟现实开发时,您可以轻松判断规模,相比在引擎和 Maya 之间来回切换,您可以更快速、轻松地获得空间感。在 Medium 中,很容易从各个角度感知场景,包括基本照明和环境遮挡。这使它成为一款强大的迭代工具,帮助您更快地了解场景的要素。

上图是我在 Maya 中的抽取网格。看起来不太像,但是它会在早期更有计划地为我提供关于如何在空间中移动的创意。

在此基础上,我将 Oculus 网格添加至某一层,设置为线框,以使用模块化部分对它实施反向工程。

接下来是大量使用基元,处理我的脚注,以及对齐物体。

现在,我在 Maya 中设置了一个摄像头,并且调整素材以构成更有趣的作品。在这里,我将文件保存为可供 Unity 读取的 .MA 格式,以便在 Maya 和 Unity 之间快速处理。距离较远的挂有怪异树枝的墙是使用 Oculus Medium 创建的,并在 ZBrush* 中重新划分了网格。我想要捕捉一个融合了有机赛博朋克风格的炫酷部分。楼梯也是在 Medium 中创建的,反映了最初的雕塑。

此处,我导入了 Maya 文件,以提前测试照明。

适当对场景进行填充。

很难向您说明这一点,但是我将原始 Oculus 雕塑覆盖在场景中。我从中获得了很多乐趣。此时,最初的投入终于得到了回报。现在,我可以以新的视角观察它,我必须打破刚性。

现在,我将墙偏移了一定角度,并添加了某些元素,以便创建更自然的动作,好像通过双眼浏览场景。台阶是关键的补充,它拥有一个我喜欢使用的独特工作流。

如左侧所示,为了轻松、完美地实施所有拼贴,我使用了两个平面。我将它们结合在一起,拼贴变得毫不费力。接下来,我添加了一些斜角,以删除硬边。最后,我使用 Maya 雕塑工具在几何体中添加噪声,以打破刚性。这是解包可用模块的最后阶段。相比默认工具,我倾向于使用 Nightshade UV,它极大地简化了工作。

然后,我将场景分块导出为 FBX 文件,开始在引擎中分配材质,查找能够与照明出色协调、同时调整照明的值。当前的步骤是最难的部分-改进。此后的工作都是苦差事:高多边形网格、装饰与拼贴以及用于添加变化控制的着色器。在最后的阶段,我们需要继续优化,以在虚拟现实中成功运行该场景。look dev 的许多后期效果都不可行。此外,正确的着色器尚未上线,无法确定最终的外观和风格,但是没关系,我已经获得了我需要的氛围。优化环境带来了 3 大缺陷:

  • 几何体:简化几何体与边缘流。
  • 纹理:降低分辨率;在减少不同的贴图(如粗糙度、遮罩和环境遮挡)和保证法线图为设定尺寸之间保持平衡。
  • 着色器:保持节点的简明扼要。每次在一个着色器内最多混合 3 种完整材质,使用遮罩和常数完成剩下的任务。

注:有时,外部技术会引发性能问题(如角色、脚本或后期效果),所以务必先检查其他选项。优化是另一个难题,正确优化并非易事,但是模块化工作流通过减少优化素材的数量,支持在需要时进行局部返工,简化了优化工作。

总结与建议

如果没有妥善组织项目并且/或者过早对单个素材进行过多的改进,以及早期的整体进展不充足,项目可能会陷入严重的中断。这是一个深刻的教训。不要觉得需要立刻完善;这是一个迭代流程。成为一名艺术家,最难的地方在于了解如何积极地接受反馈并在开始时建立自我价值感。有时我们一直在埋头苦干,时间很快就过去了。在其他情况下,问题纷至沓来,而且您可以从容应对。最后,一定要有自己的目标,对您想达成的目标有充分的认识。您会很容易陷入工作中,感觉没有目标,或者觉得自己需要付出更多才能做得更好。

我曾经得到最好的建议是,享受过程才是唯一真正重要的事情。一旦您失去了工作的乐趣,项目很快就会瓦解。更糟糕的是,您可能 30 岁的时候醒悟,感叹错付了多年的时光。有时,您需要多花一点时间完善各个步骤。学习如何使用软件与学习使用任何工具是一样的。用的越多,速度就会越快。您会在这个过程中不断掌握新技巧,在理想情况下,您永远都不会停止学习。创建一个环境或项目,专注于一个目标或者您想学习或改进的地方。不要幻想制作炫酷的东西。这算不上一个目标,而且您永远都完成不了。

对于环境艺术家,模块化是游戏开发的核心支柱。一旦您掌握了基本知识,便可以将场景分解为更高效、更切实可行的环境创建方法。艺术生产领域内的社区意识是艺术家拥有的最有价值的资产。得益于 ArtStation*、Polycount、YouTube*、Twitch*和艺术生产类文章的出版社,这确实是一个易于访问的、开放透明的社区。艺术最终得到了关注,您也知道了其中的佼佼者。多向这些人学习。在 ArtStation 上关注他们,像这些同行一样努力实现您的目标。遇到困境咬牙坚持,时刻激励自己,并努力从不同角度看待问题。其余的将会水到渠成。

实用链接

英特尔软件工程师帮助实施 Unreal Engine* 4.19 优化

$
0
0

Epic Unreal Engine* 4.19 的发布标志着面向英特尔技术的优化翻开了新篇章,尤其是在面向多核处理器的优化方面。过去,游戏引擎在图形特性和性能方面采用控制台设计点。一般而言,多数游戏未针对处理器进行优化,这使得 PC 性能得不到充分发挥。英特尔的 Unreal Engine 4 工作旨在帮助开发人员使用该引擎释放游戏潜能,充分利用 PC 平台的所有处理器计算能力。

英特尔对 Unreal Engine 版本 4.19 的支持工作取得了以下成效:

  • 增加了匹配用户处理器的工作线程的数量
  • 提高了布料物理系统的吞吐量
  • 集成了对英特尔® VTune™ 放大器的支持

每项成效都可帮助 Unreal Engine 用户充分利用英特尔® 架构,发挥多核系统的强大性能。这些成效还有助于改进布料物理、动态断裂、处理器颗粒等系统,增强英特尔 vTune 放大器和 C++ 编译器等英特尔工具的可操作性。本白皮书将详细讨论主要改进,为开发人员考虑使用 Unreal Engine 开发下一款 PC 游戏提供更多理由。

Unreal Engine 历史

1991 年,Tim Sweeney 在就读于马里兰大学期间创立了 Epic MegaGames(后来删除了其中的“Mega”)。他发行的首个作品是ZZT*一款共享软件益智游戏。他使用面向对象的模型在 Turbo Pascal 中编写了这款游戏,该游戏的一大优点是用户可修改游戏代码。那时,关卡编辑器已经非常普遍,但这一优点仍是重要进步。

在随后几年,Epic 发行了多款热门游戏,如《Epic Pinball》*《Jill of the Jungle》*《爵士兔子》*。1995 年,Sweeney 开始开发第一人称射击游戏,发行了多款大获成功的游戏,如《毁灭战士》*《德军总部》*《雷神之锤》*《毁灭公爵》*。1998 年,Epic 推出了可能是当时视效最佳的射击游戏 《Unreal》*,这款游戏具有更细致的图形显示效果,吸引了行业的广泛关注。很快,其他开发人员纷纷来电,表示希望获得 Unreal Engine 许可,以帮助自己开发游戏。

2010 年,在一篇 IGN 文章中,Sweeney 回忆了当时的情景,表示 Epic 团队对于大家的踊跃咨询振奋不已,他们与合作伙伴的早期合作为他们的引擎业务开了个好头。他解释道,他们继续使用“社区驱动方法,支持被许可人与我们的引擎团队进行开放、直接的沟通”。他们始终致力于创建组合工具,消除技术障碍,从而释放游戏社区的巨大创造力。他们还提供广泛的文档和支持,这正是早期引擎通常所缺乏的。

如今,Unreal Engine支持着游戏行业多数盈利能性良好的游戏。2017 年 3 月,在接受 VentureBeat 采访时,Sweeney 表示开发人员迄今通过 Unreal 游戏已挣得 100 亿美元的收入。Sweeney 表示:“在收入方面,Unreal Engine 的市场份额是排名紧随其后的竞争对手的两倍,不过 Unity* 拥有更多用户。这主要是因为 Unreal 专注于高端市场。在 Steam* 收入最多的前 100 款游戏中,Unreal 游戏的数量超过任何其他可授权引擎竞争对手的总数。”

英特尔帮助完善 Unreal Engine

目前获得 Unreal Engine 许可的游戏开发人员可充分利用本文描述的优化。我们的工作将帮助他们拓展可用平台的范围,支持从具有集成显卡的笔记本电脑和平板电脑到具有独立显卡的高端台式机,从而扩大其游戏的市场份额。这些优化可确保平台提供高端视效,如动态布料和交互式物理特性,帮助最终用户在多数 PC 系统上获得出色效率。此外,优化的英特尔工具将继续助力英特尔® 架构成为首选平台。

英特尔开发人员关系工程师 Jeff Rous 表示,英特尔与 Epic Games 的团队自 20 世纪 90 年代起一直在开展合作。Rous 参与 Unreal Engine 优化工作已有大约 6 年时间,通过电子邮件和电话会议与 Epic 工程师开展了广泛合作和密切沟通,而且每年会拜访 Epic 北卡罗来纳州总部两三次,与 Epic 团队就合作事宜进行为时一周的深入探讨。Rous 参加了一些游戏的开发工作,如 Epic 的游戏《 Fortnite* Battle Royale》,也参加了 Unreal Engine 代码优化工作。

在开始目前的工作之前,英特尔与 Unreal 紧密合作,帮助推出 Unreal Engine 4。英特尔® 开发人员专区提供了一系列优化教程,包括最先推出的Unreal Engine 4 优化教程第一部分。这些教程介绍了开发人员可在引擎内部和外部使用的工具,面向编辑器的一些最佳实践,以及有助于提高项目帧速率和稳定性的脚本。

英特尔® C++ 编译器增强特性

对于 Unreal Engine 4.12,英特尔在公开引擎版本中加入了对英特尔 C++ 编译器的支持。英特尔 C++ 编译器是基于标准的 C 和 C++ 工具,可提升应用性能。它们可无缝兼容其他主流编译器、开发环境和操作系统,并通过高级优化和单指令多数据 (SIMD) 矢量化、与英特尔® 性能库的集成和利用最新 OpenMP* 5.0 并行编程模型,提升应用性能。

Scalar and vectorized loop versions

图 1:标量和矢量化循环版本,具有英特尔® SIMD 流指令扩展、英特尔® 高级矢量扩展指令集和英特尔® 高级矢量扩展指令集 512。

从 Unreal Engine 4.12 开始,英特尔一直非常重视及时更新代码库,对渗透器工作负载的测试表明帧速率获得了显著提升。

纹理压缩改进

Unreal Engine 4 还支持英特尔的快速纹理压缩器。ISPC 代表英特尔® SPMD(单程序,多数据)程序编译器,支持开发人员通过使用代码库轻松实施多核与全新和未来的指令集。在集成 ISPC 纹理压缩库之前,最新、最先进的纹理压缩格式自适应可伸缩纹理压缩 (ASTC) 通常需要数分钟完成每个纹理的压缩。在 Sun Temple*演示(Unreal Engine 4 示例场景包的一部分)上,相比以前使用的参考编码器,压缩所有纹理的时间从 68 分钟缩短至 35 秒,而且质量也有了显著提升。这有助于内容开发人员更快速构建项目,节省开发人员每周用于这方面工作的时间。

Unreal Engine 4.19 优化

英特尔的 Unreal Engine 4.19 工作可为开发人员提供多种优势。在引擎层面,优化可改善扩展机制和任务处理。引擎层面的其他工作可确保渲染流程不会造成处理器利用率方面的瓶颈。

此外,游戏开发人员使用的许多中间件系统也会受益于优化。物理属性、人工智能、灯光、遮挡剔除、虚拟现实 (VR) 算法、植被、音频和异步计算都将受益于优化。

Unreal Engine 线程模型概述可帮助您了解 4.19 中任务处理系统变化的优势。

Unreal Engine 4 线程模型

图 2 表示时间(从左至右)。游戏线程在所有其他要素前运行,渲染线程比游戏线程慢一帧。现实其他要素在两帧后运行。

Game, render, audio threading model of Unreal Engine 4

图 2:了解 Unreal Engine 4 的线程模型。

物理功产生于游戏线程上,并行执行。动画也将并行评估。在最近的虚拟现实游戏 《机械重装》* 中,并行评估动画取得了良好效果。

图 3 所示的游戏线程可处理游戏设置、动画、物理属性、网络以及最重要的对象运转 (actor ticking) 的更新。

开发人员可使用 Tick Groups 控制对象运转的顺序。Tick Groups 并不提供并行功能,但确实支持开发人员控制相关行为,更高效地安排并行工作。这有助于确保任何并行工作后续不会造成游戏线程瓶颈。

Game thread and related jobs illustration

图 3:游戏线程和相关任务。

如下面的图 4 所示,渲染线程可生成渲染命令发送至 GPU。基本上,该场景将被贯穿,然后命令缓冲区将生成并发送至 GPU。命令缓冲区生成可并行完成,以缩短生成整个场景的命令所需的时间,更快速将项目发送至 GPU。

breaking draw calls into chunks

图 4:渲染线程模型需要将绘制调用划分为片段。

每帧分为逐一实施的相位。在每个相位内,渲染线程可变宽以为其生成命令表:

  • 深度预通道 (prepass)
  • 基础通道
  • 半透明
  • 速率

将帧划分为片段,然后将其分配至 worker 任务,在并行命令表中填充这些任务的结果。然后对它们实施序列化,并用于生成绘制调用。该引擎不会在调用站点连接工作线程,但会在同步点(相位末端)或速度足够快时这些线程被使用的点连接工作线程。

音频线程

主要音频线程类似于渲染线程,用作较低级混合函数的接口,实施以下任务:

  • 评估声音队列图形
  • 构建波实例
  • 处理衰减等

音频线程与所有面向用户的 API(如蓝图和游戏设置)交互。解码和 Source Worker 任务解码音频信息,并处理空间化和头相关转移函数 (HRTF) 解包等任务。(HRTF 对于虚拟现实游戏玩家非常重要,因为该算法可帮助用户检测声音位置和距离的差异。)

该音频硬件线程是依赖单个平台的线程(如 Microsoft Windows* 上的 XAudio2*),该线程可直接渲染至输出硬件并使用 mix。它并非由 Unreal Engine 创建或管理,但优化工作仍将影响线程使用。

任务包括两种:解码和 Source Worker。

  • 解码:解码一批压缩的源文件。使用双重缓冲解码压缩音频以便播放。
  • Source Worker:实施源处理,包括采用率转换、空间化 (HRTF) 和视效。在 INI 文件中,Source Worker 的数量可以配置。
    • 如果您拥有 4 个 worker 和 32 个源,每个 worker 将混合 8 个源。
    • Source Worker 具有高度可并行特征,因此,您可在处理器性能足够强大时增加数量。

《机械重装》还是首款支持 Unreal Engine 中的全新音频混合与线程系统的游戏。例如,在《机械重装》中,头相关转移函数占据了近一半的音频时间。

处理器工作线程扩展

在 Unreal Engine 4.19 之前,任务图形上的可用工作线程数量是有限的,且并未将英特尔® 超线程技术考虑在内。这会导致具有 6 个以上内核的系统的所有内核处于闲置状态。在任务图形(Unreal Engine 内部工作调度程序)上创建正确数量的工作线程,有助于内容创作者比以往更有效地扩展视觉增强系统,如动画、布料、破坏和颗粒等。

在 Unreal Engine 4.19 中,任务图形上的工作线程数量根据用户处理器进行计算,根据优先级该数量目前最多为 22:

if (NumberOfCoresIncludingHyperthreads > NumberOfCores)
    {
      NumberOfThreads = NumberOfCoresIncludingHyperthreads - 2;
    }
    否则
    {
      NumberOfThreads = NumberOfCores - 1;
    }

并行工作的首要目的是支持游戏使用所有可用内核。这是成功扩展的基本条件。借助改进的 4.19,内容创作者现在可高效实施扩展,将发烧级处理器充分用于布料物理属性、环境破坏、基于处理器的颗粒和高级 3D 音频等系统。

Hardware thread utilization

图 5:Unreal Engine 4.19 现在有机会利用所有可用的硬件线程。

在上面的性能指标评测示例(使用合成工作负载进行测试)中,基于英特尔® 酷睿™ i7-6950X 处理器的系统 (3.00 Ghz) 实现了充分利用。

破坏优势

多核系统线程利用率的提高能够促进破坏。破坏系统使用任务图形模拟网格分解为更小部分的动态断裂。典型的破坏工作负载包括几秒的大量模拟及随后的恢复至基准。具有更多内核的更强大处理器可通过更多断裂更长时间保持这些更小的部分,从而大幅增强逼真度。

Rous 认为开发人员可对破坏实施更多工作,将其称为使用适当内容提升逼真度的理想目标。他表示:“我们可以轻松扩展破坏,具体方法包括更频繁地分割网格,并在较长时间后在更强大的处理器上删除断裂片段。由于破坏通过物理引擎在工作线程上实施,因此处理器不会成为渲染瓶颈,直到许多系统同时运行。”

Simulation of dynamic fracturing of meshes

图 6:破坏系统可模拟网格分解为更小部分的动态断裂。

布料系统优化

布料系统用于通过动态 3D 网格动画系统增强角色和游戏环境的逼真度,这种系统可快速响应玩家、风或其他环境因素。游戏中的典型布料应用包括玩家披风或旗帜。

布料系统越逼真,游戏体验的沉浸感越强。一般而言,支持的布料系统越多,游戏场景越逼真。

开发人员长期面临着提升布料系统逼真度的挑战。如果逼真度不足,角色只能穿着紧身服装,服装随风摆动的场景毫无真实感。然而,布料系统建模面临重重困难。

早期的布料系统设计工作

德州农工大学的 Donald House 表示,Jerry Weil 在 1986 年推出了首个重要的布料模拟计算机图形模型。House 和其他专家讲授了剖析“基于计算机图形的布料和服装”的课程,详细介绍了 Weil 的工作。House 表示,Weil 开发了“一种纯几何方法模拟织物挂在约束点的情景”。Weil 的模拟过程包括两个阶段。首先,使用悬链曲线在几何上模拟布料表面,生成由约束点组成的三角形。然后,应用迭代松弛法,插入原始悬链交叉点以确保平面光滑。通过应用一次全面的近似和松弛法,然后依次轻微移动约束点,再次应用松弛阶段,这种静态悬垂模型也可表示动态行为。

大约在同一时间,连续模型出现,其使用物理布料行为建模方法。这些早期模型采用了连续表示法,将布料建模为弹性板。1987 年,Carl R. Feynman 的硕士论文首次探讨了这一领域的工作,他描述了在栅格图示上叠加连续弹性模型的情况。由于模拟网格大小方面的问题,使用连续技术实施的布料建模方法难以捕捉真实布料的复杂折叠和翘起行为。

颗粒模型广受欢迎

1992 年,颗粒模型开始受到关注。这年,David Breen 和 Donald House 开发了一种面向布料的非连续交互颗粒模型。House 曾指出,该模型可通过交互颗粒系统布料“清晰再现布料的微机械结构”。他解释道,开发这一模型的原因是,他们发现布料“相当于是由交互机械部件组成的机制,而不仅仅是一种物质,细线之间的微机械交互具有宏观动态属性”。1994 年,Breen/House 模型被证明可用于准确再现特定材料的褶状特征,自此得到了大规模推广。1996 年,Eberhard、Weber 和 Strasser 根据该模型取得了一项重大成就。他们使用 Breen/House 模型提出的基础能源方程的拉格朗日力学重组,开发了可用于力学计算的常微分方程系统。

这一动态网格模拟系统现已成为主流模型。它可即时响应玩家、风或其他环境因素,增强相关特征的逼真度,如玩家披风或旗帜。

为改进布料系统,Unreal Engine 已经过多次升级;例如在版本 4.16 中,APEX Cloth* 被 NVIDIA 的 NvCloth* 解算器取代。这种低级服装解算器负责颗粒模拟,确保服装的逼真度与集成的轻便和可扩展特征,以便今天的开发人员直接访问数据。

更多三角形,更高逼真度

在 Unreal Engine 4.19 中,英特尔工程师与 Unreal Engine 团队紧密合作,进一步优化布料系统,以提高吞吐量。布料模拟的处理与其他物理对象相似,在任务图形的工作线程上实施。这有助于开发人员在多核处理器上扩展内容,并避免瓶颈。通过这些改变,场景中的布料模拟数量增加了近 30%。

每一帧都可模拟布料,即使玩家并未看着特定点;模拟结果将决定布料系统是否出现在玩家视野中。大约相同数量的布料模拟逐帧使用处理器(假设未添加更多系统)。它易于预测,开发人员可调整所使用的数量以适应可用的扩展空间。

Examples of cloth systems

图 7:Content Examples 项目中的布料系统示例。

为便于本文中的图表说明,布料对象在每个网格中使用了 8,192 个模拟三角形,而且在捕捉数据时均位于视口中。所有数据均在英特尔® 酷睿 ™ i7-7820HK 处理器上捕捉。

 processor Usage

图 8:Unreal Engine 4 各版本之间的处理器使用有所不同,具体取决于场景中布料系统的数量。

 frames per second

图 9:Unreal Engine 4 各版本之间的每秒帧数有所不同,具体取决于场景中布料系统的数量。

增强的处理器颗粒

颗粒系统在计算机图形和视频游戏中的使用可追溯到早期阶段。因为运动是真实生活的重要特征,所以这些系统具有关键作用,通过颗粒建模创建爆炸、火球、云系统和其他事件对于实现全面沉浸感至关重要。

处理器颗粒可用的高质量特性包括:

  • 发光
  • 材料参数控制
  • 吸引器模块

协同使用处理器系统和 GPU 系统可增强多核系统上的颗粒。这类系统易于扩展,开发人员可持续添加处理器工作负载,直到扩展空间耗尽。工程师发现,将处理器颗粒搭配 GPU 颗粒可增强光线投射,支持光线从接触的对象上弹起,从而提升逼真度。每个系统都有其固有局限性,因此这种搭配能够产生优于部件简单组合的系统。

processor particles emitting light

图 10:处理器颗粒可根据可用的扩展空间轻松扩展。

英特尔® VTune™ 放大器支持

英特尔 VTune 放大器是一种行业标准工具,可用于确定线程瓶颈、同步点和处理器热点。Unreal Engine 4.19 增加了对英特尔 vTune 放大器 ITT 标记的支持。这种支持将帮助用户生成注释处理器跟踪数据,提供引擎在所有时间的运行情况的深度信息。

ITT API 具有下列特性:

  • 根据您收集的跟踪数据数量控制应用性能开销。
  • 支持简单的跟踪收集,无需重新编译应用。
  • 支持 C/C++ 和 Fortran 环境中的应用。
  • 支持通过仪表化跟踪应用代码。

用户可启动英特尔 vTune 放大器,借助 -VTune交换机在 UI 中运行 Unreal Engine 工作负载,从而充分利用这一新功能。进入该工作负载后,您只需在控制台上键入 Stat Namedevents,便可开始将 ITT 标记输出以支持跟踪。

Intel VTune Amplifier trace in Unreal Engine 4.19

图 11:Unreal Engine 4.19 中的带注释英特尔 vTune 放大器跟踪示例。

结论

相关改进解决了各个层面的技术挑战,包括引擎、中间件、游戏编辑器和游戏本身。引擎改进并不局限于单款游戏,而是能够造福整个 Unreal 开发人员生态系统。4.19 的进步解决了生态系统面临的下列处理器工作负载挑战:

  • 得益于每个对象的更多断点,增强了破坏的逼真度。
  • 增加了颗粒数量,改善了对象动画效果,如植被、布料和粉尘。
  • 增强背景特征的逼真度。
  • 增加布料系统的数量。
  • 改进颗粒(例如,与角色、NPC 和环境进行物理交互)。

随着更多的最终用户迁移至强大的多核系统,英特尔计划继续实施增加内核数量的路线图。任何受限于线程的系统或存在瓶颈的操作都受到团队的关注。开发人员应 下载最新版 Unreal Engine,经常访问英特尔开发人员专区,及时了解相关信息。

更多资源

Unreal Engine* 4 优化指南

面向布料模拟的处理器优化

设置 Destructive Mesh

CPU 扩展示例

没有任何秘密的 API:Vulkan* 简介第 7 部分

$
0
0

教程 7:统一缓冲区 - 在着色器中使用缓冲区

返回上一个教程Vulkan 简介第 6 部分 – 描述符集 

现在我们来总结一下目前介绍的知识并创建一个更典型的渲染场景。这里我们将看到一个非常简单的例子,但它反映了在屏幕上显示 3D 几何图形的最常用方法。我们将在着色器统一数据中添加变换矩阵,对上一个教程的代码进行扩展。这样我们可以看到如何在一个描述符集中使用多个不同的描述符。

当然,这里介绍的知识适用于许多其他用例,因为描述符集可能包含多种类型的资源,既有不同的也有相同的。我们将创建一个具有许多存储缓冲区或采样图像的描述符集。我们也可以将它们混合起来,如本课程所示。这里我们使用纹理(组合图像采样器)和统一缓冲区。我们将了解如何为这样的描述符创建布局,如何创建描述符集以及如何用适当的资源进行填充。

在本教程的前一部分中,我们学习了如何创建图像并将其用作着色器中的纹理。本教程也需要用到这一知识,但我们在本教程中只关注缓冲区,并学习如何将它们作为统一数据源。我们还将了解如何准备投影矩阵,如何将其复制到缓冲区,以及如何在着色器中访问它。

创建统一缓冲区

在这个例子中,我们希望在着色器中使用两种类型的统一变量:组合图像采样器(sampler2D 内部着色器)和统一投影矩阵 (mat4)。在 Vulkan* 中,统一变量(采样器等不透明类型除外)不能在全局范围内声明(如在 OpenGL* 中); 它们必须从统一缓冲区内访问。我们首先创建一个缓冲区。

缓冲区可用于许多不同用途。它们可以是顶点数据的来源(顶点属性);我们可以在缓冲区中保留顶点索引,以便它们作为索引缓冲区;它们可以包含着色器统一数据,或者我们可以将数据存储在着色器内的缓冲区,并将它们作为存储缓冲区。我们甚至可以将格式化数据保存在缓冲区中,通过缓冲区视图访问,并将它们视为 texel 缓冲区(类似于 OpenGL 的缓冲区纹理)。为实现上述所有目的,我们使用始终以相同方式创建的缓冲区。但是在缓冲区创建期间提供的用法定义了我们如何在其生命周期中使用既定缓冲区。

我们在“Vulkan 简介第 4 部分 – 顶点属性”中介绍了如何创建缓冲区,因此此处只提供源代码,而没有探究细节:

VkBufferCreateInfo buffer_create_info = {
    VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, // VkStructureType      sType
    nullptr,                              // const void          *pNext
    0,                                    // VkBufferCreateFlags  flags
    buffer.Size,                          // VkDeviceSize         size
    usage,                                // VkBufferUsageFlags   usage
    VK_SHARING_MODE_EXCLUSIVE,            // VkSharingMode        sharingMode
    0,                                    // uint32_t             queueFamilyIndexCount
    nullptr                               // const uint32_t      *pQueueFamilyIndices
  };

  if( vkCreateBuffer( GetDevice(), &buffer_create_info, nullptr, &buffer.Handle ) != VK_SUCCESS ) {
    std::cout << "Could not create buffer!"<< std::endl;
    return false;
  }

  if( !AllocateBufferMemory( buffer.Handle, memoryProperty, &buffer.Memory ) ) {
    std::cout << "Could not allocate memory for a buffer!"<< std::endl;
    return false;
  }

  if( vkBindBufferMemory( GetDevice(), buffer.Handle, buffer.Memory, 0 ) != VK_SUCCESS ) {
    std::cout << "Could not bind memory to a buffer!"<< std::endl;
    return false;
  }

return true;

1.Tutorial07.cpp, function CreateBuffer()

我们首先在 VkBufferCreateInfo 类型的变量中定义其参数,创建缓冲区。在这里我们定义了缓冲区最重要的参数,缓冲区的大小和用途。接下来,我们通过调用 vkCreateBuffer()函数来创建缓冲区。之后,我们需要通过 vkBindBufferMemory()函数调用分配一个内存对象(或使用另一个现有内存对象的一部分),将其绑定到缓冲区。只有这样,我们才能在应用中按照我们希望的方式使用缓冲区。分配专用内存对象的步骤如下:

VkMemoryRequirements buffer_memory_requirements;
vkGetBufferMemoryRequirements( GetDevice(), buffer, &buffer_memory_requirements );

VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties( GetPhysicalDevice(), &memory_properties );

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (buffer_memory_requirements.memoryTypeBits & (1 << i)) &&
    (memory_properties.memoryTypes[i].propertyFlags & property) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, // VkStructureType   sType
      nullptr,                                // const void       *pNext
      buffer_memory_requirements.size,        // VkDeviceSize      allocationSize
      i                                       // uint32_t          memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

2.Tutorial07.cpp, function AllocateBufferMemory()

若要创建可作为着色器统一数据源的缓冲区,我们需要创建一个具有 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT 用途的缓冲区。不过,我们可能也需要其他用途,这取决于我们想要如何将数据传输到缓冲区。这里我们希望使用一个绑定设备本地内存的缓冲区,因为此类内存的性能可能更高。不过,根据硬件的架构,可能无法直接从 CPU 映射这些内存并将数据复制到其中。这就是为何我们要使用分段缓冲区,我们要通过这个分段缓冲区将数据从 CPU 复制到统一缓冲区。为此,我们的统一缓冲区也必须使用 VK_BUFFER_USAGE_TRANSFER_DST_BIT 用途创建,因为它将成为数据复制操作的目标。下面,我们可以看到我们的缓冲区是如何创建的:

Vulkan.UniformBuffer.Size = 16 * sizeof(float);
if( !CreateBuffer( VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, Vulkan.UniformBuffer ) ) {
  std::cout << "Could not create uniform buffer!"<< std::endl;
  return false;
}

if( !CopyUniformBufferData() ) {
  return false;
}

return true;

3.Tutorial07.cpp, function CreateUniformBuffer()

将数据复制到缓冲区

接下来就是将适当的数据上传到统一缓冲区。我们将在缓冲区中存储 4 x 4 矩阵的 16 个元素。我们使用的是正交投影矩阵,但我们可以存储任何其他类型的数据;我们只需要记住,每个统一变量必须放置在适当的偏移上,从缓冲区的内存开始计数。这种偏移必须是特定值的倍数。换句话说,它必须与特定值对齐,或者它必须有特定的对齐方式。每个统一变量的对齐方式取决于变量的类型,规范将其定义如下:

  • 类型有 N 个字节的标量变量必须与是 N 的倍数的地址对齐。
  • 具有两个大小为 N(其类型有 N 个字节)的元素的矢量必须对齐到 2 N。
  • 具有三个或四个大小为 N 的元素的矢量必须对齐到 4 N。
  • 数组的对齐方式按照其元素的对齐方式进行计算,四舍五入为 16 的倍数。
  • 结构的对齐方式按照其任意成员的最大对齐方式进行计算,四舍五入为 16 的倍数。
  • 具有 C 列的行主矩阵的对齐方式等同于具有与矩阵元素相同类型 C 元素的矢量的对齐方式。
  • 列主矩阵的对齐方式与矩阵列类型的对齐方式相同。

上述规则与为标准 GLSL 140 布局定义的规则类似,我们也可以将其应用于 Vulkan 的统一缓冲区。但我们需要记住,将数据置于不适当的偏移会导致在着色器中获取不正确的值。

为了简单起见,我们的示例只有一个统一变量,因此它可以放在缓冲区的最开始。为了传输数据,我们将使用分段缓冲区。它是通过VK_BUFFER_USAGE_TRANSFER_SRC_BIT用途创建的,并由支持 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT属性的内存提供支持,因此我们可以映射。下面,我们可以看到数据如何被复制到分段缓冲区:

const std::array<float, 16> uniform_data = GetUniformBufferData();

void *staging_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.StagingBuffer.Memory, 0, Vulkan.UniformBuffer.Size, 0, &staging_buffer_memory_pointer) != VK_SUCCESS ) {
    std::cout << “Could not map memory and upload data to a staging buffer!”<< std::endl;
    return false;
}

memcpy( staging_buffer_memory_pointer, uniform_data.data(), Vulkan.UniformBuffer.Size );

VkMappedMemoryRange flush_range = {
    VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,  // VkStructureType  sType
    nullptr,                                // const void      *pNext
    Vulkan.StagingBuffer.Memory,            // VkDeviceMemory   memory
    0,                                      // VkDeviceSize     offset
    Vulkan.UniformBuffer.Size               // VkDeviceSize     size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.StagingBuffer.Memory );

4.Tutorial07.cpp, function CopyUniformBufferData()

首先,我们准备投影矩阵数据。它存储在一个 std:: 数组中,但我们可以将它保存在其他任何类型的变量中。接下来,我们映射绑定到分段缓冲区的内存。我们需要访问至少与我们想要复制的数据一样大小的内存,所以我们还需要创建足够大的暂存缓冲区来保存它。接下来,我们使用普通的 memcpy() 函数将数据复制到分段缓冲区。现在,我们必须告诉驱动程序缓冲区内存的哪些部分发生了变化;这个操作被称为刷新。之后,我们取消映射暂存缓冲区的内存。请记住,频繁的映射和取消映射可能会影响我们应用的性能。在 Vulkan 中,资源可以随时进行映射,这不会对我们的应用造成任何影响。如果我们想要使用分段资源频繁传输数据,我们应该只映射一次,并保留获取的指针以供将来使用。我们将取消映射,为您展示如何操作。

现在我们需要将数据从暂存缓冲区传输到我们的目标——统一缓冲区。为此,我们需要准备一个命令缓冲器,在其中记录适当的操作并提交这些操作。

我们从未使用的任何命令缓冲区开始。它必须从针对支持传输操作的队列创建的池中分配。Vulkan 规范要求至少有一个可用的通用队列——一个支持图形(渲染)、计算和传输操作的队列。在英特尔® 硬件中,只有一个队列系列包含一个通用队列,所以我们没有这个问题。其他硬件厂商可能支持其他类型的队列系列,甚至可能支持专门用于数据传输的队列系列。在这种情况下,我们应该从这样的系列中选择一个队列。

我们通过调用 vkBeginCommandBuffer()函数开始记录命令缓冲区。接下来,我们记录执行数据传输的 vkCmdCopyBuffer()命令, 告知其我们想要将数据从分段缓冲区的最开始(0th偏移)复制到统一缓冲区的最开始(也是 0th偏移)。我们还提供要复制的数据的大小。

接下来我们需要告诉驱动程序,在执行数据传输之后,我们的整个统一缓冲区将被用作统一缓冲区。这是通过放置一个缓冲内存屏障来实现的,在这个屏障中我们告知它,到目前为止,我们将数据传输到缓冲区(VK_ACCESS_TRANSFER_WRITE_BIT),但从现在开始,我们将使用 (VK_ACCESS_UNIFORM_READ_BIT)作为统一变量的数据源。使用 vkCmdPipelineBarrier()函数调用放置缓冲内存屏障。它发生在数据传输操作 (VK_PIPELINE_STAGE_TRANSFER_BIT)之后、顶点着色器执行之前, 因为我们在顶点着色器(VK_PIPELINE_STAGE_VERTEX_SHADER_BIT)中访问我们的统一变量。

最后,我们可以结束命令缓冲区并将其提交给队列。整个过程在下面的代码中展现:

// Prepare command buffer to copy data from staging buffer to a uniform buffer
VkCommandBuffer command_buffer = Vulkan.RenderingResources[0].CommandBuffer;

VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, // VkStructureType              sType
  nullptr,                                     // const void                  *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, // VkCommandBufferUsageFlags    flags
  nullptr                                      // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info);

VkBufferCopy buffer_copy_info = {
  0,                                // VkDeviceSize       srcOffset
  0,                                // VkDeviceSize       dstOffset
  Vulkan.UniformBuffer.Size         // VkDeviceSize       size
};
vkCmdCopyBuffer( command_buffer, Vulkan.StagingBuffer.Handle, Vulkan.UniformBuffer.Handle, 1, &buffer_copy_info );

VkBufferMemoryBarrier buffer_memory_barrier = {
  VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, // VkStructureType    sType;
  nullptr,                          // const void        *pNext
  VK_ACCESS_TRANSFER_WRITE_BIT,     // VkAccessFlags      srcAccessMask
  VK_ACCESS_UNIFORM_READ_BIT,       // VkAccessFlags      dstAccessMask
  VK_QUEUE_FAMILY_IGNORED,          // uint32_t           srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,          // uint32_t           dstQueueFamilyIndex
  Vulkan.UniformBuffer.Handle,      // VkBuffer           buffer
  0,                                // VkDeviceSize       offset
  VK_WHOLE_SIZE                     // VkDeviceSize       size
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, 0, 0, nullptr, 1, &buffer_memory_barrier, 0, nullptr );

vkEndCommandBuffer( command_buffer );

// Submit command buffer and copy data from staging buffer to a vertex buffer
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,    // VkStructureType    sType
  nullptr,                          // const void        *pNext
  0,                                // uint32_t           waitSemaphoreCount
  nullptr,                          // const VkSemaphore *pWaitSemaphores
  nullptr,                          // const VkPipelineStageFlags *pWaitDstStageMask;
  1,                                // uint32_t           commandBufferCount
  &command_buffer,                  // const VkCommandBuffer *pCommandBuffers
  0,                                // uint32_t           signalSemaphoreCount
  nullptr                           // const VkSemaphore *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

vkDeviceWaitIdle( GetDevice() );
return true;

5.Tutorial07.cpp, function CopyUniformBufferData()

在上面的代码中,我们调用 vkDeviceWaitIdle()函数,以确保数据传输操作在我们继续处理之前完成。但在实际情形中,我们应该使用信号和/或栅栏来执行更适当的同步。等待所有的 GPU 操作完成可能会破坏应用的性能。

准备描述符集

现在我们可以准备描述符集,即应用和管道之间的接口,通过这个管道我们可以提供着色器所用的资源。我们首先创建一个描述符集布局。

创建描述符集布局

渲染 3D 几何图形的最典型方式是将顶点与顶点着色器内的模型、视图和投影矩阵相乘。这些矩阵可以在模型视图投影矩阵中累积。我们需要为统一变量中的顶点着色器提供这样一个矩阵。通常我们希望自己的几何图形具有纹理;片段着色器需要访问纹理 - 组合图像采样器。我们也可以使用单独的采样图像和采样器;使用组合图像采样器可能会在某些平台上实现更出色的性能。

当我们发出绘图命令时,我们希望顶点着色器能够访问统一变量,片段着色器能够访问组合图像采样器。这些资源必须在描述符集中提供。为提供这样的集合,我们需要创建一个适当的布局,它定义了描述符集内存储哪些类型的资源。

  std::vector<VkDescriptorSetLayoutBinding> layout_bindings = {
  {
    0,                                         // uint32_t           binding
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, // VkDescriptorType   descriptorType
    1,                                         // uint32_t           descriptorCount
    VK_SHADER_STAGE_FRAGMENT_BIT,              // VkShaderStageFlags stageFlags
    nullptr                                    // const VkSampler *pImmutableSamplers
  },
  {
    1,                                         // uint32_t           binding
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,         // VkDescriptorType   descriptorType
    1,                                         // uint32_t           descriptorCount
    VK_SHADER_STAGE_VERTEX_BIT,                // VkShaderStageFlags stageFlags
    nullptr                                    // const VkSampler *pImmutableSamplers
  }
};

VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, // VkStructureType  sType
  nullptr,                                             // const void      *pNext
  0,                                                   // VkDescriptorSetLayoutCreateFlags flags
  static_cast<uint32_t>(layout_bindings.size()),       // uint32_t         bindingCount
  layout_bindings.data()                               // const VkDescriptorSetLayoutBinding *pBindings
};

if( vkCreateDescriptorSetLayout( GetDevice(), &descriptor_set_layout_create_info, nullptr, &Vulkan.DescriptorSet.Layout ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor set layout!"<< std::endl;
  return false;
}

return true;

6.Tutorial07.cpp, function CreateDescriptorSetLayout()

描述符集布局是通过指定绑定来创建的。每个绑定在描述符集中定义一个单独的条目,并在描述符集中有其自己的唯一索引。在上面的代码中,我们定义了描述符集(及其布局);它包含两个绑定。第一个绑定的索引为 0,用于片段着色器访问的一个组合图像采样器。第二个绑定的索引为 1,用于顶点着色器访问的的统一缓冲区。这两者都是单一资源;它们不是阵列。不过,我们还可以在 VkDescriptorSetLayoutBindin 结构的 descriptorCount 成员中提供大于 1 的数值,以此指定每个绑定表示一组资源。

着色器中也使用绑定。当我们定义统一变量时,我们需要指定与创建布局时提供的绑定值相同的绑定值:

layout( set=S, binding=B ) uniform <variable type> <variable name>;

有两点值得一提。绑定不需要连续。我们可以创建一个包含三个绑定的布局,例如索引 2、5 和 9。但未使用的插槽仍可能使用某些内存,因此我们应该使绑定尽可能接近 0。

我们还指定哪些着色器阶段需要访问哪些类型的描述符(哪些绑定)。如果不确定,我们可以提供更多阶段。例如,假设我们要创建多个管道,所有管道都使用相同布局的描述符集。在其中一些管道中,统一缓冲区将在顶点着色器、其他的几何图形着色器以及其他的顶点和片段着色器中访问。为此,我们可以创建一个布局,在这个布局中指定统一缓冲区将由顶点、几何图形和片段着色器访问。但我们不应该提供不必要的着色器阶段,因为像往常一样,它可能会影响性能(尽管这并不意味着它会影响性能)。

指定一个绑定阵列后,我们在一个类型为 VkDescriptorSetLayoutCreateInfo 的变量中为其提供一个指针。这个变量的指针在 vkCreateDescriptorSetLayout()函数中提供,该函数创建实际布局。当我们有这个函数时,我们可以分配一个描述符集。但首先,我们需要一个可以分配集合的内存池。

创建描述符池

当我们想创建一个描述符池时,我们需要知道将有哪些类型的资源在从池中分配的描述符集中定义。我们还需要指定可以存储在从池分配的描述符集中的每种类型的最大资源数量,以及从池中分配的描述符集的最大数量。例如,我们可以为一个组合图像采样器和一个统一缓冲区准备一个存储,但总共需要两个集合。这意味着我们可以有两个集合,一个带纹理,一个带统一缓冲区。或者只有一个同时带统一缓冲区和纹理的集合(在这种情况下,第二个池为空,因为它不能包含这两个资源)。

在我们的示例中,我们只需要一个描述符集,我们可以在下面看到如何为它创建一个描述符池:

std::vector<VkDescriptorPoolSize> pool_sizes = {
  {
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,   // VkDescriptorType  type
    1                                            // uint32_t          descriptorCount
  },
  {
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,           // VkDescriptorType  type
    1                                            // uint32_t          descriptorCount
  }
};

VkDescriptorPoolCreateInfo descriptor_pool_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, // VkStructureType     sType
  nullptr,                                       // const void         *pNext
  0,                                             // VkDescriptorPoolCreateFlags flags
  1,                                             // uint32_t            maxSets
  static_cast<uint32_t>(pool_sizes.size()),      // uint32_t            poolSizeCount
  pool_sizes.data()                              // const VkDescriptorPoolSize *pPoolSizes
};

if( vkCreateDescriptorPool( GetDevice(), &descriptor_pool_create_info, nullptr, &Vulkan.DescriptorSet.Pool ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor pool!"<< std::endl;
  return false;
}

return true;

7.Tutorial07.cpp, function CreateDescriptorPool()

现在,我们准备使用先前创建的布局从池中分配描述符集。

分配描述符集

描述符集合分配非常简单。我们只需要一个描述符池和一个布局即可。我们指定要分配的描述符集的数量,并这样调用 vkAllocateDescriptorSets()函数:

  VkDescriptorSetAllocateInfo descriptor_set_allocate_info = {
    VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, // VkStructureType   sType
    nullptr,                                        // const void       *pNext
    Vulkan.DescriptorSet.Pool,                      // VkDescriptorPool  descriptorPool
    1,                                              // uint32_t      descriptorSetCount
    &Vulkan.DescriptorSet.Layout                // const VkDescriptorSetLayout *pSetLayouts
};

if( vkAllocateDescriptorSets( GetDevice(), &descriptor_set_allocate_info, &Vulkan.DescriptorSet.Handle ) != VK_SUCCESS ) {
    std::cout << "Could not allocate descriptor set!"<< std::endl;
    return false;
}

return true;

8.Tutorial07.cpp, function AllocateDescriptorSet()

更新描述符集

我们已经分配了一个描述符集。它用于为管道提供纹理和统一缓冲区,以便它们可以在着色器中使用。现在我们必须提供将用作描述符的特定资源。对于组合的图像采样器,我们需要两个资源——可以在着色器中进行采样的图像(必须使用 VK_IMAGE_USAGE_SAMPLED_BIT 用途创建)和采样器。这是两个单独的资源,但它们一起提供以形成单个组合的图像采样器描述符。如欲了解关于如何创建这两个资源的详细信息,请参见 Vulkan 简介第 6 部分 – 描述符集。对于统一缓冲区,我们将提供一个先前创建的缓冲区。为了向描述符提供特定资源,我们需要更新描述符集。在更新期间,我们指定描述符类型,绑定数量,并完全按照我们在创建布局时的方式进行计数。这些值必须匹配。除此之外,根据描述符类型,我们还需要创建这些类型的变量:

  • VkDescriptorImageInfo,用于采样器、采样图像、组合图像采样器和输入附件
  • VkDescriptorBufferInfo,用于统一和存储缓冲区及其动态变化
  • VkBufferView,用于统一和存储 texel 缓冲区

通过它们,我们提供了应当用于相应描述符的特定 Vulkan 资源的句柄。所有这些都提供给 vkUpdateDescriptorSets()函数,如下所示:

VkDescriptorImageInfo image_info = {
  Vulkan.Image.Sampler,                    // VkSampler        sampler
  Vulkan.Image.View,                       // VkImageView      imageView
  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL // VkImageLayout    imageLayout
};

VkDescriptorBufferInfo buffer_info = {
  Vulkan.UniformBuffer.Handle,             // VkBuffer         buffer
  0,                                       // VkDeviceSize     offset
  Vulkan.UniformBuffer.Size                // VkDeviceSize     range
};

std::vector<VkWriteDescriptorSet> descriptor_writes = {
  {
    VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,    // VkStructureType     sType
    nullptr,                                   // const void         *pNext
    Vulkan.DescriptorSet.Handle,               // VkDescriptorSet     dstSet
    0,                                         // uint32_t            dstBinding
    0,                                         // uint32_t            dstArrayElement
    1,                                         // uint32_t            descriptorCount
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, // VkDescriptorType    descriptorType
    &image_info,                               // const VkDescriptorImageInfo  *pImageInfo
    nullptr,                                   // const VkDescriptorBufferInfo *pBufferInfo
    nullptr                                    // const VkBufferView *pTexelBufferView
  },
  {
    VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,    // VkStructureType     sType
    nullptr,                                   // const void         *pNext
    Vulkan.DescriptorSet.Handle,               // VkDescriptorSet     dstSet
    1,                                         // uint32_t            dstBinding
    0,                                         // uint32_t            dstArrayElement
    1,                                         // uint32_t            descriptorCount
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,         // VkDescriptorType    descriptorType
    nullptr,                                   // const VkDescriptorImageInfo  *pImageInfo
    &buffer_info,                              // const VkDescriptorBufferInfo *pBufferInfo
    nullptr                                    // const VkBufferView *pTexelBufferView
  }
};

vkUpdateDescriptorSets( GetDevice(), static_cast<uint32_t>(descriptor_writes.size()), &descriptor_writes[0], 0, nullptr );
return true;

9.Tutorial07.cpp, function UpdateDescriptorSet()

现在我们有一个有效的描述符集。我们可以在命令缓冲区记录期间绑定它。但我们需要一个使用适当的管道布局创建的管道对象。

准备绘图状态

创建的描述符集布局有两种用途:

  • 从池中分配描述符集
  • 创建管道布局

描述符集布局指定了描述符集包含的资源类型。管道布局指定管道及其着色器可以访问哪些类型的资源。这就是我们可以在命令缓冲区记录过程中使用描述符集的原因,我们需要创建管道布局。

创建管线布局

管道布局定义既定管道可以访问的资源。这些分为描述符和 push 常量。要创建管道布局,我们需要提供描述符集布局的列表以及 push 常量范围的列表。

Push 常量提供了一种方法,可以轻松快速地将数据传递到着色器。遗憾的是,数据量也非常有限。规范仅允许 128 个字节可用于在既定时间提供给管道的 push 常量数据。硬件厂商可能允许我们提供更多数据,但与通常的描述符(如统一缓冲区)相比,数据量仍然非常少。

在本例中,我们不使用 push 常量范围,所以我们只需要提供描述符集布局并调用 vkCreatePipelineLayout()函数。下面的代码就是这样做的:

VkPipelineLayoutCreateInfo layout_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, // VkStructureType              sType
  nullptr,                                       // const void                  *pNext
  0,                                             // VkPipelineLayoutCreateFlags  flags
  1,                                             // uint32_t                     setLayoutCount
  &Vulkan.DescriptorSet.Layout,                  // const VkDescriptorSetLayout *pSetLayouts
  0,                                             // uint32_t                     pushConstantRangeCount
  nullptr                                        // const VkPushConstantRange   *pPushConstantRanges
};

if( vkCreatePipelineLayout( GetDevice(), &layout_create_info, nullptr, &Vulkan.PipelineLayout ) != VK_SUCCESS ) {
  std::cout << "Could not create pipeline layout!"<< std::endl;
  return false;
}
return true;

10.Tutorial07.cpp, function CreatePipelineLayout()

创建着色器程序

现在我们需要一个图形管道。从性能和代码开发的角度来看,管道创建是一个非常耗时的过程。我将跳过这段代码,仅提供着色器的 GLSL 源代码。

在绘制过程中使用的顶点着色器获取顶点位置,并将其乘以从统一变量读取的投影矩阵。这个变量存储在一个统一缓冲区中。描述符集(我们通过描述符集提供统一缓冲区)是管道布局创建过程中指定的描述符集列表中的第一个(也是这种情况下的唯一一个)。因此,当我们记录命令缓冲区时,我们可以将其绑定到 0th索引。这是因为,我们绑定描述符集的索引必须与在管道布局创建期间提供的描述符集布局相对应的索引相匹配。必须在着色器中指定相同的集合索引。统一缓冲区由该集合内的第二个绑定(其索引等于 1)表示,并且还必须指定相同的绑定编号。这是整个顶点着色器的源代码:

#version 450

layout(set=0, binding=1) uniform u_UniformBuffer {
    mat4 u_ProjectionMatrix;
};

layout(location = 0) in vec4 i_Position;
layout(location = 1) in vec2 i_Texcoord;

out gl_PerVertex
{
    vec4 gl_Position;
};

layout(location = 0) out vec2 v_Texcoord;

void main() {
    gl_Position = u_ProjectionMatrix * i_Position;
    v_Texcoord = i_Texcoord;
}

11. shader.vert, -

在着色器内,我们还将纹理坐标传递给片段着色器。片段着色器利用它们,对组合的图像采样器进行采样。它通过绑定到索引 0 的同一个描述符集来提供,但它是其中的第一个描述符,所以在这种情况下,我们指定 0(零)作为绑定的数值。查看片段着色器的完整 GLSL 源代码:

#version 450

layout(set=0, binding=0) uniform sampler2D u_Texture;

layout(location = 0) in vec2 v_Texcoord;

layout(location = 0) out vec4 o_Color;

void main() {
  o_Color = texture( u_Texture, v_Texcoord );
}

12. shader.frag, -

在应用中使用它们之前,需要将上述两个着色器转换为 SPIR-V* 程序集。核心 Vulkan 规范仅允许将二进制 SPIR-V 数据作为着色器指令的来源。我们可以利用它们创建两个着色器模块,每个着色器阶段一个模块,并使用它们创建图形管道。其余的管道状态保持不变。

绑定描述符集

假设我们已经创建了所有其他资源并准备好用于绘制几何图形。接下来开始记录命令缓冲区。绘图命令只能在渲染通道内调用。绘制任何几何图形之前,我们需要设置所有需要的状态 - 首先,我们需要绑定图形管道。除此之外,如果我们使用顶点缓冲区,我们需要为此绑定合适的缓冲区。如果我们想发布索引绘图命令,我们也需要绑定一个具有顶点索引的缓冲区。当我们使用统一缓冲区或纹理等着色器资源时,我们需要绑定描述符集。我们的方法是调用 vkCmdBindDescriptorSets()函数。在这个函数中,我们不仅需要提供描述符集的句柄,还需要提供管线布局的句柄(所以我们需要保留它)。只有在此之后,我们才能记录绘图命令。这些操作在下面的代码中提供:

vkCmdBeginRenderPass( command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

vkCmdBindPipeline( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

// ...

VkDeviceSize offset = 0;
vkCmdBindVertexBuffers( command_buffer, 0, 1, &Vulkan.VertexBuffer.Handle, &offset );

vkCmdBindDescriptorSets( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.PipelineLayout, 0, 1, &Vulkan.DescriptorSet.Handle, 0, nullptr );

vkCmdDraw( command_buffer, 4, 1, 0, 0 );

vkCmdEndRenderPass( command_buffer );

13.Tutorial07.cpp, function PrepareFrame()

不要忘记,典型的动画帧需要我们从交换链获取图像,如上所述记录命令缓冲区(或更多),将其提交给队列,并呈现之前获取的交换链图像,以便其根据交换链创建期间请求的当前模式进行显示。

教程 7 执行

我们来看看示例程序生成的最终图像应该如何显示:

track with Intel logo

我们仍渲染一个纹理应用于其表面的四边形。但当我们改变应用窗口的大小时,四边形的大小应该保持不变。

清理

像往常一样,当应用结束时,我们应该执行清理。

// ...

if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
  vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
  Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
}

if( Vulkan.PipelineLayout != VK_NULL_HANDLE ) {
  vkDestroyPipelineLayout( GetDevice(), Vulkan.PipelineLayout, nullptr );
  Vulkan.PipelineLayout = VK_NULL_HANDLE;
}

// ...

if( Vulkan.DescriptorSet.Pool != VK_NULL_HANDLE ) {
  vkDestroyDescriptorPool( GetDevice(), Vulkan.DescriptorSet.Pool, nullptr );
  Vulkan.DescriptorSet.Pool = VK_NULL_HANDLE;
}

if( Vulkan.DescriptorSet.Layout != VK_NULL_HANDLE ) {
  vkDestroyDescriptorSetLayout( GetDevice(), Vulkan.DescriptorSet.Layout, nullptr );
  Vulkan.DescriptorSet.Layout = VK_NULL_HANDLE;
}

DestroyBuffer( Vulkan.UniformBuffer );

14.Tutorial07.cpp, function destructor

像往常一样,大部分资源都被销毁。我们按照与创建顺序相反的顺序来执行此操作。这里只介绍与我们的示例相关的代码部分。通过调用 vkDestroyPipeline()函数销毁图形管道。若要销毁其布局,我们需要调用 vkDestroyPipelineLayout()函数。我们不需要单独销毁所有描述符集,因为当我们销毁一个描述符池时,从它分配的所有集也都被销毁。若要销毁描述符池,我们需要调用 vkDestroyDescriptorPool()函数。描述符集布局需要使用 vkDestroyDescriptorSetLayout()函数单独销毁。之后,我们可以销毁统一缓冲区;这一操作通过 vkDestroyBuffer()函数完成。但我们不能忘记通过 vkFreeMemory()函数销毁绑定到它的内存,如下所示:

if( buffer.Handle != VK_NULL_HANDLE ) {
  vkDestroyBuffer( GetDevice(), buffer.Handle, nullptr );
  buffer.Handle = VK_NULL_HANDLE;
}

if( buffer.Memory != VK_NULL_HANDLE ) {
  vkFreeMemory( GetDevice(), buffer.Memory, nullptr );
  buffer.Memory = VK_NULL_HANDLE;
}

15.Tutorial07.cpp, function DestroyBuffe()

结论

在本部分教程中,我们通过将统一缓冲区添加到描述符集来扩展前一部分的示例。与所有其他缓冲区一样创建统一缓冲区,但在创建期间指定了VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT用途。我们还分配了专用内存并将其绑定到缓冲区,并使用分段缓冲区将投影矩阵数据上传到缓冲区。

接下来,我们准备了描述符集,首先创建一个描述符集布局,其中包含一个组合图像采样器和一个统一缓冲区。接下来,我们创建了一个足够大的描述符池来包含这两种类型的描述符资源,并且我们从中分配了一个描述符集。之后,我们更新了带采样器句柄的描述符集,采样图像的图像视图以及在这部分教程中创建的缓冲区。

其余操作与我们已知的操作相似。我们在管道布局创建时使用了描述符集布局,然后在将描述符集绑定到命令缓冲区时也使用了描述符集布局。

我们再次了解了如何为顶点着色器和片段着色器准备着色器代码,并且我们学习了如何访问通过相同描述符集的不同绑定提供的不同类型描述符。

本教程的下一部分将有所不同,我们将了解并比较管理多个资源并处理各种更复杂任务的不同方法。

面向永久性内存编程的 C++ 扩展

$
0
0

概述

行业日益关注永久性内存技术。目前已上市的产品包括非易失性双列直插式内存模块 (NVDIMM-N) 和动态随机访问内存 (DRAM) 及基于 NAND 闪存的存储。  新技术正不断涌现,如英特尔® 3D XPoint™ 内存,该技术将配备在英特尔® 至强® 可扩展处理器产品家族更新版(代号 Cascade Lake)中。这些新型硬件为开发人员带来了令人振奋的全新可能性和一些编程挑战。

永久性内存编程 完全不同于易失性内存的传统编程,前者要求在完成程序、发生应用或系统崩溃或 电源故障后保留数据。英特尔 开发了一套开源库,即 永久性内存开发人员套件 (PMDK) ,以简化了为使用永久性内存实施的应用转换。 本文描述了面向 PMDK libpmemobj  库的 C++ API,以及其他对 C++ 标准的变更提议。

下载技术文章 (PDF)  

资源

英特尔开发人员专区上的永久性内存编程信息

pmem.io - 使用永久性内存开发人员套件 (PMDK) 进行编程

Github site 永久性内存编程

Google Group永久性内存编程

CppCon 2017:Tomasz Kapela 的课程 C++ 和永久性内存技术,如英特尔的 3D-XPoint

 

 

 

代码示例:面向永久性内存编程的 Java* API 简介

$
0
0

文件:

下载
许可:3-Clause BSD 许可
面向...优化 
操作系统:Linux* 内核版本 4.3 或更高版本
硬件:模拟:参考如何使用动态随机访问内存 (DRAM) 模拟永久性内存
软件:
(编程语言、工具、IDE、框架)
C++ 编译器、JDK、永久性内存开发人员套件 (PMDK) 库和面向 Java* (PCJ) 的永久性集合
前提条件:

熟悉 C++ 和 Java

简介

在本文中,我将介绍面向永久性内存编程的 Java* (PCJ) API 的永久性集合。该 API 面向永久性集合,因为集合类别可有效映射至许多永久性内存应用的用例。我演示了如何对永久性集合进行实例化和存储(不进行序列化),并稍后在重启后提取它。本文详细描述了一个完整示例(包括源代码),其包含一个员工永久性数组(从头开始实施的员工自定义永久性类别)。在本文最后,我说明了如何编译和运行使用 PCJ 的 Java 程序。

我们为何需要 API?

NVM 编程模型 (NPM) 标准由存储和网络行业协会 (SNIA) 中的业内主要厂商制定,内存映射文件是其中的核心。选择该模型的主要目的是避免无谓的重复工作,他们努力解决的多数问题(例如,如何采集和查找内存选项,及对其命名,或如何提供访问控制、权限等)已被文件系统 (FS) 解决。此外,内存映射文件已存在数十年。因此,它们非常稳定、易于掌握且得到广泛支持。使用专门 FS,用户空间中运行的流程可在打开和映射文件后直接访问映射内存,无需 FS 的直接支持,从而避免了高成本的数据块高速缓存/刷新,及操作系统 (OS) 双向上下文切换。

然而,直接对照内存映射文件进行编程并非无关紧要。即使我们可避免动态随机访问内存 (DRAM) 上的数据块高速缓存,最新写入的一些数据可能仍会驻留在 CPU 高速缓存中(不会刷新)。遗憾的是,这些高速缓存未受到突然断电方面的保护。如果发生这种情况,而且高速缓存中的部分写入内容仍未刷新,数据结构可能会损坏。为避免这一问题,程序员在设计数据结构时需允许临时残缺写入 (torn-write),确保及时发布合适的刷新指令(刷新过多会影响性能,因此也不可取)。

可喜的是,英特尔开发了 永久性内存开发人员套件 (PMDK),其包括一系列开源库和工具,可提供低级基元和实用的高级抽象化功能,从而帮助永久性内存程序员克服这些障碍。尽管这些库采用 C 实施,企业也在努力提供适用于其他主流语言的 API,包括 C++、Java*(本文主题)及 Python*。尽管面向 Java 和 Python 的 API 仍处于初期的试验性阶段,但相关工作正在稳步、快速推进。

面向 Java* 的永久性集合 (PCJ)

该 API 支持在 Java 中进行永久性内存编程,主要面向永久性集合,原因在于集合类别可有效映射至许多永久性内存应用的用例。相比于 Java 虚拟机 (JVM) 实例,这些类别的实例可持续(可访问)更长时间。除内置类别外,程序员还可定义自己的永久性类别(见下文)。我们甚至可通过低级存储器 API 创建自己的抽象化功能(采用 MemoryRegion接口形式),但该话题不在本文的探讨范围。

下面列出了该 API 支持的永久性集合:

  • 永久性基本数组: PersistentBooleanArray、PersistentByteArray、PersistentCharArray、PersistentDoubleArray、PersistentFloatArray、PersistentIntArray、PersistentLongArray、PersistentShortArray
  • 永久性数组: PersistentArray<AnyPersistent>
  • 永久性元组: PersistentTuple<AnyPersistent, …>
  • 永久性元组列表: PersistentArrayList<AnyPersistent>
  • 永久性哈希图: PersistentHashMap<AnyPersistent, AnyPersistent>
  • 永久性链接列表: PersistentLinkedList<AnyPersistent>
  • 永久性链接队列: PersistentLinkedQueue<AnyPersistent>
  • 永久性跳跃列表图: PersistentSkipListMap<AnyPersistent, AnyPersistent>
  • 永久性 FP 树图: PersistentFPTreeMap<AnyPersistent, AnyPersistent>
  • 永久性 SI 哈希图: PersistentSIHashMap<AnyPersistent, AnyPersistent>

类似于 PMDK 中的 C/C++ libpmemobj库,我们需要通用的对象固定在永久性内存池中创建的所有其他对象。对于 PCJ,该操作可通过名为 ObjectDirectory的单例类别完成。在内部,该类别可使用 PersistentHashMap<PersistentString, AnyPersistent>类哈希图对象实施,这意味着我们可使用人类可读的名称存储和提取对象,如以下代码片段所示:

...
PersistentIntArray data = new PersistentIntArray(1024);
ObjectDirectory.put("My_fancy_persistent_array", data);   // no serialization
data.set(0, 123);
...

该代码首先分配大小为 1024 的永久性整数数组。此外,它会将其参考插入名为 "My_fancy_persistent_array"ObjectDirectory。最后,该代码将一个整数写入该数组的第一个位置。因此,如果我们并未将参考插入对象目录,并丢失了对象的最后参考(例如,由于实施 data = null),Java 垃圾回收器 (GC) 会采集对象,并将其内存区域从永久性池中释放(这意味着该对象会永久丢失)。这种情况不会发生在 C/C++ libpmemobj库中;在相似的情况下会发生永久性内存泄露(不过泄露对象可以恢复)。

以下代码片段显示我们可在重启后提取对象:

...
PersistentIntArray old_data = ObjectDirectory.get("My_fancy_persistent_array",
                                                        PersistentIntArray.class);
assert(old_data.get(0) == 123);
...

您可以看到,我们无需对新数组实施实例化。变量old_data直接分配至永久性内存中存储的名为 "My_fancy_persistent_array"的对象。assert()在此处用于确保数组相同。

完整示例

现在,让我们看一个完整示例,了解各个部分如何有序组合(您可下载源代码 from GitHub*)。

import lib.util.persistent.*;

@SuppressWarnings("unchecked")
public class EmployeeList {
        static PersistentArray<Employee> employees;
        public static void main(String[] args) {
                // fetching back main employee list (or creating it if it is not there)
                if (ObjectDirectory.get("employees", PersistentArray.class) == null) {
                        employees = new PersistentArray<Employee>(64);
                        ObjectDirectory.put("employees", employees);
                        System.out.println("Storing objects");
                        // creating objects
                        for (int i = 0; i < 64; i++) {
                                Employee employee = new Employee(i,
                                                   new PersistentString("Fake Name"),
                                                   new PersistentString("Fake Department"));
                                employees.set(i, employee);
                        }
                } else {
                        // reading objects
                        for (int i = 0; i < 64; i++) {
                                assert(employees.get(i).getId() == i);
                        }
                }
        }
}

上述代码列表对应着类别EmployeeList(定义请见EmployeeList.java文件),其包含程序的 main()方法。该方法会尝试获取永久性数组“员工”的参考。如果参考不存在(即返回值为零),大小为 64 的全新PersistentArray对象将被创建,参考会保存在ObjectDirectory中。完成该操作后,数组将包含 64 个员工对象。如果该数组存在,我们会对其重复实施,以确保员工 ID 的数值即为我们之前插入的数值。

有关该代码的一些详情需在此说明。首先,需导入lib.util.persistent.*下的软件包所包含的所需类别。除永久性集合外,PersistentString等永久性内存的基础类别也包含在其中。如果用过 C/C++ 接口,您可能希望了解我们会把池文件的位置及其大小等信息传送至库的什么位置。对于 PCJ,需使用名为 config.properties(需驻留在当前的工作目录上)的配置文件实施该操作。以下示例将池路径设置为/mnt/mem/persistent_heap,并将其大小设置为 2GB(假设永久性内存设备—真实设备或使用 RAM 模拟—安装在 /mnt/mem) 中:

$ cat config.properties
path=/mnt/mem/persistent_heap
size=2147483648
$

如上所述,如果简单的类型(如整数、字符串等)无法满足需求,需要更复杂的类型进行补充,我们可定义自己的永久性类别。本示例中员工就是这样的类别。该类别如下表所示(您可在文件 Employee.java中找到它):

import lib.util.persistent.*;
import lib.util.persistent.types.*;

public final class Employee extends PersistentObject {
        private static final LongField ID = new LongField();
        private static final StringField NAME = new StringField();
        private static final StringField DEPARTMENT = new StringField();
        private static final ObjectType<Employee> TYPE =
                             ObjectType.withFields(Employee.class, ID, NAME, DEPARTMENT);

        public Employee (long id, PersistentString name, PersistentString department) {
                super(TYPE);
                setLongField(ID, id);
                setName(name);
                setDepartment(department);
        }
        private Employee (ObjectPointer<Employee> p) {
                super(p);
        }
        public long getId() {
                return getLongField(ID);
        }
        public PersistentString getName() {
                return getObjectField(NAME);
        }
        public PersistentString getDepartment() {
                return getObjectField(DEPARTMENT);
        }
        public void setName(PersistentString name) {
                setObjectField(NAME, name);
        }
        public void setDepartment(PersistentString department) {
                setObjectField(DEPARTMENT, department);
        }
        public int hashCode() {
                return Long.hashCode(getId());
        }
        public boolean equals(Object obj) {
                if (!(obj instanceof Employee)) return false;

                Employee emp = (Employee)obj;
                return emp.getId() == getId() && emp.getName().equals(getName());
        }
        public String toString() {
                return String.format("Employee(%d, %s)", getId(), getName());
        }
}

乍一看到这个代码,您可能发现它与 Java 中定义的任何普通类别相似。首先,我们有定义为 私有的类别字段(以及 静态常量,更多信息请见下)。包括两个构造器。第一个构造器利用id名称部门等参数构建一个新的永久性对象,需要将其类型定义(ObjectType<Employee>的实例)传送至父类别 PersistentObject(所有自定义类别需要将该类别作为其继承路径中的祖先)。第二个构建器需要通过作为参数传送的另一个员工对象 (p) 进行自我复制,从而构建一个新的永久性对象。在这种情况下,整个对象 p 都可传送至父类别。 最后,我们可获得 getter 和 setter,以及所有其他公共方法。

您还可能注意到字段声明的方式有点奇怪。我们为何没有将 ID 声明为普通的 long?,或将其命名为string??此外,字段为何被声明为静态常量?原因在于它们并非传统字段,而是 元字段。它们只用作 PersistentObject的引导,支持访问永久性内存中的实际偏置字段。将他们声明为静态常量可帮助我们为相同类别的所有对象提供一个元字段副本。

元字段只是未获得 Java 原生支持的永久性对象的伪影。PCJ 使用元字段将永久性对象分布在永久性堆上,并依靠 PMDK 库实施内存分配和事务支持。PMDK 库的原生代码需使用Java 原生接口 (JNI)调用。如需查看该实施堆栈的高级概览,请参见图 1。

Overview of the persistent collections for Java
图 1.面向 Java* (PCJ) 实施堆栈的永久性集合的高级概览。

下面,我将谈论一下事务。借助 PCJ,可通过所提供的存储器方法(如 setLongField()setObjectField())对永久性字段自动实施任何修改。这意味着,如果在字段写入过程中发生电源故障,修改可以恢复(从而避免数据损坏,如长字符串上的残缺写入)。然而,如果要一次为多个字段实施原子性,需要执行明确事务。本文不会详细说明这些事务。如果希望了解事务在 C/C++ 中的处理方式,您可阅读下面的 C 中 pmemobj 事务简介以及C++ 中 pmemobj 事务简介

下面的片段显示了 PCJ 事务的基本特征:

...
Transaction.run(()->{
        // transactional code
});
...

如何运行

如果您从 GitHub中下载了该示例,所提供的 Makefile 可通过各自存储库下载 PCJ 和 PMDK。您只需要系统上安装的 C++ 编译器(当然还有 Java)。然而,我将在这里向您介绍编译和运行永久性内存 Java 程序所需的步骤。如需实施这些操作,您需在系统上安装 PMDK 和 PCJ。

编译 Java 类别时,您需要指定 PCJ 类别路径。如果您在主目录上安装了 PCJ,请执行以下操作:

$ javac -cp .:/home/<username>/pcj/target/classes Employee.java
$ javac -cp .:/home/<username>/pcj/target/classes EmployeeList.java
$

然后,您会看到生成的*.class文件。为了在 EmployeeList.class内运行 main()方法,您需要(再次)传送 PCJ 类别路径。您还需要将java.library.path环境变量设置为用作 PCJ 和 PMDK 间桥接器的已编译原生库的位置:

$ java -cp .:/…/pcj/target/classes -Djava.library.path=/…/pcj/target/cppbuild EmployeeList

总结

在本文中,我介绍了面向永久性内存编程的 Java API。该 API 面向永久性集合,因为集合类别可有效映射至许多永久性内存应用的用例。我演示了如何对永久性集合进行实例化和存储(不进行序列化),并稍后在重启后提取它。本文详细描述了一个完整示例,其包含一个员工永久性数组(从头开始实施的员工自定义永久性类别)。在本文最后,我说明了如何编译和运行使用 PCJ 的 Java 程序。

关于作者

Eduardo Berrocal 于 2017 年 7 月加入英特尔,担任云软件工程师。此前,他在伊利诺斯州芝加哥市的伊利诺理工大学(IIT)获得了计算机科学博士学位。他的博士研究方向主要为(但不限于)数据分析和面向高性能计算的容错。他曾担任过贝尔实验室(诺基亚)的暑期实习生、阿贡国家实验室的研究助理,芝加哥大学的科学程序员和 web 开发人员以及西班牙 CESVIMA 实验室的实习生。

资料来源

  1. 非易失性内存 (NVM) 编程模型 (NPM)
  2. 永久性内存开发套件 (PMDK)
  3. 面向 PMDK 的 Python 绑定
  4. 面向 Java 的永久性集合
  5. 使用永久性内存开发套件 (PMDK) 查找泄露的永久性内存对象
  6. 如何模拟永久性内存
  7. Java 原生接口 (JNI)
  8. pmemobj 简介(第 2 部分) – 事务
  9. 面向 libpmemobj 的 C++ 绑定(第 6 部分) – 事务
  10. GitHub 中示例代码链接

准备就绪,备受关注,大获全胜:独立游戏营销实用指南

$
0
0

英特尔自 20 世纪 70 年代末开始为 PC 游戏社区提供支持,当时的英特尔  8088处理器在 IBM PC上以 4.77 MHz 的速度运行。尽管硬件进步占据了早期的头条版面,大型工作室在行业刊物中拥有绝对的话语权,人们一直对独立游戏开发人员的角色非常感兴趣。最新的想法、最有趣的故事和最具开创性的进步仍出自勇于将自己的愿景推向市场的独立工作室。他们设法平衡新技术的掌握,赢得激烈的市场营销,其复杂度不断增加。

英特尔最新的“准备就绪,备受关注,大获全胜”计划旨在帮助独立游戏开发人员在营销流程的各个阶段获得重要工具、信息和指南。该营销指南为重要客户和小型团队提供了最新内容和可靠资源,帮助他们在动态的游戏市场中获得关注。

本文提到的任何特定游戏、产品或工具均未得到英特尔的认可。

行业现状

根据全球游戏、电子竞技、移动市场等领域的市场情报领先提供商 Newzoo预测,2017 年,全球超过 22 亿的玩家创造了 1089 亿美元(USD)的游戏收入。全球游戏市场为独立开发人员提供了许多极具吸引力的目标。

LAI Global Game Service 报道称,西欧现在凭借 31% 的总销售额成为市场领导者,其每款移动游戏的最高支出为 4.40 美元。北美是发展前景逐渐放缓的第二大市场,预计 MENA(中东、北非和土耳其)将同比增长 21%。亚洲以每年 13% 的速度增长,拉丁美洲 2016 年的增幅为 14%,东欧(尤其是俄罗斯)是另一个重要新兴市场。东南亚等长期被忽视的地区甚至大部分尚未开发。

2017 年年初,Apple 报道称 ,它们的 App Store 在上一年创造了 200 亿美元的收入。2017 年 1 月 1日,它们的单日 2.4 亿美元收入创造了新记录。游戏是最畅销的应用,任天堂的《超级马里奥酷跑》*  在所有应用中位列第一。


图 1.多年后依然备受欢迎,《超级马里奥酷跑》* 是 2016 年下载次数最多的应用。

来自娱乐* 软件协会 2017 年 2 月报告的统计数据显示了振奋人心的前景。“视频游戏行业的发展很可能得益于1独立视频游戏开发人员的崛起,2016 年,他们占全部新增公司的 98.1%;2 940 所美国高等教育机构提供的越来越多的视频游戏研究、课程和计划。”

根据 PCGamesN.com,2017 年,Steam* 将创下发布 5,000 个新品的记录,这意味开发人员拥有巨大的机会。Statista 对游戏行业的数字进行了归纳整理;经过仔细的研究,聪明的独立开发人员会发现几个不断扩大、利润丰厚的专营市场。例如,您是否应该为美国日益增长的老年人口开发产品?根据美国人口调查局的预测,2050 年 65 岁及以上的美国人口将达到 8370 万。开发活跃大脑的益智游戏或营养日记可能很有必要。与之相反,面向中东低龄儿童的游戏可能带来可观的利润。

游戏市场为渴望成功的独立开发人员提供了许多机会。2017 年,Kenneth Tran 在 Gamasutra.com 博文中写道:“独立游戏开发行业目前已近乎完美。”Tran 表示市场“正被数字分发和个人发行所颠覆。每个人都了解 Google Play*、(苹果)App Store、Unity* 个人版Free2Play*崛起的故事。”

Tran 博文的要点是完全竞争的概念。独立游戏开发人员可以创建不可思议的美感,以前所未有的方式进行平等竞争。免费开发套件、大量培训和文档以及厂商提供的多种帮助正逐渐降低进入门槛。如果您将新获得的技能与大量可用的市场研究与数据结合在一起,实现备受关注和大获全胜具有很大的吸引力。请记住,Microsoft Windows* 在全球各个平台推广《我的世界》*之前,它已经是一款非常成功的独立游戏,Markus Notch Persson 因此成为亿万富翁。2016 年,《福布斯》杂志将《我的世界》评选为  史上第二大畅销游戏,已累计发行 1.07 亿份,尽管远低于预计已售 4.95 亿份的俄罗斯方块*


图 2.被微软收购进而走向千家万户之前,《我的世界》* 一开始是一款受欢迎的独立游戏。

营销为什么至关重要

营销被定义为“将货物从生产者转移至消费者所涉及的一系列功能,促销、销售、配送产品或服务的流程或技术。”

有效的营销可以使鲜为人知的邪典电影成为轰动一时的大片。营销包括传播信息、吸引注意力、满足消费者和调动积极性;如果使用得当,它是可衡量、可规划、可重复的。如果您能够熟练使用着色器、物理引擎和编译器,您必定能参与市场营销竞争。准备好投入时间和精力去学习。

游戏设计师 Sarah Woodrow 估计,开发人员仅花费 30% 的工作时间进行编码。“您需要利用剩下的时间去做其他事,尤其当您在孤军奋战时。”如果您能保持精简与灵活,唯一的成功之道便是不断学习与适应。尝试新角度,快速失败,然后继续前进。了解如何开展业务、营销与建立人际关系网,但是准备好花一些时间与资金。

独立开发人员通常需要处理家庭、工作和其他事务,他们在项目中投入的时间已经被挤压。抽时间开展市场推广活动有一定难度。专家表示理解他们的处境,但是强调必须为市场营销预留时间。来自英特尔的 Patrick DeFreitas 是一位合作伙伴营销经理,他与独立游戏社区有过接触。对他而言,“何时开始营销”这一问题的答案显然是:立刻马上!

最近,他解释称:“我从报告中得知,每年有 4,000 多款游戏推出,也就是每天 11 款游戏!”他建议尽早开始推销您的应用。Patrick 解释称:“每天,您的止步不前将使一批竞争者进入和您的领域,吸引相同的消费者,争夺相同的份额。您必须决定在营销中投入的时间。如果您花费‘x’小时开发您的应用,必须花费同样的时间进行营销。”

将营销看作一种与游戏社区开展对话与建立关系的活动。您可能正在进行营销,只不过没有意识到而已。开发人员博客、网站、社交媒体、玩家与游戏开发人员论坛、视频预告片以及其他诸多任务将向大众展示您的游戏开发,以宣传其进度与特性。与潜在客户的每次交谈都是一次营销活动。在这个过程中,您可以通过收集反馈来帮助迭代设计,甚至延长您游戏的有效期。

本指南的“如何着手实现备受关注”部分深入探讨了这些营销概念,展示了有效使用这些概念的策略并讨论了如何避免常见缺陷。

现在是最好的时机

关于什么时候开始思考使您的游戏在市场中脱颖而出,我认为是越早越好。但是等到发布时才开始宣传就太迟了。多数游戏在发布后不久便开始销售,因此,潜在客户需要提前充分了解您的游戏即将上市,这点非常重要。

营销方法根据您所处的开发流程的不同而有所差异。使用表 1 开始创建您的营销策略和活动时间表。

表 1.创建营销策略。

开发阶段活动目标
初步规划创建故事与游戏情节,选择编程语言、游戏引擎、图形和音频工具时,寻找您处理问题的独特之处。稍后在创建您的价值定位时使用它。如果可以的话,确定您的游戏开发流程中有别于其他游戏的元素。您也许灵活多变或在 QA 中严格使用学生志愿者。将它作为应用价值定位的一部分。接下来,创建一个时间表,为营销的每个阶段设置截止日期,从定义和写下您的价值定位,到创建素材、市场推广材料和列出每个开发阶段的所有后续步骤。
素材创建、原型设计分享游戏图形和音频示例的平台包括:
  • 论坛
  • 您的网站与博客
  • 社交媒体网站,如 Facebook*、Twitter*、YouTube*
  • 研讨会*
  • 游戏赛事

当您的原型可随时分享时,为测试用户提供密钥,以便他们试玩游戏。

为测试用户建立一个通信通道。

在游戏活动中担任演讲嘉宾或小组成员。

创建并分享至少一个预告片(或多个预告片),以提高游戏的关注度。

开始借助视频与音频示例传播您的价值定位和早期游戏体验。收集用户反馈并用它改进游戏。

如果您收到了非常积极的社区反馈,鼓励人们开始宣传您的游戏。

鼓励分享游戏视频,以激发人们的兴趣。

与关键影响者建立合作关系,包括媒体,他们的利益与您的相一致。让他们谈论您从事的工作,分享您的图像或声音片段。发布您关于游戏开发的视频或采访。

顺势激发人们对零售和在线分销渠道的兴趣。

完成开发,准备发布

举行比赛与增值促销(向好友分享以获得解锁特性或彩蛋的密码)。

在游戏活动上展出,您可以在展位信息网上购买低成本的展位或与工具厂商建立合作关系,借用他们的部分展位。

如果您无法参展,带上您的宣传材料、游戏副本、游戏激活码、名片、T 恤等出席展会。

继续在博客上更新您的进展,随时随地提供您的网站/博客链接。

提升兴趣与推动销售。

鼓励关键影响者和粉丝讨论游戏并分享他们的体验,以激发其他潜在客户对玩游戏和购买游戏的兴趣。

发布游戏与吸引关注,无需营销

在活动、研讨会和展会上展出与发言。

举办店内促销或在线促销活动,如竞赛、博客和增值促销活动,以在因素消失时提升动量。

使用社交媒体和其他可用的渠道使人们了解在哪里可以试玩与购买您的游戏。

在适当的情况下,定期发布新的关卡或提供解锁菜单和隐藏功能的线索。

在杂志上推广,以及通过博客或或影响者的网站进行线上推广。

保持观众在游戏中的积极参与和兴趣。

帮助人们发现与购买您的游戏。

当游戏的新鲜感逝去后,保持良好的势头并重新点燃人们对游戏的兴趣。

借助特殊的插件在假期进行促销。

如何着手实现备受关注

在初始规划阶段(见表 1),需要考虑您的目标观众以及如何接近他们。在 2013 年 Konsoll 大会上,致力于促进瑞典独立游戏社区发展的著名独立游戏女专家* Emmy Jonassen 谈到了如何以 0 美元预算成功推广您的独立游戏。

她讲述了 Dawn of Play 的《猴子与劳工》*的案例,该游戏一开始销量不佳,后来他们在 Touch Arcade上征集了一篇游戏评论。这篇积极的评论使游戏销量飙升了 6 倍。她还提到了 Hitbox Team,该团队花费 10 万美元创建了 《尘埃克星》*,但是在营销上投入甚少。一位朋友主动帮助他们写新闻稿、创建预告片视频、管理媒体通讯并且在发布游戏前开展营销工作。《尘埃克星》在发布前已经吸引了极大的兴趣,获得了较高的知名度,发表了 100 多篇文章,包括 GameSpot 上的一篇正面文章。他们的投入在 7 天内获得了丰厚的回报,这款游戏迅速实现了盈利。

勇敢迈出第一步。提高您在社交媒体上的曝光率、开通博客、开始与行业内的朋友进行交流以及建立新的联系,这些几乎都是免费的。阅读 Gamasutra 上的  《如何不推广您的独立游戏》,作者是 Frozen District 首席执行官兼首席设计师 Dushan Chaciej,他也是 《术士2:神杀手》*的创作人员。他犯过所有可能的错误。

从何处入手:难以抗拒的宣传材料

创建令人忍不住分享与讨论的难以抗拒的宣传材料。Jonassen 认为,预告片视频、屏幕截图、社交媒体上的曝光率、登录页面和开发博客是最值得您投入时间的地方,预告片尤其重要。

预告片视频

Envato 上的游戏开发教程包括一份独立游戏开发营销清单,作者是以 逻辑书呆子自居的 Robert DellaFave,他创立了四维游戏。他指出,预告片不需要“过于华丽或浮夸,游戏应该为观众留下一个持久的印象”。使用视频捕捉软件和编辑工具创建预告片。客户通过预告片视频了解观众是否喜欢游戏的外观、音乐、艺术、概念和可玩性。游戏记者依赖预告片对杂乱的收件箱进行分类。

Jonassen 以 PC 游戏《墨西哥英雄大混战!》*黄金版的预告片视频为例进行说明。该游戏是 Drink Box 工作室推出的一款横向卷轴射击游戏。该预告片由著名的金牌预告片制作人 Kert Gartner 创作,开始的 3 到 5 秒非常惊艳,插图、动作和音乐为观众带来了欢乐、疯狂的气氛。该预告片时长不到 60 秒,引用了嘉评,以显示它是在获得正面评价之后更新的,结尾处包括行动号召和标识。它是一个有趣、疯狂的预告片,令人感到意犹未尽。


图 3.PC 版《墨西哥英雄大混战!》* 的黄金版预告片能在顷刻间带来欢快、动人和生动的体验,只持续了 59 秒。

虚拟现实(VR)游戏是创建预告片的新挑战,因为您需要在 2D 视频中还原激动人心的 3D 世界。Gartner 正在探索混合现实环境,在绿屏中拍摄玩家,并使用基于人体的预告片增强视觉效果。这些工作会消耗大量时间,播放、录制、合成和流传输需要强大的处理能力,但是全新英特尔®酷睿™ i9-7980XE 至尊版处理器可以从单个系统中完成全部任务。  有关同时处理多个虚拟现实游戏预告片制作任务(之前需要多台电脑)的更多信息,请访问英特尔® 开发人员专区(英特尔® DZ)中的这篇文章

截图

截图是您的装备库中的另一个重要武器。它们应该是具有高分辨率和光线良好的构图。避免黑暗的图像,跳过菜单和界面,除非它们是构图的一部分,以及注重创建的美。选择截图时,选用迷人的场景,如 DellaFave 所说,捕捉“游戏最辉煌的时刻”。让您的观众沉浸在艺术中,激起他们想看更多图像的兴趣,如同《雷提康之谜》*(维也纳 Broken Rules 团队创建)中的该场景。


图 4.《雷提康之谜》的截图显示了梦幻、富有情调的 2D 冒险游戏。

新闻稿

需要立即充满激情地准备新闻稿。请记住,邻近截止日期时,紧张不安的作家喜欢可以复制粘贴的片段。因此,请确保您的文章质量-不要认为它们会在发表前完成 Jonassen 认为第一段至关重要,它必须总结您想要表达的所有要点。将读者置于游戏中,为他们提供玩家视角,这样能提高销量。

情况说明

DellaFave 还建议创建一页情况说明,并附上您的网站、登录页、开发人员博客链接和联系方式、网址、团队历史和成绩以及您开发的其他游戏。读者喜欢引言,因为它增加了文字页面的生机,让人感觉更真实、更容易产生共鸣。尽量在您的情况说明中添加引言。

截图、预告片、新闻稿和情况说明是宣传材料的关键组成部分。访问 presskit()*以获取指导。 presskit() 为新手提供免费资源,以帮助他们获得关注,为其提供加速任务处理的模版。另请参阅《企业家* 杂志》信息图表,以获得及时的书写建议。

登录页

一旦您获得了某人的关注,便可以将其导航至其他地址,出众的登录页可以助您一臂之力。创建一个独一无二的网址,将访客转化为客户,并添加一个识别度较高的游戏购买按钮。离开该页面的唯一导航应为购买流程或提供更多公司信息。该页面应包含截图、嘉评和其他艺术形式。您的登录页应该可以通过 Facebook、Twitter、LinkedIn* 和 Google+* 轻松分享。

开发人员博客

开发人员博客是建立与维系客户联系的另一个关键要素,也是您接近粉丝的最佳方式。Jonassen 表示,相比没有博客的网站,开通开发人员博客的网站提升了 55% 的流量。她建议您每周至少发布一篇博文。

撰稿人 Joshua Becker 在 becomingminimalist.com 上给出了为什么他认为更多人需要发布博文的重要原因。该列表的第 4 点为:“您将拥有一双发现有意义事物的眼睛。”一旦您开始将博客视作每周的任务,您会发现自己开始记下读者可能喜欢读的观点。您将很快发现有说不完的事情。由于这个充满活力的行业正在不断演变,有趣、创新的人无处不在,因此,您可以通过博客与他们保持联系,以培养与他们的关系,并建立新的关系。

优秀的开发人员博客应包括引人注目的设计,为订阅者提供简单的 RSS 源、电子邮件订阅或社会订阅选项。在每篇博文中,您都会拉响集结的号角,粉丝(和媒体)会应声而来。添加一个下载按钮或链接,以方便用户提取演示。如果您已经进入了修改阶段,并且想激发人们的兴趣,可以插入真正的游戏。


图 5.来自 Drink Box 工作室的此类开发人员博客对引导流量和不断吸引客户至关重要。请注意右侧的便捷订阅按钮。

与媒体接洽

有条理地联系行业媒体,邀请他们正式评论您的游戏。评论界存在一种从众心理;您只要获得了一条评论,便成为群体的一员,如果您坚持联系媒体,将获得更多的评论。您可以从 GameSpotGamasutraVentureBeatIndieGamesGamesIndustry.bizPolygon等网站上寻找评论人。

生成媒体报道和跟踪编译器漏洞一样,两者均需要有序地进行。首先创建一份媒体联系人列表,然后将其扩展为电子数据表,以便您追踪发送的内容、时间、得到的回复等。为了创建列表,您需要在会议上收集名片、获得网站署名的联系方式并且花时间浏览网站中的姓名。广结人脉也有帮助 - 与其他独立开发人员分享消息,始终关注新技术。行业内的人员流动从未间断,因此,需要花时间维护媒体联系人列表,这样才能确保它具有极高的价值。

当您已经准备好宣传材料,并且去除了所有错误,可以通过社交媒体和电子邮件联系游戏评论人。创建一个模板,以进行自我介绍、团队和游戏介绍,确保为每位评论人提供个性化的信息。回答以下问题 - “该评论家为什么要关注该游戏?”您的宣传材料需要具有吸引力、清晰并且简洁。这是您的背景介绍,凸显了您工作的意义;请耐心地对它进行润色。

跟进

坚持对媒体联系人和评论人进行跟进,尤其在您有所突破,获得了梦寐以求的知名度时。Jonassen 举例称她在评论发布后跟进并感谢了评论人的关注。她收到了回复、建立了友谊并且扩大了她的社交圈。该联系人现在重新发布了她的新闻稿,她的信息必定会传递下去。她和该联系人保持联系,并且认真地评论对方发表的新文章。当她在游戏展或会议上看到联系人时,她感觉他们的友谊更真诚了。

您的营销活动目标非常简单 - 创建与维护粉丝群。使用社交媒体发布更新内容有助于人们轻松地发现您的游戏。如果条件允许的话,每天在社交媒体上发布更新,即便您只是转发一些文章。将它视作营销工作中提升存在感的手段。在论坛和博客中踊跃发言,并参加游戏制作节和其他活动。出席当地的游戏开发社区活动。他们急需帮助并且对新、老面孔热情欢迎。发起众筹不仅能筹集资金,还能帮助您改进网上形象。

将访客转化为活跃粉丝

当您创建与更新促销材料以及定期发布博文时,会将不速之客转化为付费客户。您甚至可以将客户升级为活跃粉丝。定期推送关于游戏和公司的消息对于您的发展至关重要。通过个性化服务和快速回答问题来扩大您的粉丝基础。但是不要只发布与回复 - 通过发起对话来引领话题。在某些决策上征求粉丝的建议,使粉丝感到参与了该过程。面对质疑与评论时,请记得保持专业素养。只需解释您做决定的原因,不要火上浇油,卷入长达两周的激烈论战。

Jonassen 表示,Sauropod 工作室的加拿大团队开发了《城堡故事》*,只获得了一些礼貌性的关注,销量并不高。他们没有在销售材料上投入太多精力,他们的第一个演示太短了。但是在他们讲述了自己的故事并录制了 11 分钟的视频后,他们登上了 Reddit的版面。在一个小时内,他们在游戏社区的关注人数暴增,这些关注最终均转化为销售业绩。

类似《城堡故事》的入门宣传材料示例在互联网随处可见。如果您知道某个游戏在这方面做得很好,访问它们的网站并下载它们的材料,您将由此了解自己需要什么。

维持您的营销势头

一旦拥有了可播放的演示,您便可以作为参展商或厂商随时登台亮相。您也可以创建自己的活动。例如,Inc. Magazine上的一篇入门指南就如何筹划您自己的活动提供了 8 条策略。该文章介绍了如何使用 GPS 跟踪、添加增强现实、管理社交媒体等。某些建议似乎很烧钱,但是许多示例不需要较高的预算。关键是通过您的 Twitter 帐号、Facebook 主页和博文不断地宣传与更新,坚守社交媒体这块阵地。

竞赛是推出增值促销活动的另一种经济方法。在重大节日期间插入游戏彩蛋,或者融入近期的活动。向您的客户寻求帮助,您可以提议,如果他们联系了一位好友,将获得一个密码。某些游戏通过这种方式取得了巨大的成功,如King.com(总部位于英国)创作的《糖果粉碎传奇》*。2013 年,该游戏的下载量达到 5 亿次,平均每日用户数量为 670 万,仅 iOS 应用市场一家的日均收入便高达 633,000 美元。2015 年,Activision 以 59 亿美元的价格收购了 King。

播客和采访是传播消息的另一种方式。面对优秀的采访人,您可以谈论您的理念、激情和目标。视频形式的采访更易分享与发布。切记,如果您心情不佳或者您的采访人准备不充分,结果将一直保存。因此,有备而来,随机应变,最好能提前知道您的问题。

需要避免的常见错误与缺陷

开展营销活动时,请牢记以下要点:

  • 建立并执行营销活动安排表。不要半途而废。
  • 计划在营销活动中投入至少和开发一样多的时间和精力。某些专家建议开发和营销分别占 1/3 和 2/3。
  • 分享信息时要小心谨慎 - 不要泄漏您的秘密武器。此外,如果您过早地公布您的计划,将引发轰动,但是您无法快速满足公众的期待,因此,您将失去他们的关注。
  • 在您的博客和社交媒体中使用公众代言人,与观众平等交流,而不是把他们当作您的跟班。保持幽默感、谦卑,反思您的方法。
  • 清楚影响者和评论人的定位。例如,如果您创建了一个 PC 游戏,不要联系仅专注于移动游戏的个人或出版社。
  • 讲述游戏的故事,将重点放在价值定位上。不断宣传使您的游戏与众不同的要素,无论是艺术、设计、故事、音乐还是智慧。Entrepreneur.com 提供了一个关于确立价值定位的重要教程。当然,在这个过程中,注意不要过分吹嘘。让其他人就游戏质量得出自己的结论。

什么使您与众不同?

营销策略的第一步是明确您的价值定位。如果您了解游戏的独特之处以及产品的目标市场,您已经处于领先地位。回忆一下您的第一轮首头脑风暴会议,记住是什么激励您设计这款游戏。如果您当时的想法是“从未有过这样的游戏”或者“没有人这样做过”,您拥有了一个独一无二的故事开头。使用使您脱颖而出、与众不同的概念创建公司口号,以营销活动为重点。

为了充实您的想法,请执行 sitepoint.com 中的步骤,作者是 Small Business Bonfire(面向企业家的社交、教育和协作社区)的创始人 Alyssa Gregory。

  1. 描述您的目标观众。他们使用 PC、智能手机还是平板电脑?他们属于哪个年龄段?他们喜欢竞技、技术还是只喜欢快速解题或游戏?
  2. 解释您正在解决的问题。世界为什么需要另一款射击游戏?您的益智游戏有什么独特之处?
  3. 列出重要优势。他们会感到愉悦、困惑、充满挑战还是比较满意?
  4. 定义您的承诺。您是否立志打造最有趣的游戏,拥有最具吸引力的主题或最唯美的艺术形式,或者忠于您的信念?对任何公司而言,拥有凝聚所有人的共同承诺是一个重大优势。
  5. 请将您在前 4 步中得出的见解整理为包含 3 到 4 个完整句子的段落,不少于 60 字。
  6. 进行大篇幅的修改,删掉杂乱和突兀的语言。润色标语,直到它变得令人难忘、朗朗上口,并且整个团队认同标语中的每个字。

以商界广为流传的几个价值定位为例:

  • 快、快、快速见效。– Anacin*
  • 只溶在口,不溶在手。– M&M* 糖果
  • 临床证明可有效去屑。– 海飞丝&*
  • 30 分钟内为您送达热气腾腾的现烤披萨,超时免单。– 达美乐* 披萨
  • 保证隔天送到。– 联邦快递*
  • 找大都会,它会理赔。– 大都会人寿*
  • 我们是低成本航空公司。– 西南航空*

Convince & Convert 列出了明确价值定位需要考虑的事项:

  1. 大方地迎合您的理想客户。例如,Abercrombie & Fitch* 表示它们的理想客户是又酷又好看的人。他们深耕细分市场,而不是吸引大众。
  2. 任用特别的人。如果您有一位极具个人魅力和辨识度的领导,任用他/她!
  3. 避免卷入巨星的激烈竞争。不要努力成为最好的 - 以独特的方式脱颖而出。

人口统计

为了创建价值定位,您必须了解您的目标受众:他们的年龄层次、性别、他们来自某个区域还是分布于全球以及他们的兴趣是什么、有怎样的购物习惯、什么会让他们心动?

Joel Julkunen 在 GameRefinery发表了一篇关于目标受众和竞争对手的文章。作为 GameRefinery 分析部门的领导,他创建了算法和统计模型,通过拆分数据使人们理解数据。他了解游戏开发人员面临的营销挑战。“合理的策略旨在制作一款吸引您的目标受众的游戏,同时使其在众多类似游戏中脱颖而出。”如果您正在编写一款角色扮演游戏(RPG),需要吸引传统的 RPG 发烧友。同时,您必须提供差异化内容,否则您的游戏将平淡无奇。

Julkunen 建议在矩阵中绘制游戏,以确定它位于哪个认知和空间范围。在横轴上,准确定位您的游戏在迅速反应与正确反应以及简单和复杂思维之间的位置。想一想您的游戏需要传授的技能。策略游戏强调战术意识、解谜和模式识别,注重玩家的认知技能。速度并不重要,但是逻辑能力和系统方法至关重要。射击游戏挑战玩家的感知和运动技能,如速度、瞄准和反应。在该轴的身体(感知运动)和心智(认知)之间标出游戏的位置。


图 6.Julkunnen 使用简单的 2x2 矩阵显示游戏在心智与身体以及复杂与简单轴上的位置。即时战略(RTS)游戏《沙丘》* 位于 X 轴的正中央,Y 轴的顶部(资料来源:GameRefinery.com)。

纵轴用于区分核心层和模型复杂性。Julkunen 介绍称,一维游戏比较简单,因为它们围绕一个核心层展开,如反复解开打乱的字谜。《部落冲突》*等游戏处于该维度的另一端,《部落冲突》是一款需要通过多元思维进行规划、资产优化和资源分配的移动战略游戏。例如,Cryo(总部位于巴黎)1992 年创建的即时战略游戏《沙丘》*是一个超级复杂的挑战。为了获胜,玩家必须保持攻守均衡、建造建筑物或武器、计划袭击、节约资源以及提防零散的沙虫。需要大量的点击,但是不太需要瞄准。您现在有一个 2x2 矩阵,用于标注游戏在身体与心智以及简单与复杂轴上的位置。


图 7.在《愤怒的小鸟》*中,玩家不仅需要完成体力任务,如拉紧弹弓上的橡皮带,还要计算爆炸如何破坏建筑以及赶走讨厌的小猪。该游戏包括多层挑战的身体与心智元素。

完成矩阵后,基于您对您所熟悉的游戏的假设构建,并确定吸引哪种类型的玩家。还要考虑您对畅销游戏的了解,并确定您是否有一个简单的故事,以吸引同类畅销游戏的买家。研究成功的特许经营游戏,以深入了解它们的魅力、方法、营销、推广和其他任务,绝佳示例无处不在。

角色:神话原型

在开发过程中,创建一个为您提供引导的角色,即神话原型消费者。一个角色代表一组重要的行为模式,可以按照购物习惯、技术采用、生活方式选择、服务偏好和其他行为、态度和动机进行划分。确定这些模式时,您可以创建一个一般性角色,以代表整个类别。

在 Gurusability 博客上,Papa_Lamp 讨论了游戏世界中某些整体角色的主题。他引用了 Flavio Camasco 在 Gamasutra 上发表的一篇文章,论述了硬核玩家和随兴玩家的差异,他表示沉迷于《Journey》*的玩家和经常玩《铲子骑士》* 的玩家以及长时间在 《星露谷物语》*上管理农场的玩家一样硬核。他表示,您也可以仅根据玩家在游戏上投入的时间进行区分。

Papa_Lamp 随后描述了如何收集指标,以确定玩家的行为。他讨论了安大略理工大学 Lennart Nacke的工作,Lennart Nacke 在 2009 年加拿大游戏开发者大会(GDC-C)上发表了一篇 演讲。Nacke 提倡使用游戏指标来帮助确定与构建角色,虽然数据难以追踪,但是这个假设是正确的。Nacke 建议混合定性和定量指标,并描述了如何将它们输入更大的图像,为游戏设计提供信息。

UXMag用户洞察总裁 Kevin O’Connor 将角色称作“卓越用户体验的基础”,并表示角色应适用于任何年龄、性别和教育背景。

O’Connor 建议至少与 30 人进行一对一访谈,然后研究结果以观察模式的演变。他还建议在环境中进行采访,如玩家的游戏场所,这样能确保不遗漏任何环境线索。这种专门研究约花费 35,000 美元,耗时 3 到 6 个月,相当于一名独立开发人员的几个生命周期。您只能用自己的洞察和案例取代正式报告,但是了解基础科学非常重要。

您可以使用 UpCloseAndPersona.com and ImFORZA等在线工具创建角色。工具的优劣取决于您创建它们所用的假设,但是它们只是一个开始。您可以基于虚构的名字、一系列假定的背景信息和简单的目标陈述创建简单的角色。例如,Andre 是一位空闲时间充裕的法国时尚达人,想以具有挑战性的赛车游戏作为消遣方式。。或者 Tomoko 是一位繁忙的中年东京女性,她需要在乘坐短途列车时玩简单的游戏或益智游戏

如果您全面地接触过游戏社区,可能已经对理想客户群有了充分的认识。基于收到的反馈,您可能已经知道哪些人是您的目标受众。显然,在识别角色特征方面,您花费的时间和资金越多越好。

一旦您开始销售您的游戏,您可以通过快速调查轻松地回答客户的问题。SurveyMonkey*PollEverywhereTypeformSoGoSurvey等网站专门协助您提出问题与收集答案,以助力您实现大获全胜。

竞争分析

竞争分析是另一种关键营销工具,它是对您的业务战略和竞争关系的笼统陈述。您需要尽可能多地了解与您在同一领域内竞争的企业,越多越好。根据 Entrepreneur.com,如果您可以对竞争对手了如指掌,您将掌握他们的优势和缺点。他们写道:“通过该评估,您可以明确您的产品和服务的独特之处,从而有针对性地突出某些特征,以吸引您的目标受众。”

Inc.com 建议您提出有关竞争对手的下列问题,这些问题适用于游戏行业:

  • 他们的优势是什么? 美术、主题曲、可玩性、可扩展性、社区的支持和建立的形象全部都有可能是您的薄弱之处。
  • 您可以利用哪些缺陷?他们也许人手不足、工作过度、资金短缺或已经超出了目标期限。他们可能音乐简单、艺术乏味,但是他们的人工智能无人能敌。
  • 他们的基本目标是什么? 他们是否试图提高市场份额?他们是否尝试征服至尊客户?通过他们的视角观察您的行业。他们致力于实现什么目标?
  • 他们使用了什么营销策略? 了解他们的广告、公共关系等。
  • 您如何从他们手中争夺市场份额?
  • 您进入市场后,他们将有何反应?

此时,您需要放下您的开发人员身份,进入商务人士这个角色。将这个过程想象成一个需要快速思考、长远策略和敏捷反应的益智游戏。将它变为一种乐趣!

从竞争对手的网站上收集关于他们的信息,如他们的团队规模和专业知识。如果您尚不清楚您的竞争对手是谁,可以联系贸易展参展商、阅读社区公告栏、研究游戏活动或询问销售人员。创建一个电子表格或网格,以方便收集信息。您可能不知道竞争对手的年销售额,但是您可以在开始时用高、中、低来描述。

创建网格时,尝试追查以下关键因素:

  • 列出与您的游戏相似的游戏。
  • 估算它们的定价模式。
  • 判断他们的发布地址。
  • 确定团队规模。
  • 分析其优势和劣势。
  • 在地图上对其进行定位。
  • 猜测他们的声誉优势。
  • 衡量他们开发您的游戏类型的决心。
  • 将他们的威胁等级划分为高、中或 低级。

网格创建完成后,调整您的假设,继续收集信息与提问。您的未来取决于您对竞争对手的了解以及您能以多快的速度上市。例如,了解 MVP(最小可行产品)的概念。在 Agile Alliance,该术语表示为了吸引指导性反馈,演示中应存在的附加程序数量。如果您依照该概念推断竞争分析,便可以确定您是否可以安全地缩减关卡、复杂性、角色或结构。如果您的竞争对手没有提供 50 多个关卡、25 件武器或 12 个角色选择,您可能也不需要通过它们来发布您的游戏。

与您能接触的任何人交流。如果您在活动中发现一位竞争对手,尽可能亲自问他们几个问题。谁知道呢 - 您可能因此结交一位朋友,未来他可能成为您的合作伙伴!毕竟,Digi-Capital* 的报告显示,2016 年游戏行业的兼并与收购已经打破了记录。

策略与目标

是否有能力设置现实、可实现的目标和策略对于实现大获全胜至关重要。首先把它们写下来。Jawfish Games 创意总监 Tadhg Kelly 在 GamesBrief 上的文章中探讨了独立开发人员最大的营销误区,这是我见过最好的论述。他的回答非常简洁:“制作一款没有营销故事的游戏。”在同一篇文章中,Applifier 宣传官 Oscar Clark 表示,团队应该经常问“那又怎样”。换言之,就算您正在制作一款绝佳的(某类型)游戏,那又怎样?一个营销故事就足够了吗?

该团队在 GameSparks.com 上专门针对游戏营销发表了一篇 博客。他们建议采用两种方法来指导您的营销工作:营销策略和营销计划。策略指导整体目标,而战术帮助您实现目标。

表 2 列举了 GamesBrief 上公认的优秀营销策略的关键。

表 2.优秀营销策略的关键。

要素描述
GaaP 对比 GaaS您提供了产品还是服务?您会频繁更新令用户激动不已的新插件吗?或者您在发布一款产品后,会投身于下一个项目吗?
业务模式您是收取一次性费用,还是免费开发游戏并通过应用内项目收取现金?
目标受众根据您的目标定义您的分销和营销选择,而不是反其道行之。
平台和应用商店除非您计划在多个平台上发布游戏,否则,您对于游戏机与 PC、智能手机与平板电脑以及 Xbox* 与任天堂的选择对于预判非常重要。
地区您打算全部发布还是在某个区域发布?您需要翻译服务吗,或者您想限定在某个地区?屏幕上的单词越少,需要翻译的内容就越少,因此,上述问题可以指导您制定早期设计决策。
预算尽管您的预算很少,但是您有自由的时间。时刻关注资金和时间的去向。并考虑放弃准则,即停止在项目上花费任何精力。
营销渠道活动、评论、广告、发布会、博文、社交媒体等渠道可以帮助您宣传。哪种方式适合您?
测量您如何收集统计数据,以更好地分配有限的资源?

更多信息敬请参阅  《如何发布游戏》(作者:Nicholas Lovell)。该文章提供了丰富的建议、工具和策略。Black Shell Media 也给出了相同的建议并提供了一整套营销解决方案。Peter Zackariasson 和 Timothy L. Wilson 发表于 academia.edu 的学术论文进行了更深入的探讨。

整体策略就位后(或者至少正在形成中),建立针对每个部分的具体策略。重要信息详见以下博文与文章: 

  • 来自创新型游击营销的成功视频游戏营销案例
  • Mike Templeman 在 entrepreneur.com 上讨论了利用《口袋妖怪 Go》*的具体策略。
  • 在 Strength in Business 网站上阅读以了解 Xbox One* 与 Sony PS4* 之间的竞争
  • David Murdio 的文章提供了面向视频游戏的数字营销建议,这是继视频与社交媒体营销建议文章的后续作品。 

您的营销计划包含您打算使用的策略与战术。美国拳击手迈克•泰森通常在比赛中早早击倒对手。当被问到进入拳击场后,在多大程度上实施了自己的计划时,他回答道:“每个人在脸被击中前都有自己的计划。”请记住他说的话。准备好适应不断变化的市场环境 - 保持灵活至关重要。

营销目标

管理大师 Peter Drucker 讲过一句名言:“您无法管理不可测量的事物。”他的观点很简单 - 如果您在修改前没有掌握任何统计数据,便不会知道您的改动将对结果造成多大的影响。软件工程师熟悉一次只改变一个因素的原则,这样他们便可以查看测量仪器的指针是否有移动。如果您采用了突击销售法,并且同时尝试几个策略,可能无法了解哪个策略产生了重大影响。

对于独立开发人员,这个原则可能难以遵循,因为您通常没有时间尝试一种策略并测量结果。您做的就是挖掘记录单个更改的影响的统计数据。例如,收集谷歌的网站流量统计数据,然后更频繁地发表博文,以测量变化。研究您的 Twitter 和 Facebook 粉丝数量的变化,并确定您发布一个视频和简单评论时的变化速度。

有时您的数据采集仅显示“流量增长了”。您可以继续深入研究,测量前后对比结果。然后回顾您的战术与目标,为自己布置一个具体的任务,如“我想在接下来的 2 个月内提升 10% 的流量。”这是一个需要集中精力去执行的目标。

销售线索生成

销售线索生成决定销售团队的存亡。由于引导游戏的销售工作是您的分内职责,您必须了解这个术语。销售线索生成指的是列出一张姓名清单,然后尽力将清单上的人转变为销售对象。

如果您在网上搜索该问题,将看到 HubspotInfusionSoft*ThriveHiveLynda.com*Salesforce给出了长篇建议。多数网站旨在销售服务,UnbounceMarketJSDuctTapeMarketing等网站将帮您完成游戏制作任务,或者提供建议。

您可以通过以下方式着手生成销售线索:

  • 创建一个全新的演示视频,然后进行广泛宣传。
  • 检查您的网站,以确保哪怕您像沙尘暴中的牛仔一样眯着眼看行动号召,它仍然非常显眼。如果您看不到它,解决这个问题!
  • 遵守网站设计的席克定律 - 为网站访客提供更少的选择,而不是更多的选择。最好能突出重点。
  • 捕获电子邮箱地址,以换取内容。
  • 使用 FollowerWonk等服务确定 Twitter 上的线索。
  • 尝试 Quora 等工具,使用以是/否为答案的问题来跟踪链接。访问该案例研究的链接,以了解如何构建可以转换的连接。
  • 将演示幻灯片发布到 SlideShare 等网站。案例研究显示,SlideShare 拥有 7 千万访客,而且该网站会令人上瘾。请务必添加一个返回您的登录页的链接,以为您的读者提供更多信息。
  • 在活动上畅所欲言。寻求分享您的开发经历的机会,并习惯于谦虚地标榜自己,宣传您的信誉,并公开感谢默默付出的另一半。
  • 更新您的电子邮件签名。确保它包含您的联系方式和标识。如果您最近赢得了比赛或获得了褒奖,请更新您的签名栏。
  • 尝试租用电子邮件列表。LaunchBit 等网站是一个良好的 开端。 

所有这些提示都有一个目标:构建更好的销售线索列表。最终目标是将这些销售线索转化为销售。将您的工作设想为开始对话。您的工作是不断创造新的分享内容、新对话以及与不断增长的粉丝群进行互动的新方式。建立口碑是一个缓慢的过程,就像照料花园一样。

在 Gamasutra 的一项事后检验工作中,Rob de Lara 描述了他在按时完成 NyxQuest: Kindred Spirits*(Wii World* 一款屡获殊荣的动作类平台游戏)方面的一个问题。。他表示:“我知道 many hats问题是独立开发人员的一个常见错误,但它让我感到完全措手不及。我没有想到电子游戏的管理、文案和 PR 要求需要占用如此多的时间。我们不得不(并且仍然)花费大量时间撰写电子邮件,请求审查,准备预告片和截图并回答采访。几个月后,我们觉得还有很多人没有听过 NyxQuest。有些杂志将我们的游戏提名为“Best Sleeper Hit”,这是有原因的。希望我们能够解决这个问题,并打造出更受欢迎的游戏。我们想创建一个博客、开发日记和其他媒体内容,但由于工作量很大,我们不得不留到将来进行。总结经验:PR 是一个需要投入大量时间的领域。投入的时间越多,对游戏的了解越深。

如果您要走向世界,请根据各个地区定制信息。这显然需要花费更多的时间和精力,但使用 统一的信息可能会影响您建立关系。同样,如果您收集的统计数据似乎指向某个方向,请遵循那个方向。在一个地区建立真正的口碑可帮助您的游戏在其他地区大获成功。但这需要一点火花。

创建品牌

您的品牌概括了您的吸引力、定位、形象、态度和设计变更,全部体现在一份微妙的声明中。Inc.杂志指出,知名品牌都是在相同的领域表现出色

  1. 专注于单个品牌。
  2. 抢先获得一个好域名。
  3. 保持简单。
  4. 在描述性、引起共鸣和异想天开之间选择一个。
  5. 避免委员会进行品牌推广。
  6. 始终如一地应用品牌。
  7. 保护品牌

福布斯拥有一份创建绝佳品牌的检查清单,Branding Strategy Insider 也是如此。参见 Strategic Thunder 的 的问题列表来回答;Brand Butterfly 也有一些项目需要考虑。无论您提出哪些内容,都需要认真地维护身份,以建立您的企业形象。品牌应该遍布您的网站、名片、登录页面、联系页面、下载页面、开发博客和其他营销材料。贵公司的方方面面(包括音频和视频)都需要一致且适当的品牌标记。

我们已经撰写了关于主要品牌错误的多本书籍和文章;Entrepreneur.com、Precision Intermedia、All BusinessInc.只是其中几个例子。阅读这些例子,并类推到独立游戏开发领域。例如,Xerox 是一个全球性的复印品牌,但该公司曾尝试停止使用 Xerox作为替代品。Esurance 曾屈服于一些评论家的批评,在他们心爱的吉祥物 Erin Esurance大受欢迎之时停止对其的使用。高露洁曾认为公司可以从牙膏转向包装食品,尽管两者几乎没有联系。汉堡王*的诡异吉祥物 “国王”曾被视为制胜法宝,但事实并非如此。雪佛兰的一位高管曾要求员工放弃使用简短用语并使用整个词——雪佛兰。

低预算工作

独立预算很少。尽管如此,您需要开始制定预算并跟踪成本和费用,因为当您开发第二个游戏时,你可以参考这些成本。

制定预算不仅仅是打如意算盘。随着您逐渐成为商业领袖,您必须熟悉投资回报 (ROI)、风险回报和性价比等术语。如果您不了解成本,则无法计算数字。

投资回报是试图利用数据指导决策。如果您想要在生产力工具上投资 100 美元,那么最好投资回报至少为 101 美元。通过升级到 Unity* Pro或为您的主系统购买更多内存,您可以获得更高的回报。计算投资回报的关键不在于数字,而在于如何用数字衡量难以量化的事物。

例如,请代理商创建品牌的预期收益是什么?假设成本为 8000 美元。将任务交给供应商而非队友的好处是什么?您如何衡量预期(或期望)结果?用美元数字衡量价值,不必独立完成项目,这并不简单,但绝对值得。

图 9.计算投资回报需要
牢牢掌握投入和结果。

您可以在网上的许多地方找到投资回报计算器,如Financial CalculatorsEasy CalculationMoney-Zine*等。

成本收益分析

成本收益分析首先需要以一种系统化的数据驱动型方法分析您将时间和精力投入到哪些方面。您可以在The BalanceMind Tools*ChronInvestopedia*中找到多种在线工具和成本收益分析说明。当您在处理学校、工作、人际关系和身体健康问题时,分配时间和实现高效的日常工作似乎很难,但这有助于制定计划。在这份计划中,请对独立项目的预算进行细化。不要只是简单地分配每周 10 小时进行游戏开发;将其进一步细化,以便您的营销工作不会随着工作的推进而被忽略。

通过 Tesla* SpaceX*等企业改变世界的著名南非企业家 Elon Musk 曾高调声明,他没有读过任何关于时间管理的书籍。但他有自己的时间管理方式。“有一个反馈循环非常重要,您需要不断思考自己所做的事情以及如何做得更好。最重要的建议是:不断思考如何更好地开展工作并质疑自己。”

如果您有足够的数据来计算某个决策的投资回报率,例如拓展到多个地区,则您遥遥领先于大多数独立开发者。更常见的情况是,游戏开发者根据直觉制定投资回报决策,这充其量不过是漫无目的的决策罢了。好消息是,如果你至少尝试一下计算投资回报率和成本收益分析,就会给投资者留下更深刻的印象。这样可以使探索数据驱动型营销变得更加重要。

指标:我们信任数据

思考这幅Beat Hazard*的游戏销售图,它是一款来自Cold Beam Games的独立游戏。这款银河街机射击游戏支持根据玩家所选音乐的节拍进行设置,总销售额达到 200 万美元。


图 10.Beat Hazard *的销售图,Y 轴显示销售额,X 轴显示时间。假日营销违背了大多数游戏的典型初期爆发/长尾模式(来源:ColdBeamGames.com)。

大多数游戏都是在开始时出现高峰,并随着时间的推移逐渐退热,Beat Hazard 的发布很有代表性。这款特定游戏的优势在于围绕假日主题在游戏中引入了新内容,并通过更新的游戏方式产生了新的高峰。

出色的独立开发者都热衷于收集自己能找到的所有东西的指标。思考一下您可以跟踪的所有业务方面:

  • 社交媒体上的口碑以及评论、下载和访客数量
  • YouTube 和 Facebook 上的点赞数量
  • Twitter、Facebook 和 Google+ 上的粉丝数量
  • 受影响的市场影响者数量

德鲁克有一句名言,如果无法衡量,就无法管理。您需要收集所有关键营销任务的指标。

另外,您必须识别某个想法何时失效。如果您的营销目标是将新视频的流量提高 10%,而且几乎没有任何障碍,那么视频、分发、时间或其他因素就有问题。也许问题就在访客评论中。再次尝试使用不同且全新的新视频。

在 Developer.com 上,工作人员写了一篇引人入胜的文章,标题为“我宁愿编码:收集指标。它解释了为何收集指标对于营销人员和项目经理同样重要。

分析

Google Analytics* 服务可为如今的独立开发人员提供显著优势。查看 Google 在其 网站上编译的成功案例,或阅读其中一些教程。

Gamasutra 有一篇 由 Nemanja Bondzulic 撰写的文章,主题是“游戏中的 Google Analytics 分析”。他们跟踪了用户与在线街机空间射击游戏 SUPERVERSE*的交互方式。他们需要了解用户玩这款游戏最常用的硬件配置。他们通过跟踪使用情况来收集信息,这对他们的未来规划很有帮助。


图 11.Google Analytics 服务可提供关于用户活动的重要信息(资料来源:GamaSutra.com)。

收集分析时要避免的一些陷阱包括:

  • 信息由于外部事件、游戏中的变化或其他变量而过时。
  • 统计信息的单一来源,并且始终需要确认您收集的数据。
  • 根据一个地区的统计资料得出的结论可能不适用于其他地区,因此与所有测量一样,需要使用一些判断。

福布斯福布斯发布了一篇有趣的文章,文章中介绍了 2017 年最有效的 12 种 SEO 策略。在文章中,John Rampton 将内容长度作为位置结果排名的关键动力。例如,在撰写博客帖子时,避免过早停止的倾向。Rampton 表示:“实际上迄今为止所做的每项研究都显示了较长内容和较高排名之间的关联。有些人建议 1,200–1,300 字,而另一些则认为最少 1,500 字。如果您想让自己的内容排名靠前,那么标准博客帖子的最小长度为 1200 字,[无时间限制]内容为 2000 字。

随着您获得营销专业知识,熟悉一下搜索引擎优化 (SEO)。当消费者搜索与您的游戏相似的游戏时,请确保您的游戏显示在结果列表中。该网站包含有关这一主题的大量信息,一篇来自 Moz的文章介绍了一个八步骤流程,对您实现目标特别有用。

营销渠道

营销渠道是指商品和服务流向消费者的方式。例如,游戏可以通过自己的网站直接从开发人员转移到客户手中。或者零售商可以参与销售顶级盒装游戏。

您选择的分销渠道取决于您的工作意愿有多强。如果您直接通过您的网站收取现金,您将成为 PayPal 专家,您将生成独特的产品密钥,并且您将追踪帐户。这种方法可能会非常耗时。

渠道营销包括数字营销、直销、电子邮件营销等。趋势千变万化,因此您需要收集指标来确定电子邮件营销活动是否比横幅广告更适合您。Andrew Medal 在 Entrepreneur.com 发布的 article 2017 年文章指出,多达 60% 的横幅广告是误点击的。约有 91% 的广告观看时间不到一秒。很显然,这些指标不再适用于横幅广告。

到目前为止,独立游戏最常见的营销渠道是下载网站。消费者可以从充当批发商的多家提供商下载独立游戏,如 Xbox Games StoreMicrosoftSteamGameJoltIndieDBEpicBundle。虽然您可能会通过更大的网站损失一定比例的销售收入,但您可以从更高的曝光率和流量中获益。

展会与活动

游戏制作节 (Game jam)、贸易展览和游戏活动是集中开展营销工作的绝佳地方。您的预算可能不允许经常出差,但对于刚刚起步的许多独立开发人员而言,睡在朋友家沙发上和拼车仍然很常见。虽然您可能无法提供 T 恤、钥匙圈或 U 盘,但您可以在展会和活动中随处走动,看看其他人在做什么。

一些最著名的独立开发者活动包括电子娱乐博览会 (E3)、游戏开发者大会 (GDC)、独立游戏节 (IGF) 和 PAX。如欲查看不断更新的 list行业活动列表(涵盖电子竞技和休闲娱乐),请访问 gamesindustry.biz

参加小组讨论或提供有关您案例的幻灯片是获得认可的绝佳方式。您会发现,在谈论您最喜欢的话题时,公开演讲并不那么难。准备在您的演讲中提供建议和鼓励,并通过您的所有社交媒体渠道宣传您的演示。

游戏制作节和聚会

游戏制作节是指各种正式或非正式的聚会,目的是在短时间内规划、设计和创建一个或多个游戏,时间通常在 24 到 72 小时之间。参与者包括程序员、设计师、艺术家、作家和粉丝。游戏制作节可能是令人兴奋和激动的,也可能是令人疲惫不堪的。这些聚会是结识其他独立开发人员的好机会,但参加这样的聚会可能会让您筋疲力尽。

PixelProspector 提供完整的 游戏制作节列表,就像does维基百科一样。以下是advice from BáiYù给出的一些建议:

  1. 避免危机和截止日期压力——加快步伐并了解自己的极限。
  2. 了解项目范围;不要制定要求过高的计划。/li>
  3. 做好最坏的打算。如果有人退出,请立即缩小范围。
  4. 与团队积极沟通。陈述每个人的工作职责。
  5. 留出测试和错误修复的时间。
  6. 保护自身健康。不要过于激动。停下来休息,呼吸新鲜空气,舒展身体

聚会是另一种绝佳的联谊方式。从非正式的当地聚会到有发言人和时间表的正式会议,这些都属于聚会。有些聚会由开发人员主导,另一些由玩家主导。您可以在Meetup.com 或其他地方搜索,了解您周围有哪些聚会活动。在聚会上,您可以进行游戏展示以获得反馈,展示游戏预告片,发表演讲,或者与志趣相投的独立粉丝和开发者交流。您可能会发现,这些聚会也是寻求设计、编码、图形或音乐帮助的好地方。

封闭的 Alpha Exposure(Alpha 暴露)

项目经理将软件项目的 alpha 阶段作为严格测试的第一阶段。虽然代码可能不稳定,但您现在可以在聚会、游戏制作节和其他聚会中收集反馈。玩家可以告诉您他们喜欢或不喜欢哪些地方,并帮助您决定要添加或删除哪些功能。很少有独立开发者敢于主持一个所有参与者都可以参加的开放 alpha 测试阶段。这就是为何大多数参加 alpha 活动的团队通常会亲近精心挑选的受众。获得玩家反馈、衡量可玩性和积极性以及创造口碑的优势不一定能够抵消任何难缠的问题,因此您需要判断何时展示自己的作品。

比赛

参加游戏比赛是一种久经检验的方法,可让您获得裁判员的反馈,并可能在最需要时获得鼓励。赢得比赛可让您士气大振,为您提供一个即时营销点,并为您的开发博客和社交媒体提供信息。推动自己度过最后一关以满足参赛截止日期要求也可以提供动力。

反馈和技术帮助是年度 英特尔® 进阶游戏开发人员大赛 (Intel® Level Up Game Dev Contest) 的重要内容。英特尔汇集了来自独立领域和领先开发工作室的各种裁判员,他们的洞察和见解是这一活动的特殊组成部分。奖金不仅仅是现金奖励;2017 年的所有比赛获胜者都获得了 Razer Blade Stealth Ultrabook*,年度最佳游戏得主获得了 5,000 美元的奖励、一项针对其需求量身定制、由机构推进的数字营销活动(价值 12,000 美元),以及一份与 Green Man Gaming 的经销合同。

不要转发

社交媒体是一把锋利的双刃剑,如果落到坏人手中,结果可能是致命的。慎选战场,忠于您游戏的特征和声音,学会对批评不予理会,无论批评多么尖锐以及是否出于善意。

一些最常见的社交媒体网站是众所周知的,如 Facebook、Twitter、Snapchat 和 Instagram* 等,并且随时可能出现新的社交网站。在 MakeUseOf.com 上发表文章的 Joel Lee 在 2013 年为游戏玩家列出了三个绝佳的社交网络:Raptr、Playfire 和 Duxter。其中 Duxter 已经关门大吉,Playfire 已转手到 Green Man Gaming,Raptr 于 2017 年 9 月关闭。在新网站中投入大量时间时务必要谨慎。

定价和盈利策略

游戏发行领域最大的挑战之一就是如何定价。北卡罗来纳大学的 Yu Zhan 制定了一份简单的定价策略指南,分为三个部分:付费即胜利、付费游戏和免费游戏。

玩家可以购买更好的英雄、更好的武器或免费关卡之后的更多关卡,例如全明星英雄*。在这个游戏中,玩家只有付款才能赢得真正的胜利。

Dark Souls*是一个“付费游戏”的例子。他们销售续集和可下载的内容,以及在线版本。Minecraft Realms*是这一策略的另一个示例,《魔兽世界》也是如此。

免费游戏(有时称为免费增值)使用游戏免费的策略,但充满了广告和诱惑力。最近的大多数游戏都使用这种策略,有时支持玩家付费免广告。

设定合适的价格需要前面所述的竞争分析,并了解类似游戏如何处理定价。如果您了解自己的目标受众及其期望,您应该能够设定价格并坚持这一价格。

在游戏推出后的 6 到 12 个月内,通过不提供折扣、销售和其他促销活动,确定是否获得最大盈利。如果您正在跟踪销售和收集用于作出明智决策的统计数据,则可以突破整个行业定价趋势的界限。但请记住,一旦您进入市场,提高价格几乎是不可能的。

零售商希望您获得成功,以便他们也能增长。实体场所已变得不那么重要,特别是对于独立开发人员而言。您的大部分销量可能会来自 Steam、Green Man Gaming、Humble Bundle 和 G2G 等在线销售。在 GameJolt 市场中,查看境外零售商,按地区列出零售商。在 Statista 上,找到关于特定地区游戏市场收入的信息。

在如今的独立环境中,与 Steam 或其他供应商共享收入是必要的。Gamasutra 解决了这个问题并询问是否值得。答案是 也许。他们得出结论,“如果与错误的人选合作(或者即便与正确的人选合作,但在错误的条件下),没有任何合同可以帮助您。但经历这个过程非常关键。最重要的是,合同可以帮助您避免陷入错误的合作关系。此外,如果您和您的合作伙伴出现分歧,合同将为您提供可靠的框架。”

公关和自我推销

美国公共关系协会指出,公关 是一种“建立组织与公众之间互惠关系的战略沟通过程。”如果做得好,公关包括复杂规划、指标收集和开发阶段。遗憾的是,聘请一家公关公司来帮助推广您的游戏是大多数独立开发人员无法承受的。但是,您可以通过便宜的活动来提高您在公众中的声誉。

eZanga.com 创始人兼首席执行官 Rich Kahn 告诉smallbusinesspr.com您可以做五件简单的事情:

  1. 成为您所在行业的权威。接受可以找到的任何演讲活动,经常做志愿者,并在博客上为初学者回答问题。在 Twitter 上发布有趣的花絮,参与有礼貌的辩论,为自己树立名声。确保您的想法与品牌一致。例如,如果您是 RPG 工作室,请不要炒作 FPS 游戏。
  2. 与学校联系。学生是未来的员工,以客座讲师的身份出现在他们面前是提高公众知名度的简单方法。您可以拓展校园关系,根据自己的需求聘请来自工程、商业、平面设计和写作课程的实习生。
  3. 与媒体交朋友。与您尊重的评论家和编辑接触并交流。有朝一日,他们可能需要引言来完美捕捉关键洞察,没什么能够比通过一段重要引文向大家展示您的智慧更加重要。对于您所在行业,特别是您所在特定领域的人员,您了解得越多,您在会议和聚会上的乐趣就越多。
  4. 考虑品牌合作。如果您的游戏在本地制作节或聚会上取得成功,那么您分享这一成功的次数越多,您为该实体提供的营销帮助越多。如果您在不断增长的网站上发表正面评论,您可以像自己的努力一样帮助他们做出努力。良好的品牌合作就像在派对上展现出良好的教养,当然您要感谢主持人并赞美他们的工作。
  5. 把握行业脉搏。制定关于有趣挑战的在线调查,并将结果发布到博客上。在社交媒体上传播新闻,分享财富。您可能会吸引媒体朋友的关注,这会让您在下次调查时更具可信度。

您的游戏开发之旅充满了冒险和刺激,有时在同一天出现。若要Get Ready(准备就绪), Get Noticed(备受关注)以及 Get Big(大获全胜),您会面临许多挑战。虽然本指南涵盖了您将面临的一些最大障碍,但每天都会出现新的挑战。

由于大多数独立开发人员的预算很紧张,所以您必须在整个过程中随机应变、不断调整并克服挑战。虽然本指南提供了一些建议,但有些要素是必须具备的,例如社交网络知名度、网站登录页面、可信赖的视频预告片和可播放的演示。尽早开始宣传,建立并运用发言权,避免沉默的倾向。查找要加入的小组讨论,讲述您的故事。您可能没有高预算,但您有独一无二的故事和热情。经验丰富的老手会很欣赏您的热情,因此请尽可能保持住您的热情。

回到 Gamasutra.com,游戏设计师 Sarah Woodrow 给予了以下鼓励:“独立游戏开发将推动游戏的未来。独立游戏开发人员将打破我们对游戏的现有认识,创造真正创新、有趣的体验。现在有一些刚刚起步的独立游戏开发人员,他们将在 10-20 年内成为游戏行业的商业领袖。我们已经看到,独立游戏开发人员的数量呈上升趋势,未来将越来越多。

关于作者

Garret Romaine从 1992 年来一直从事游戏行业工作,包括审查游戏、开发功能以及编写白皮书、案例研究和分析等。他拥有波特兰州立大学的工商管理学硕士学位,为 RH+M3 编写代码。

人工智能 (AI) 助力皮肤癌筛查

$
0
0

ai-helps-with-skin-cancer-screening

“人工智能的长期目标和真正潜力是在宏观层面复制复杂的人类思维,然后超越这种思维以解决复杂的问题——既有充分证据且目前又难以想象的问题。”1

挑战

皮肤癌已成为世界上大部分地区的流行病。我们需要通过简单的测试执行大范围初步筛查,以鼓励个人在必要时寻求治疗。

解决方案

Doctor Hazel 是一种基于人工智能 (AI) 技术的实时皮肤癌筛查服务,它依靠大量的图像来区分皮肤癌和良性病变,帮助人们更轻松地寻求专业的医疗建议。

背景和历史

经证明,黑客马拉松是一项富有成效的活动,可利用能源和技术专业知识解决具体问题并产生新的应用技术创意。Doctor Hazel 就是如此。Doctor Hazel 是旧金山 2017 年黑客马拉松活动中一项值得关注的项目,由英特尔® 软件创新者 Peter Ma 和 Ethos Lending 工程副总裁兼 Doctor Hazel 创始人 Mike Borozdin 共同开发。(参见图 1)。

Peter 指出:“我和我的联合创始人有一个非常亲密的朋友,他在 30 出头时死于癌症。他的离去触发了我们想要治疗癌症的愿望。对人工智能和癌症进行研究后,我们认为自己实际上可以有效利用人工智能技术筛查皮肤癌。”

Peter Ma (left) and Mike Borozdin show screening techniques
图 1.Peter Ma (左图)和 Mike Borozdin 展示筛选技术。

通过购买和使用价格低廉的高功率内窥镜摄像头来拍摄图像,Peter 和 Mike 开始创建 Doctor Hazel 网站,他们在 TechCrunch 黑客马拉松活动上展示了该项目并获得了广泛赞誉。Peter 表示:“自从我们在 2017 年 9 月构建了第一个原型以来,TechCrunch、华尔街日报IQ by Intel等许多媒体和刊物都纷纷报道了我们的项目。根据我们的经验,我们相信我们能够满足技术要求。但我们最大的挑战是获得美国食品和药物管理局 (FDA) 的批准和收集更多分类图像。”

Peter 表示:“对于所有初创企业而言,想法是最简单的,执行是最难的。大部分项目的失败原因都是无法找到产品市场定位。我构建了数百种原型,但得到关注的寥寥无几。当您向人们展示 Doctor Hazel 时,每个人都希望参与测试并提供帮助。我们每个星期都会收到数百个咨询问题,很多人想要提供数据并试用服务。”

重要的项目里程碑

  • Doctor Hazel 概念和原型于 2017 年 9 月在 TechCrunch 黑客马拉松上首次亮相
  • 发布 Doctor Hazel 网站,以解释项目并从希望帮助构建数据库的各方征集图像和信息。
  • 许多媒体和刊物报道了我们的项目,包括华尔街日报、TechCrunch 和 IT by Intel 等。
  • 我们在多项活动中演示了项目能力,包括 2017 年 11 月 7 日和 8 日举办的全球物联网 DevFest II。

Peter Ma demonstrates the technology at Strata Data NY in 2017
图 2.2017 年,Peter Ma 在纽约 Strata Data 峰会上展示了该技术。

支持技术

项目的硬件部分很容易组合在一起。该团队使用从亚马逊*购买的约 30 美元的大功率内窥镜摄像头,拍摄了痣和皮肤损伤的高分辨率图像,以便与不断增长的数据库中的图像进行比较。Peter 和 Mike 利用英特尔® AI DevCloud 训练人工智能模型。这款基于英特尔® 至强® 可扩展处理器的平台对英特尔® 人工智能研究院会员免费提供,且支持多种主要的人工智能框架,包括 TensorFlow* 和 Caffe*。为提高这款诊断工具的效用,Doctor Hazel 采用英特尔® Movidius™ 神经计算棒,它支持在无法立即访问互联网的情况下进行筛查。

Peter 表示:“英特尔可满足人工智能方面的软硬件需求,包括培训和部署等。作为初创企业,构建原型的成本相对较低。英特尔® Movidius™ 神经计算棒售价约为 79 美元,支持人工智能实时运行。我们使用英特尔® Movidius™ 软件开发套件 (SDK),该工具对这个项目非常有用。”

英特尔® Movidius™ 神经计算棒采用 USB 外形设计和低功耗英特尔® Movidius™ 视觉处理单元 (VPU),能够使用字自包含推理引擎加速深度神经网络处理。开发人员可以选择使用基于 Caffe 或 TensorFlow 框架的卷积神经网络模型启动项目,并使用多个示例网络之一。工具套件可以对神经网络进行分析和调整,然后编译一个用于嵌入神经计算平台 API 的版本。如欲获取如何借助英特尔 Movidius 神经计算棒进行开发的提示,请访问此网站。

包含疑似和确认的皮肤癌病变数据的庞大图像数据库是优化机器学习和提高识别准确度的首要必备条件。

团队从国际皮肤成像协作项目、皮肤癌基金会和爱荷华大学下载了数千张图像,以便在初期嵌入学习流程。在评估样本时,Doctor Hazel 测量了 8,000 个变量,以检测图像样本是否可能是皮肤癌、痣或良性病变。

该项目的目标是为所有人提供一种免费进行皮肤癌筛查的手段。为了构建图像数据库并收集更广泛的已确认皮肤癌图像样本,测试版 Doctor Hazel 网站正在征求意见和数据。在一次 TechCrunch访谈中,Mike 评论道:“我们在获取医药领域的人工智能数据方面存在巨大的问题,但也可能会有出色的成果。共享数据的人越多,系统的准确性越高。”该团队正在努力将识别率提高到 90% 以上,随着图像数据库的扩展,他们离这个目标越来越近了。

该团队正在规划一款用于配合平台的应用,考虑使用一款紧凑、廉价的图像捕捉设备进行筛选。该项目的一个基本目标是允许个人在诊所或通过使用实时测试系统的免费中心轻松进行自我测试,如果结果显示皮肤癌概率很高,则寻求皮肤科医生或医疗专业人员的帮助。医生不再需要进行初步筛查,从而专注于更需要根据癌症阳性指征进行治疗的患者。(见图 3)

Doctor reaching for a dermascope to examine a patient’s skin lesion
图 3.医生用 dermascope 检查病人的皮肤病变。

人工智能正在为医学进步开辟创新之路

人工智能在诊断医学和治疗方法中的应用正在为优化全球医疗创造新的机遇。通过专用芯片的设计和开发、优化的软件和框架、研究资助、教育推广与行业合作等,英特尔坚定致力于推动人工智能 (AI) 的发展,帮助化解医学、制造、农业、科学研究和其他行业的挑战。英特尔与政府组织、非政府组织和公司密切合作,发现和推进解决重大挑战的解决方案,同时遵守现行的政府政策和指令。

英特尔® 人工智能产品组合包括:

Intel Xeon logo

英特尔® 至强® 可扩展处理器:借助针对深度学习等广泛人工智能工作负载优化的计算架构,化解人工智能挑战。

Framework Optimization

框架优化:在强大的可扩展基础设施上更快速训练深度神经网络。

Intel Movidius Myriad

英特尔® Movidius™ Myriad™ 视觉处理单元 (VPU):创建和部署设备上神经网络和计算机视觉应用。

更多信息请访问产品组合页面:https://ai.intel.com/technology

对于英特尔® 人工智能研究院会员,英特尔 AI DevCloud提供了一个面向机器学习和深度学习训练的云平台和框架。英特尔 AI DevCloud 采用英特尔至强可扩展处理器,提供长达 30 天的免费远程访问,以支持研究院成员的项目。

立即加入:https://software.intel.com/zh-cn/ai/sign-up

“人工智能将支持我们推进科学方法,这本身就是一种工具,一种使我们能够获得可重复、可再生结果的过程。现在我们需要将更多数据纳入这些推论,以推动这一领域的发展。一个人查看了一些数据,然后突然迸发出灵感的时代已经过去了。现在我们需要整合多个数据源、协作以及工具。”2

– Naveen Rao,英特尔副总裁兼人工智能产品事业部总经理

资源

英特尔® 人工智能研究院

英特尔 Developer Mesh 中的皮肤癌项目

IQ by Intel 文章——使用人工智能检测皮肤癌

皮肤癌研究的深度学习算法

Doctor Hazel 网站

Doctor Hazel 使用人工智能进行皮肤癌研究

借助 Caffe 深度学习框架充分发挥人工智能的优势

英特尔® Caffe* 分发包*

英特尔® Movidius™ 神经计算棒

皮肤科医生对皮肤癌的分类

参考资料

1.Carty、J.、C. Rodarte 和 N. Rao。“人工智能在制药和保健服务中的应用”,HealthXL。2017

2.https://newsroom.intel.com/news/intel-accelerates-accessibility-ai-developer-cloud-computing-resources/

基于英特尔® AI DevCloud 的英特尔® Optimization for TensorFlow* 中的英特尔® 至强® 可扩展处理器架构和优化概述

$
0
0

我在 2017 年底加入了英特尔® 学生开发人员计划,当时我非常兴奋地试用了英特尔® 至强® 可扩展处理器。[1] 这是他们大约同一时间推出的英特尔® AI DevCloud 的一部分。为了确保所有人保持同步,我想先介绍英特尔至强可扩展处理器及其对计算的影响。接下来我将介绍最后一部分——优化深度学习 TensorFlow* [2] 代码。

与前一代英特尔® 至强™ 处理器 E5-2600 v4 产品家族(以前称为 Broadwell 微架构)相比,基于“Purley”平台的英特尔® 至强® 可扩展处理器产品家族是一种具有许多附加功能的新型架构。我认为,英特尔® 至强® 可扩展处理器的人工智能 (AI) 计算能力之所以很出色,主要原因在于英特尔®高级矢量扩展 512(英特尔® AVX-512)[3] 指令集。它提供超宽 512 位矢量运算能力,可以处理 TensorFlow *所需的大部分高性能计算工作。由于 TensorFlow 中最基本的计算单元涉及通过矢量处理单元并行运算的张量流。这些称为 单指令多数据 (SIMD) [4] 运算。举个例子,比如我们以自然的方式添加两个向量,我们将循环遍历维度并添加相应的单位。尽管支持矢量的中央处理单元 (CPU) 将通过单个加法运算来添加这两个矢量,从而减少向量维数因子的延迟,但是在我们的示例中,它最多可以执行 512 位。我们将获得比普通 CPU 高 3 到 4 倍的性能提升。

接下来我们来看看我进行的实验,它证明了我的观点。当我加入该计划时,我已经拥有 1500 行神经图像标题系统的代码库,可以试用以前仅在Google* Cloud 平台 [5]上运行的代码库。神经标题系统是通过编码器 - 解码器神经网络系统为图像生成标题的系统。就我而言,我对Vinyals 等人 [6] 的工作进行了轻微修改。我的图像编码系统是VGG16 模型 [7]。这是一个ILSVRC [8] 中的卷积神经网络,用于对象识别竞赛。事实证明,它可以作为一个很好的特征提取器,所以我删除了第 7 层完全连接层之后的所有内容,并使用了最终的 4096 长度矢量。这种方法在深度学习社区中称为“转移式学习”。我预先提取了 Microsoft COCO [9] 数据集所有图像的特性,然后对数据进行了主成分分析 (PCA),以便将其尺寸减小到 512。我对两个数据集(PCA 和非 PCA)进行了实验。这项工作正在进行中,如果您想查看,代码库就在我的GitHub库中。

在运行过程中,当我第一次尝试在英特尔至强处理器上运行时,性能毫无提升,反而有些下降。所以上个月我一直在研究这个问题,希望能找出原因。在此我想与您分享一些我发现的步骤,以帮助您在这款复杂而强大的处理器上实现性能提升。

  1. 当我们通过多个数据进行批量处理时,应尽可能避免任何类型的“磁盘读写”。在英特尔 AI DevCloud 中,我们的主文件夹——网络文件系统 (NFS) 在计算节点和登录节点之间共享。集群上的读取写入需要很长时间,因为它与家用 PC 相比距离更远。那么,我们如何去做呢?TensorFlow 通过其 数据集 API [10] 提供了一个基于简洁队列的操作。API 支持我们使用可重用的操作构建复杂的输入管道。它用您选择的预处理操作包装您的数据,并将它们分配在一起。这将大大缩短您的延迟,因为您的整个数据集将在所有限制和要求下进行缓存,并将所有操作嵌入到计算图中。
     
  2. Colfax research [11] 有一篇很好的文章,我想根据这篇文章写一篇对性能有直接影响的技巧。它能够基于 YOLO [12] 对一个对象检测网络进行优化,并建议调整某些从性能角度而言非常重要的关键变量。
  • KMP_BLOCKTIME:这是诸多变量之一,用于控制 OpenMP * API 的行为。该 API 是一个并行编程接口,主要负责 Tensorflow API 内部的多线程操作。这个变量用于控制 OpenMP 线程在休眠之前的等待时间(以毫秒为单位)。较大的数值可确保数据经常访问,但也很容易造成其他线程资源的闲置,所以这个变量需要调整,以最好地满足我们的需求。在本示例中,我将其设为 30。
    os.environ["KMP_BLOCKTIME"] = “30”
  • OMP_NUM_THREADS:这是指 TensorFlow 操作可以使用的并行线程的数量。TensorFlow 的推荐设置是将其设置为物理内核的数量。我试了一下 136,它很合适。
    os.environ["OMP_NUM_THREADS"] = “136”
  • KMP_AFFINITY:这提供了对 OpenMP 线程放置到物理内核的抽象控制。TensorFlow 的推荐设置为‘fine, compact, 1, 0’。‘Fine’ 可阻止线程迁移,从而减少缓存未命中。‘Compact’ 将邻近的线程放在一起。‘1’ 将线程优先放置在不同的可用物理内核上,而不是在有超线程的相同内核上。这种行为与电子轨道填充原子的方式类似。‘0’ 是指索引核心映射。
    os.environ["KMP_AFFINITY"] = "granularity=fine,compact,1,0"
  • 内部操作并行线程:这些是 TensorFlow 提供的变量,用于控制可以运行多少个并发操作以及每个操作可以运行多少个并行线程。在本例中,我将前者设为 2,并使后者等于 OMP_NUM_THREADS(按照建议)
    tf.app.flags.DEFINE_integer(‘inter_op’,2,”””Inter op Parallelism Threads”””)
    tf.app.flags.DEFINE_integer(‘inter_op’,136,”””Intra op Parallelism Threads”””)

调整上述所有变量后,性能提高了多达 4 倍,周期时长从 2.5 小时缩减为 30 分钟,从而大大缩短了延迟。正如之前介绍的,英特尔至强可扩展处理器非常强大,而我们在英特尔 AI DevCloud 中获得的性能是理论上承诺的 260 TFlops 性能。除非在适当位置放置了特定卡,否则无法直接实现这一性能。

参考

  1. 英特尔至强可扩展处理器
  2. arXiv:1603.04467: TensorFlow:异构分布式系统上的大规模机器学习
  3. 高级矢量扩展
  4. 单指令多数据 (SIMD)
  5. Google cloud Platform
  6. arXiv:1411.4555: 展示说明:神经图像标题生成器
  7. arXiv:1409.1556 用于大规模图像识别的非常深的卷积网络
  8. ImageNet 大规模视觉识别竞赛 (ILSVRC)
  9. arXiv:1405.0321v3 Microsoft COCO: 上下文中的通用对象
  10. TensorFlow 的数据集 API
  11. 英特尔至强可扩展处理器上实时对象检测的优化,Colfax
  12. arXiv:1506.02640: 只查看一次:统一的实时对象检测

创建速度更快的代码 - 突破极速

$
0
0

尊敬的(先生/女士):

无论您是一名学生、老师、开源贡献者,或技术、高性能计算(HPC)、企业或云开发人员,总能找到适合您的内容!

您要开发需要提高运行速度的软件?您的软件需要执行大数据分析、医学成像、对时间要求严格的财务分析、模拟(例如 CFD 或天气)、机器/深度学习或需要立即完成的数千种任务之一?

那么您需要的正是这样一款高性能工具套件:英特尔® Parallel Studio XE,利用英特尔® 至强® 处理器以及英特尔® 至强融核™ 处理器和协处理器日益增加的核心数量及向量寄存器宽度,提高应用速度。

加入我们,参加北京时间2018年5月29日上午10点开始的在线研讨会,与英特尔资深技术工程师和并行科技应用总监一起,学习如何在您的计算密集型应用中利用英特尔®技术的强大功能和性能,帮助您的应用程序更好、更快、更智能地运行英特尔工具和技术。

讲师介绍

甘驰
英特尔技术工程师团队经理

12年来一直致力于为开发人员提供面向英特尔软件工具的技术支持、培训以及赋能服务,专注于系统级和微架构级性能调优。

 

 

乔楠
并行科技应用总监

带领团队搭建了超算网格产品体系和服务体系。前任英特尔有限公司中国高性能计算团队经理,专注于服务器应用软件的性能推进。

 

 

若有疑问,请联系:
李金星  邮箱:lijinxing@itcgb.com
电话:010-64410820

 

 

TSDI可信软件定义基础设施白皮书

$
0
0

概述

当前,越来越多的企业面临着信息化平台与企业发展不匹配的问题,大部分企业希望能够通过云平台解决上述问题,充分利用云的灵活性和扩展性,满足其快速迭代和创新发展需求,但又对云的安全性、合规性顾虑重重,为了解决这些问题,企业需要一套创新的云平台基础设施解决方案。大唐高鸿信安的可信软件定义基础设施TSDI(Trusted Software Defined Infrastructure)是一套融合可信计算技术、云计算技术和软件定义技术,兼容OpenStack架构的云平台基础设施解决方案,可为企业提供安全可信的虚拟化功能和云计算服务,为企业满足合规性管理要求提供技术支撑。

Viewing all 583 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>