0%

Unity基础:DrawCall从入门到精通

正文:

Draw Call从来都不是问题。换句话说,一开始没有问题,直到你添加了一个又一个的渲染元素,最后渲染线程突然变成了新的瓶颈。你能猜出为什么现在阻止Unity Draw Call比以往任何时候都更加重要吗?

img

警告:这是一篇深入探讨的文章,让自己感到舒适并喝杯茶。如果你着急,可以根据目录来查找想看的内容。

  • 初遇Draw Call

  • 可能有太多Draw Call的信号

    • 弱信号:电池消耗过快
    • 弱信号:设备升温
    • 弱信号:游戏不能顺滑的运行
    • 弱信号:VR用户比以往任何时候都头晕
    • 强信号:Unity Profiler显示了渲染线程瓶颈
  • 等一下…什么是Draw Call

    • Batches vs SetPasses
    • 计算Unity DrawCall
  • 需要减少Draw Call的3个原因

  • 战斗:Unity Draw Call批处理

    • 前提:合并Unity 材质(Materials)
    • 技术1:Unity静态批处理(Unity Static Batching)
    • 技术2:Unity GPU Instancing
    • 技术3:Unity动态批处理(Dynamic Batching)
    • 技术4:Unity运行时批处理API
  • 实践才是检验真理的唯一标准

img

初遇Draw Call

就在几年前,我还是一个没有经验的年轻小伙子……尤其是在游戏编程方面。

我当时正在做我的第一个专业任务,任务很明确。

我只需要改进和实现现有游戏的几个游戏系统。

很公平。

这就是我几个月来一直专注的内容。为我的玩家创造乐趣。

img

问题是,游戏开发中的每个领域对我来说仍然还是个未知数。

并且我不停的问自己……

如果我不得不解决在我未涉及领域的问题时该怎么办?

这个简单的想法使我非常不安。毕竟,我不想让我的老板失望。他雇用我是有原因的,所以他当然希望我能完成我的任务。

当然我知道这种情况只有在我必须面对从未解决过的问题的时候才会发生。

而且我还没有准备好。

不管怎样,我很高兴不断为游戏添加内容,不再对此担心。一切进展顺利,我收到了很好的工作反馈。

更好的是,性能一直很出色……

但不幸的事发生了。

img

几个月后,我注意到有些事不对劲了。

我去了商店,开始注意到游戏有越来越多的负面评论

虽然我已经习惯了一定比例的负面评论。这始终是将你的作品展示给全世界的一部分。

但是趋势使我担心。它比以往任何时候都更糟。

越来越多的用户开始抱怨电池耗电更快,设备发热比以往任何时候都高,并且游戏感觉太慢

我花了一些时间将这些点连接起来。

我认为那一定是性能出现了问题

我开始担心会损害到了用户体验。

更糟糕的是,我的客户会怎么看我?这肯定和我在游戏开发方面所做的工作有关。

忧虑迅速转变为压力

虽然我习早已惯了压力。毕竟在几年前,我经常每天在大学里呆12个小时以上。

但是这次却有所不同。不再只是和我有关,我正在让某些用户感到失望。

怀着勇气,我开始深入挖掘未知的性能世界。

而且我特意在业余时间做这件事。

我很快了解了Unity Profiler。那个有价值的工具向我展示了渲染线程似乎花费了太长时间。但是我不知道为什么。

所以我一直在调查。

但是,无论我投入多少时间,问题的出现速度都比我解决问题的速度快。

我正在思考是否要放弃

毕竟,也许游戏开发并不适合我。

img

但是后来,我成为了世界上最幸运的开发者之一。

我很幸运能看到一篇有关技术债务的出色在线文章。我意识到给我自己挖了坑。

但与此同时,我也受到启发。

在开发过程中,随着时间的推移,我引进了一些内容,这些内容使我在游戏中的Unity Draw Call数量激增。Draw Call是Cpu向GPU发起的一种在屏幕上绘制内容的请求。

130个Draw Call太多了吗?是的,一定是这样,我想。我添加了未优化的内容,这会导致电池消耗加速和游戏运行速度变慢,所以现在我需要着手对其进行优化

因此,我开始了优化材质的漫长旅程。毕竟在Unity中,Draw Call与材质的设置高度相关。我为此花费了大量的时间。我造成了这个问题,所以应该由我来解决它。

不过,我并没有停止思考长期问题。

如果我遇到了这个问题,其他人也有可能遇到。

在我看来,解决这个问题的唯一方法就是创建流程来持续监控性能指标。

那是我接下去的工作。

img

在几天之内,我实现了一个原型系统来连续监视游戏中的Unity Draw Call数。我想确保将来人们(尤其是我自己)仅提交优化过的内容。

尽管如此,我依然仍努力遵守最后期限。

我知道我必须完成它。我确实完成了。

经过不懈的努力,我统一了绝大多数游戏中使用的材质,并大大减少了着色器的数量

漫长的努力,我的抽Draw Call数量终于降低到60以下了。

新的工作流保证了每个人都遵从了性能指南,性能再次变得很棒。

我为此感到自豪。

img

但是,我仍然有内心的声音提醒我,我让这些玩家失望了。

他们曾经玩游戏很开心。在游戏中他们成为了朋友。他们希望通过游戏加强与家人的关系。

这就是为什么我努力减轻他们在评论中表现出来的痛苦的原因。

但是这些玩家再也没有回来。永远失去了他们

真是令人心碎。

img

通过失去这些玩家,使我了解了在整个项目过程中监控性能的重要性。

加载时间帧速率,性能峰值电池和电源效率……所有这些事情比我想象的要重要得多。

这是使我如此专注于游戏性能优化的决定性时刻之一。我学到了教训。

该游戏今天仍然表现良好。而且由于进行了优化,因此可以轻松的移植到性能较弱的平台。

从那时起,我几乎每天都在监控我的游戏性能。

但是我不是一个人做。

我有系统可以自动报告这些数字。当系统报告有问题的时候,我就会去调查。

随着虚拟现实获得了巨大的吸引力,监视Unity Draw Call的任务以往任何时候都更加重要。我们必须以72、90甚至144 Hz的帧速率进行渲染。这些时间不会给你可观的CPU预算。

img

可能有太多Draw Call的信号

在游戏开发过程中的任何特定时刻都需要注意一些重要的标志信息。

随着时间的流逝,您会发展出第六种感觉,每当你遇到这些情况时,都会在脊柱上流下一些寒气。

但是,症状只是症状。由于瓶颈可能来自多个角落,因此它们并不都是Draw Call的问题。为了使区别更加清楚,我将它们分为两类。

弱信号表示有比较微弱的可能是由高Draw Call产生。它们可以完全来自其他性能因素,例如OverDraw

最后,强信号表示你的游戏很有可能已经使用了太多的Draw Call。

img

弱信号:电池消耗过快

手机电池通常在正常使用情况下可持续使用一到两天。

但是游戏擅长于从用户的锂离子电池中窃取能量。

但是,请注意,不同游戏的功耗会有所不同。经过优化的游戏将使您的CPU和GPU压力减少,从而减少功耗。

而且优化游戏非常重要,因为您的用户非常擅长注意到早餐玩游戏时消耗了多少电量。

即使你不关心用户(谁会不关心用户?),这仍然是与你相关的因素。

这就是为什么…

高效运行的游戏让你的玩家可以玩更长的时间。而且他们玩的越多,就可以出售了更多应用内购买,更多广告展示,获得更多的口碑,就会有更多的钱落在你的口袋(或你的老板的口袋)中,你说对不对?

优化电池消耗是一个很好的投资回报。

img

弱信号:设备升温

你从用户设备中获取的大部分能量都转换为热量(和光),惊不惊喜?

在冬天,这可能会很方便以使你的手变热。但是,仍然剩下3个季节,你的用户并不希望到带着充电宝到处跑。

我仍然记得我在柏林的时候在冬天命令Glühwein温暖我的手。我不喜欢以葡萄酒为基础的饮料,但我学会了欣赏自己手中的温暖感觉。

所以我一直买。

如果你的目标是一个虚拟现实设备,你应该特别优化能源效率。除非你正在开发一个应用程序来取代传统的面部晒黑。

img

弱信号:游戏不能顺滑的运行

想象一下……

你正在玩节奏慢的多人射击游戏。

但是,由于你是新手,所以每个人都比你玩的更好。

经过数小时的挫折之后,你终于发现了机会:一个分心的狙击手

你在从后面偷偷接近他。他们还没注意到你,但你知道你的机会仅仅存在于他们背对你的一小段时间内。所以你瞄准了头部准备来一次完美的爆头。

你慢慢的把鼠标向上移动了几像素。

但是,经过半秒钟的延迟,你的十字准线现在突然指向天空。

狙击手注意到了你,等到你意识到的时候,你已经死了。

到底发生了什么事?

低帧率。

现在,由于多种原因可能会导致性能下降,但是,大量的Unity Draw Call无疑是其中之一。

我记得“反恐精英”的早期,人们使用最强大的GPU扔烟雾弹以获得不公平的优势。玩家如果使用低端计算机,计算机将无法真正很好地处理烟雾,因此他们最终死于低帧率。这可能更多是由于OverDraw而不是Draw Call,但我仍然值得一说。

弱信号:VR用户比以往任何时候都头晕

VR游戏的性能越低,用户的感受就会越差。

等等,我可以用不同的方式讲。

你潜在的Draw Call越多,你给用户糟糕的情况就越严重。

是的,这种说法更好。

img

强信号:Unity Profiler显示了渲染线程瓶颈

如果渲染线程执行所需的时间太长,则可能有太多的绘制调用。或更糟糕的是,您可能有太多的Set Passes Call(我们将在下一部分中看到不同之处)。

当渲染线程完成处理所有绘制调用之前,你的主线程可能闲着。

您可以通过Unity Profiler识别这种情况,如下所示。

img

Unity Draw Call:昂贵的渲染线程

你拥有的这些迹象越多,你的游戏中出现太多Draw Call的可能性就越高

我们已经谈论Draw Call很长时间了。

但是究竟是什么Draw Call?

等一下…什么是Draw Call?

简而言之,Draw Call 就是你的CPU要求你的GPU绘制一些东西

就是你的CPU在说:嘿,用这些纹理和照明信息在那角落画这张椅子

如果你有很多“东西”,那么你可能需要准备许多Draw Call:

  • 我想要那个角落画椅子
  • 还有另一个角落画椅子
  • 哦,再画上书柜就更好了。

问题是,准备Unity Draw Call会占用CPU大量的时间和精力。Unity必须将场景内容转换为GPU可以理解的格式。这个过程开销最昂贵的部分是设置正确的渲染参数,例如纹理,着色器,网格等。

手动设置渲染参数很繁琐。这就是游戏开发人员引入材质概念的原因。

材质是一种包含有关如何绘制对象的信息的数据结构。它包含一个着色器及其所有参数,以及有关如何设置GPU渲染状态的信息。

而且,添加到场景中的每种材质都会增加渲染管线的复杂性。每种材质至少使用一个SetPass(用于设置渲染参数)。如果你希望自己的游戏表现出色,那么你真的需要将这些最小化。

这是否意味着我们不能一次绘制太多对象?

不是的。

游戏开发人员使用批处理将相似对象的渲染分组到同一个Draw Call中。这样,CPU只需支付一次绘制调用即可渲染多个对象。

在使用批处理时,我们要求GPU一次在这里,那里和后面画三把椅子,而不是在三个不同的时间。

现在,关键要理解什么是“相似”(对象)。

在批处理中,相似定义为在不同的对象上使用相同的材料。如果完成此步骤,则可以完成最复杂的步骤。

在Unity Frame Debugger的帮助下,你可以在下面看到4个Draw Call的顺序:3个用于家具,1个用于地板。

动图封面

Unity Draw Call:无批处理

这样子开销真的很昂贵,但是分批处理将帮助我们减少这些Draw Call。这将减少播放器的CPU负载。拥有更多性能资源可以让你添加更多游戏功能,或者只是用这种方式减少设备的能耗。

Batches vs SetPasses

有一点细节,很少有开发人员知道。

在分析器和统计信息窗口中看到的“Batches”和“SetPasses”指标之间存在着差异。

这种差异具有巨大的影响。

Batches通常被我们称为绘图调用(Draw Call)。这些是简单的绘制命令,例如,在此处绘制此对象,然后在此处绘制另一个对象。这主要是关于使用当前全局渲染状态绘制相同着色器、相似参数的对象。

但是,SetPasses描述了一种更昂贵的操作:材质更改。更改材质很昂贵,因为我们必须设置一个新的渲染状态。其中包括着色器参数和管线设置,例如Alpha Blending,Z-Test,Z-Writing。

让我通过示例向你说明。

让我们考虑一下,有3个椅子使用相同的网格。

img

img

img

三把椅子

现在,我们将探讨三种具有不同批处理和材质设置的方案。每个方案将导致不同的Batches和SetPasses。请看下表。

方案:最坏情况 方案:还是不行 方案:好起来了 方案:最佳
批处理设置 禁用 启用 禁用 启用
材质设置 不同的(x3) 不同的(x3) 相同的(x1) 相同的(x1)
绘制事件 1. SetPass(椅子1) 2 . Draw Call(椅子1) 3 . SetPass(椅子2) 4 . Draw Call(椅子2) 5 . SetPass(椅子3) 6 . Draw Call(椅子3) 1. SetPass(椅子1) 2 . Draw Call(椅子1) 3 . SetPass(椅子2) 4 . Draw Call(椅子2) 5 . SetPass(椅子3) 6 . Draw Call(椅子3) 1. SetPass(椅子材质) 2. Draw Call(椅子1) 3 . Draw Call(椅子2) 4. Draw Call(椅子3) 1. SetPass(椅子材质) 2 。Draw Call(椅子1 + 2 + 3)
SetPasses 3个 3 1个 1个
Batches (D.C.) 3个 3个 3个 1个
性能 最差 最差 最好

第一种情况和第二种情况相似:不同的材质使我们的SetPass数量猛增。在渲染线程中性能最差,无法进行批处理,因为批处理需要使用相同的材质。

但是,在第三种情况下,我们看到了一丝曙光。共享材质使一切变得不同。具有独特的材料可以将SetPass计数减少到1,这可以极大地提高性能。当然,我们仍然有3个Draw Call,但这些操作非常廉价。

最后,如果你真的想达到最优,那么第四种情况最适合你。在这里,我们启用批处理。批处理喜欢相同的材料。启用批处理可将Draw Call计数减少到1。在这里,我们得到了最理想的输出:❤️1 SetPass,1 Batch❤️️

计算Unity DrawCall

在深入研究Draw Call之前,我们首先需要适当的工具来对其进行衡量。有很多可用的工具,例如RenderDoc,但我们将坚持使用最简单的方法:统计窗口和Unity Frame Debugger。

你可以随时通过单击游戏视图右上角的“统计”按钮来访问“统计”窗口。此面板向你显示当前游戏视图的指标。如果你的屏幕内容发生变化,则这些数字会不断变化(如果你认真对待游戏开发,肯定会有所变化)。

在那里,注意Batches(Draw Call)和SetPasses(材质变更)。如我们所见,它们是相互联系的,但是对性能的影响却并不相同。

img

Unity Performance Stats Window

最后,你可以并且应该使用Unity Frame Debugger。该工具将为你显示当前视图正在发出的特Draw Call(Batches)。你可以点击Window → Analysis → Frame Debugger菜单打开它。

需要减少Draw Call的3个原因

我的经验告诉我,即使SetPasses和Draw Call不是你的游戏瓶颈,但也至关重要的。这是我从项目开始就减少Draw Call的主要原因:

  • 它使移植到未来平台变得更加容易。你肯定花了数千小时来开发游戏。这是每个开发者交付游戏都必须支付的基准成本。一旦你支付了该成本,为什么不从移植到其他平台中获利呢?你将花费一小部分成本使销量成倍增长。问题是,游戏的优化程度越高,移植过程中的工作量就越少。
  • 避免亡羊补牢。一开始就对其进行优化!你可能知道优化你的同事一年前创建的十几种资源的感觉,尤其是当你的同事离开项目时。这在Asset Store购物时尤其要注意的是:商店中的大多数资源并不完善,很可能使用多种材质设置。这将导致不可能以移动端作为兼容目标。
  • 它可以使你的游戏提高游戏效率。将使用更少的CPU资源,并获得更好的性能,这在VR中至关重要。你将从玩家的电池中窃取更少的能量。玩家和社区将通过更好的评论和买更多的应用内购来奖励你。用户玩游戏的时间越长,他们看到的广告越多,他们内容付费的速度就越快。

在使用Unity时,默认情况下你会不由自主的添加Draw Call。除非你有意识注意资产的性能,否则资产多数会更倾向于使用不同的材质。随着时间的流逝,不同的材质不断被添加,各种不能合批的Draw Call也会增加。这将会导致性能爆炸。

img

战斗:Unity Draw Call批处理

我们不希望绘制一个对象10次,而是一次绘制了10个对象。

这就是批处理(batching)的力量。

批处理Draw Call的主要要求是对象使用相同的绘制属性(材质)。在这种情况下,Unity可以将不同的网格合并为使用统一材质的单个网格。

就想前面说的,默认情况下,大多数资源将使用不同的材质。但是不用担心,我们将看到将几种材质合并为单个方法。

下面是一张流程图,总结了在Unity中进行合批的方法和限制(个人补充:静态批处理复杂场景不要用,可能会导致渲染队列混乱,产生严重的OverDraw

img

Unity Draw Call Reduction: The Guru’s Batching Diagram

你的切入点是要查找要那些对象需要使用相同的材质

使用相同的材质是批处理工作的前提。不同的材质具有不同的绘图设置,这些设置会更改全局GPU渲染状态。

如果这些对象不使用完全相同的材料,但它们足够相似,则必须将它们合并为一个。这通常涉及创建共享的纹理图集,并更新单个对象的UV坐标以指向新的正确位置。下面会提到一些可以帮助你的工具

一旦你的对象使用相同的材质,便可以选择许多方法来批处理这些Draw Call。

我建议您使用的批处理技术取决于要批处理的对象的性质。

但是在进行批处理之前,让我们看看如何合并材质。

填写邮箱下载图表PDF高清版本

  • 全分辨率工作流程图
  • 图表的可打印版本
  • 还有Unity示例工程

现在下载

下一个…

前提:合并Unity 材质(Materials)

合并材质的第一个要求是:

img

要批处理的对象的必须使用相同的着色器材质

更换当前着色器(Shader)是你可以执行的最昂贵的操作之一。这会大大降低渲染速度。

几乎每个游戏都必须在某种程度上更换着色器,这很正常,现在你知道它的切换成本,就应该尝试减少项目中的着色器数量(包括着色器变体)。

如果你可以将两个相似的着色器合并到同一着色器中,那么你将获得巨大的性能优势。

因此,第一步是尽可能从项目中减少着色器。处理完成之后,你会得到许多原始材质,即使它们使用相同的Shader,看起来的效果和之前也没什么区别。

一旦目标对象使用相同的着色器,下一步就是合并其材质。这可能很复杂,因为他们可能具有不同的材质参数,例如:

  • 纹理:每种材质通常具有一个或多个与其他材质不共享的纹理。在不同材质上使用相同纹理的一种方法是创建包含所有单独纹理的较大纹理。这些纹理称为图集。
  • 材质参数:例如金属,镜面反射和其他参数。要合并这些值,你可以找到适合所有条件的共同平均值,也可以在特定通道中创建包含该值的纹理图集。你可以将3个或4个纹理通道用于不同的参数,例如将金属值存储在红色通道中。

现在,你有了多个具有相同材质的对象,但它们必须具有不同的参数,则可以把他们收集成为MaterialPropertyBlock。你可以为每个需要自定义参数的渲染器创建MaterialPropertyBlock,而不是创建单个材质实例。然后,你可以在每个Block中设置自己的参数。虽然这不会减少Draw Call的次数,但是会降低渲染的成本,因为你明确地告诉了Unity每个对象的不同之处。

为共享着色器的材质创建纹理图集通常遵循以下几个步骤:

  1. 创建一个大纹理,我们将其称为纹理图集
  2. 获取所有材质的纹理通道,并将其纹理复制到新创建的纹理图集中。
  3. 遍历使用这些材料的网格以重新计算其UV。新的UV将指向包含原始纹理的纹理图集的新的子区域。
  4. 禁用旧的网格,然后使用具有更新的UV的新网格
  5. 使用合并过的材质替换原本材质。
  6. 对着色器使用的每个纹理属性重复所有这些步骤。

我建议你在3d软件中执行此操作。如果有时间的话,这是最好的方法,因为它可以使你更好地控制过程。这可以提高输出质量,因为你可以调整关键变量,例如纹理像素分辨率。你还可以应用更高级的技术,例如调色板。

如果你没有时间、资源或经验去做这些事,那么我将带你走另一条路……

使用工具为你执行此操作。

作为程序员,我使用3D建模工具的效率不高,因此我经常使用这些方法。

以下是一些可用的Unity软件包,你可以使用它们在Unity中合并材质:

一旦我们将多种材质合并为一种材质,便可以开对Draw Call进行合批了。

你需要一些标准来帮助你在不同的批处理方法之间进行选择。

所有的批处理技术都是要付出代价的。他们都有一些限制,如何使用需要根据你的游戏情况来选择。

让我们按照顺序对其进行学习。

img

技术1:Unity静态批处理(Unity Static Batching)

静态批处理默认状态下处于启用状态(我建议保持这种方式)。

Unity自动将此技术应用于场景中共享材质的所有静态对象。我们来看以下示例:

  • 绘制静态的椅子1
  • 绘制静态的椅子2
  • 绘制静态的桌子

如果使用静态批处理,就会把他们合并成一个Draw Call:

  • 绘制静态餐椅(包含2把椅子和一张桌子)。

img

Unity Draw Call Batching 设置

你可以在Player Setting下找到这个设置,如图所示。选择您要启用的目标平台。

请注意,稍后在player settings中也会启用/禁用动态批处理(Dynamic Batching)。

更准确地说,Unity将查找启用了batching static标志的对象。然后,Unity将尝试合并公用材质的对象。

Unity静态批处理通过创建包含各个网格的巨大网格来工作。但是Unity也会保持原始网格的完整,因此我们仍然能够单独渲染它们。这样我们可以仅绘制可见视野内的对象,而丢弃不可见的对象,使得视锥裁切正常工作。

通过将所有网格都放在一个网格中,我们就可以在不更改渲染状态的情况下全部绘制它们。

静态批处理仅在你按下编辑器上的“Play”按钮之前发生,并且在构建时也会发生。Unity尝试遍历每个场景,并尝试批处理尽可能多的静态对象。

动图封面

Unity Draw Calls: Static Batching

静态批处理的主要限制是每批可以具有的顶点和索引的数量,通常为每个64k,可以在此处检查限制更新(如果有)。

静态批处理的缺点是增加了内存使用量。如果您有100个石头,每个石头模型占用1MB,则可以预期内存使用量将超过100MB。发生这种情况的原因是,巨大的批处理网格将所有石头一起包含在一个网格中。

但是,内存使用对你来说不是问题。毕竟,你可以查看我的Unity可寻址对象教程,该教程将帮助你节省大量内存。

img

技术2:Unity GPU Instancing

GPU Instancing是我最喜欢的批处理技术之一,因为它可用于非静态对象。

如果我们有这些Draw Call:

  • 绘制动态石头1
  • 绘制动态石头100

然后使用GPU Instancing将它们转换为一个Draw Call:

  • 在这里到那里画100个动态的石头…

这是针对每种材质激活GPU Instancing设置,如下所示。

img

Unity GPU Instancing:材质设置

GPU实例化让你可以非常高效地绘制相同的网格几次。Unity通过向GPU传递转一个Transform列表来做到这一点。毕竟,每块石头都有自己的位置,旋转和缩放。

与静态批处理相比,这是一项强大的技术,因为它不会激增内存使用量,并且不需要对象是静态的。

动图封面

Unity Draw Calls: GPU Instancing

要使用GPU Instancing,你只需要在材质检视面板中启用它即可。如果你有多个具有相同网格和材质的对象,那么Unity对它们将自动进行批处理。

但是,创建Transform列表会降低性能。如果在游戏过程中没有物体移动/旋转/缩放,则只需支付一次此开销。但是,如果对象每帧都更改一次,则需要每帧支付一次开销。

推荐一个插件:GPUInstance比Unity默认的要好用的多。

img

技术3:Unity动态批处理(Dynamic Batching)

如果你不能满足静态批处理和GPU Instancing的使用条件,你仍然有希望。

你可以对使用不同网格物体的动态对象进行动态批处理。

动图封面

Unity Draw Calls: Dynamic Batching

但是,请记住,Unity动态批处理受到更加严格的限制。你只能将其应用于具有少于300个顶点和900个顶点属性(颜色,UV等)的网格。材质也应使用single-pass着色器。此处有完整的限制列表。

出现此限制的原因是在运行时创建这些批处理的CPU性能成本。与单独发出绘图调用相比,超过300个顶点很难证明批量CPU的成本合理。

不仅如此,动态批处理非常不可预测。你无法真正确定对象将如何被批处理。结果通常会随着帧的变化而变化。打开Unity Frame Debugger并查看结果,在每帧之间动态批处理的结果发生巨大变化是令人困惑的。

我认为,这应该是你的不得已的方法。但是,嘿,它仍然是一种有用的工具,请不要忽略它。

img

技术4:Unity运行时批处理API

如果在特殊情况下,希望对批处理进行更好控制,可以用手动进行。

不用担心,你不必自己处理网格顶点。

Unity使你可以访问2个强大的API,以在运行时合并网格。

假设你正在开车。在车的内部,你会看到一些元素,例如座椅,把手,挡风玻璃和你收藏的咖啡杯。你可以在比赛开始之前设定这些元素。

一旦你做出选择并开始比赛,这些元素就会在你的赛车中变成静态的(无法再次更改)。让我解释一:

汽车本身是动态的。毕竟,你正在开车。

但是它所有不动的内部零件?可以认为它们相对于汽车对象是静态的。挡风玻璃将始终留在车内的同一位置。

但是,Unity认为所有这些都是动态的。这就是为什么在这种情况下无法进行静态批处理的原因。

尽管如此,我们仍然可以通过使用静态批处理API,手动创建合批。

最简单的方法是使用StaticBatchingUtility.Combine。该函数传入一个根游戏对象,并将遍历其所有子对象并将其几何形状合并为一个大块。一个容易遗漏的限制是,要批处理的所有子网格的导入设置必须允许开启CPU read/write

第二种方法是使用Mesh.CombineMeshes。此函数间接获取网格列表并创建组合的网格。然后,你可以将该网格分配给mesh filter渲染。

我简化了这两种功能的解释。查看文档以获取有关如何使用它们的详细信息。

在下图中,您将看到我如何应用StaticBatchingUtility API的功能在运行时将一些动态坦克批合并为一个网格。

img

Unity Draw Call:使用StaticBatchingUtility.Combine进行手动批处理

实践才是检验真理的唯一标准

我知道…

有这么多的可能性,好处和局限性,Unity中的Draw Call批处理最初可能会令人不知所措。

很难在一个小时内理解我花了多少年才掌握的东西。

但是你可以采取捷径。

这就是为什么我准备了一个资源包来指导你优化游戏的原因。

这是您将得到的:

  • 批处理解析图的全分辨率图像,包括可打印的版本
  • 带有四个场景的Unity项目向你展示了我如何应用四种批处理技术

介绍

收集了相关的Unity插件,供大家学习使用,如果用于商业活动,请购买相关版权

插件列表

1. A* Pathfinding Project

一个功能强大并且易于使用的 Unity 寻路系统。通过快速的路径寻找,将在点对点之间找到最合适的路径解决方案

2. Behavior Designer

适合于不仅仅是程序员使用也适合于产品+设计师,Behavior Designer 提供具有强大的 API 的直观可视化编辑器,帮您轻松创建新任务

3. DOTween Pro

可以用极少的代码处理复杂的动画效果,替代了我们在协程或者在Update里面撰写大量的逻辑控制代码

4. Easy Save

在实际开发中我们需要储存各种类型的数据,并想要数据得到保护,那么EasySave绝对是为这些需求量身定做的

5. Lean Touch

为手指触摸控制你的游戏提供极其简单的解决方案,这让你可以快速添加触摸控制到你的游戏-无需代码

6. Playmaker

这是unity3d最好的一款可视化编辑器,突破了非程序员止步于开发任务的障碍

7. Cross Platform Replay Kit

能在移动设备iOS和Android上轻松进行屏幕录制,可以在 Unity 上对你的游戏和其他你想要录制记录的画面轻松进行屏幕录制

8. BestHttp

提供包括Websock,Socket.IO,SignalIR,download/upload等特性。适合于大型网络游戏/项目,网络联机游戏项目

9. UFPS

快速构建FPS的模板项目
提取码:nyj6

10. Photon PUN+

是一款制作多人联机游戏的插件,同时支持PC端、手机端和网页端的Unity项目

11. Ultimate Inventory System

制作背包仓储系统的最佳插件
提取码:7eu5

12. Obfuscator

可以保护游戏代码和游戏资源,防止被逆向工程破解

13. Anti-Cheat Toolkit

保护游戏数据,免于被某些玩家作弊

14. SALSA LipSync Suite

一款制作唇语同步&面部动画的插件

15. Edy’s Vehicle Physics

可以为赛车类游戏提供各种逼真的真实物理效果

16. Editor Console Pro

取代Unity原生控制台编辑器的插件,生产力提升必备插件

17. Odin - Inspector and Serialize

一款可视化编程插件,可以让开发者序列化任何内容

18. I2 Localization

unity功能最全的本地化插件,可以本地化图片、文字、声音、图集、预设等资源

19. Rewired

可以帮助开发者轻松处理各种外设的输入,以控制游戏世界

20. Loxodon Framework Bundle

一个非常好用的AssetBundle加载器,也是一个AssetBundle冗余分析工具

21. QHierarchy

增强Hierarchy窗口的功能,极大提升开发效率

22. Koreographer Professional Edition

制作音乐节拍类游戏的必备神器

23. Master Audio

是Unity游戏开发的终极音频解决方案

24. Final IK

反向动力控制插件

25. PuppetMaster

高级的活跃布娃娃物理系统,带有布娃娃创建和编辑工具

26. Curved UI

资源商店中唯一真正的曲线界面系统

27. VR Interaction Framework

借助 VR 交互框架,轻松创建属于自己的可交互对象并迅速上手
提取码:kvse

28. Amplify Shader Editor

一款基于节点的可视化shader制作插件

29. Octave3D-Level Design

帮助Unity开发者制作关卡

30. Mesh Baker

用来合并Mesh和Materials,这样可以降低游戏对内存和显卡的消耗

31. Corgi Engine

一款制作2D/2.5D的平台游戏引擎,内部集成了若干有趣的游戏
提取码:jydg

32. Doozy UI Manager

Unity内部的一个UI管理插件
提取码:bbik

33. Fantasy Environment

包含了大量的幻想风格的游戏素材
提取码:xyqs

34. MapMagic World Generator

一款基于节点制作地形的插件
提取码:ssvw

35. TopDown Engine

Unity 最完善的自上而下解决方案
提取码:e5x7

36. Dynamic Bone

可给角色骨骼和关节增添物理效果

37. Feel

可以增加游戏感觉的插件
提取码:d341

38. Text Animator for Unity

让对话变得活泼,借助这一简单易用的自定义插件将游戏文本动画化

39. Dialogue System for Unity

可在游戏中轻松添加互动式对话和任务

40. Adventure Creator

帮助你制作传统的 2D、2.5D 和 3D 冒险游戏

41. All In 1 Vfx Toolkit

快速简单的创建出VFX
提取码:khgj

42. RPG Character Mecanim Animation Pack

1320个漂亮又专业的动画, 可兼容Xbox360游戏手柄, 以及基于物理学的人物控制器脚本

43. The Vegetation Engine

植物渲染资源包
提取码:m37x

44. Emerald AI

快速创建AI
提取码:noik

45. Enviro - Sky and Weather

非常好用的天气资源
提取码:mmat

46. UMMORPG

Unity最好的RPG游戏模板项目
提取码:y87r

47. UMOBA

Unity最好的MOBA游戏模板项目

吃人且崇尚祭祀的商朝是怎么完蛋的

2023年2月7日

0

116

文章原来的标题是《周灭商与华夏新生》,原作者李硕,载于《读库1205》。由于不是我自己写的,所以就不开评论了。文章很长,可以收藏起来慢慢看,希望大家喜欢。

上篇

文王八卦

据说周文王在忍痛吃掉了儿子的肉之后,才被商纣释放。这似乎流于野史传说。但在商人的殷墟遗存和甲骨文献里,这种行为再平常不过……

公元前一千余年,《旧约》中以色列大卫王之世,《封神演义》的传说时代。正当壮年的商纣王君临“天下”,统治着亚欧大陆最东端的华北平原。

吃人且崇尚祭祀的商朝是怎么完蛋的

商朝形势图

此时的周文王,只是一个远在西陲(今天陕西)的小小部族酋长。好几代人以来,周族都臣服于商朝。文王周昌已经年过五旬,在那个年代已经是十足的老人,且又痴迷于怪异的八卦占卜,更给这个撮尔小邦笼罩了沉沉暮气。

一支商军突然开到西部,逮捕了周昌,将他押解往商朝都城——朝歌。这是商人一次惯常的惩戒征讨。数百年来,商王对于他征服之下的数百个邦国、部族,都是这样维持统治的。

这次的结果却迥然不同。

尘封梦魇

三千年后的今天,河南安阳殷墟,黄土掩埋着殷商王朝最后的都城:朝歌。

吃人且崇尚祭祀的商朝是怎么完蛋的

河南安阳殷墟

一个世纪以来,考古学者在这里发掘出了数量惊人的被残杀的尸骸,一起出土的甲骨文显示,他们死于商人血腥的祭祀典礼。累累骸骨告诉世人:这里掩埋了被忘却的血腥文明,梦魇般恐怖而悠长的岁月。

在殷墟一座宫殿旁边,发掘出一百多座杀人祭祀坑,被杀人骨近六百具。这些尸骨大都身、首分离,是砍头之后被乱扔到坑里。两个坑内还埋着十七具惨死的幼童。这座宫殿奠基时也伴随着杀人祭祀:所有的柱子下面都夯筑了一具尸骨;大门则建造在十五个人的遗骨之上,其中三人只有头颅。

吃人且崇尚祭祀的商朝是怎么完蛋的

1930年代殷墟发掘时的照片

商王陵墓区有一座人祭场,比操场大两倍以上,出土近3500具人骨,分别埋在九百多个祭祀坑中。尸骸很多身首异处,有些坑中只埋头骨,或者只埋身躯,甚至是在挣扎中被掩埋的活人。王陵区之外也有人祭现场。比如后岗一座坑内,埋着73具被杀者的骨骸,大都是20岁以下的男性青少年,甚至有十多具幼儿的尸骨。商人文化所到之处,如河南偃师、郑州的商代早期遗址,甚至东南到江苏铜山,也都有大型人祭场的遗址。

多年的自然变迁和人工已经破坏殷墟遗址,整个商朝共有过多少这样的人祭现场,就无法确知了。这些遗址时代早晚不同,说明人祭的做法曾延续了很多年。它绝不是某位暴君心血来潮的产物,而是一个文明的常态。

但在被考古学家的铲子揭露之前,中国古史文献从来没有提及商人的这种习俗。

吃人且崇尚祭祀的商朝是怎么完蛋的

安阳殷墟王陵遗址内的祭祀坑

文王之子——周武王灭商之后,朝歌城被废弃、掩埋,商人的这种风俗也消散如云烟。但周朝人又为什么删除了对那个血腥时代的记忆?这和他们的兴起、灭商、建立周朝又有什么关系?

甲骨文和考古发掘向我们提出了这些问题。如果尝试解答它,还必须从上古的儒家经书、古史文献中,搜罗吉光片羽般珍稀飘渺的信息,将它们和考古材料拼合,还原那湮没三千年的恶梦——不,事实。

吃人且崇尚祭祀的商朝是怎么完蛋的

安阳殷墟王陵遗址内的车马坑

商朝和它的臣虏:羌、周

商人兴起于东方。他们统治的核心区在今日河南省东北部,属于华夏世界的东方。对于西部的异族,商人称之为“羌”,甲骨文这个字形如大角羊头,代表居住在山地、放牧牛羊为生的人群。这只是一个泛称,“羌”人包含着无数互不统属的松散族邦、部落。

吃人且崇尚祭祀的商朝是怎么完蛋的

甲骨文中“羌”的三种写法

商纣王之前二百年,一位商王的王后“妇好”率军征讨西方,把商朝的势力扩张到羌人地区。那次远征在甲骨文献中的规模最大,全军有一万三千人。和西部蛮族相比,商人有先进的青铜冶炼技术,兵器坚固锋利;他们还有记录语言的独特技术:文字,由此组建起庞大军事和行政机器,以及高度分工的文明。这都是蛮荒部族无法想象的。

吃人且崇尚祭祀的商朝是怎么完蛋的

妇好墓

吃人且崇尚祭祀的商朝是怎么完蛋的

妇好率领一万三千军队伐羌方的卜甲:

辛巳卜,贞,登妇好三千登旅万,呼伐(羌)

商人从没有用自己的文化改变蛮夷的想法。他们只想保持军事征服。商王习惯带着军队巡游边疆,用武力威慑周边小邦,让他们保持臣服,必要时则进行杀鸡儆猴式的惩戒战争。商朝的本土并不比今天的一个河南省大太多。

对于“周”这个西方部族,商人有点说不清它的来历,因为它太渺小了。周人史诗讲述了自己的早期历史,也混杂了大量神话。传说周族始祖是一位叫“姜嫄”的女子,她在荒野里踩到了巨人的足迹,怀孕生子后稷,繁衍出了周人氏族。商周语言中,姜就是羌,所以周人也属于广义的羌人,他们形成部族后,才给自己冠以“姬”姓,而把周围其他部族称为“姜”姓。这标志着他们之间的血缘关系已经疏远,可以相互通婚。按照西方的风俗,同姓、同族的人不能通婚。

到文王周昌的祖父——古公亶父一代人,才有了比较可靠的记载。周人原来生活在深山之中,和野蛮民族(其实就是他们的近亲羌人)没什么区别。古公亶父带着族人迁出深山,沿着一条小河来到渭河平原的边缘,开始进行农业垦殖,从此脱离野蛮,进入了一种更“文明”的生活方式。

这些史诗掺入了周人的自我夸耀,只是部分可靠。从考古发掘看,这个时期关中渭河流域的文明形态都差不多,各族邦都不过几千或万余人,过着种植谷子、高粱,饲养牛羊的生活。他们最主要的农具是磨制石器,居家使用粗糙的灰陶,上层族长才有一点外地输入的奢侈品,比如玉器和铜器。周人并不比羌人邻居们“文明”多少。在商人眼里,他们都同样落后,根本不是值得尊敬的对手。

古公亶父带给周族的最大变化,是他投靠了强大的商王朝,成为商人在远西地区的统治代理人。

吃人且崇尚祭祀的商朝是怎么完蛋的

铜戈

河南安阳小屯村殷墟妇好墓出土

吃人且崇尚祭祀的商朝是怎么完蛋的

铜圆斝

河南安阳小屯村殷墟妇好墓出土

吃人且崇尚祭祀的商朝是怎么完蛋的

妇好铜圈足觥

河南安阳小屯村殷墟妇好墓出土

吃人且崇尚祭祀的商朝是怎么完蛋的

妇好方鼎

河南安阳小屯村殷墟妇好墓出土

在彼时,周族不过是个万余人的小部族,对统治着数百万人口的庞大商朝有何用处?

正如殷墟考古发掘所揭示,商人相信,上帝和祖先神灵主宰着人世间的一切祸福,而异族人的血肉,则是奉献给上帝和祖先的最好礼物——甲骨文中的“祭”字,就是一只手拿着肉块奉献于祭台。他们祭祀用人最主要的来源,就是羌人。甲骨文的人祭记载中,羌人占了被杀者的一大半。他们被称作“人牲”。

吃人且崇尚祭祀的商朝是怎么完蛋的

甲骨文“祭”

亶父带领周族投靠商人之后,最主要的职责就是为商朝提供羌族人牲。这是被后来周人刻意掩埋、忘却的历史,但出土甲骨文泄露了一点信息。

周族自己没有文字。甲骨文“周”字是商人所造。商人对杀人献祭有一个专门的动词:“用”。无数片关于祭祀的甲骨文都记载,商王“用”羌人男女和牛羊奉献神灵。甲骨文中的“周”,是“用”和“口”两个字的合写;《说文解字》对“周”字的解释也是“从用、从口”——在商人看来,“周”族特征,就是缴纳供“用”的人口。

商人的“周”字还有一种更可怕的写法:“用”字的小方格中点满了点。甲骨文这种点代表鲜血,它来自被杀的人牲,是神明最新鲜的饮食。甲骨文还有专门描绘用鲜血献祭的字:一座凸起的祭台上,用点表示的血液正在淋漓滴沥下来。

吃人且崇尚祭祀的商朝是怎么完蛋的

刻辞卜甲

殷墟博物馆藏

从血缘关系讲,古公亶父和周人的这种行为,是对家乡族人的无耻背叛。靠着捕猎羌人,周族成了商朝在西方的血腥代理人,也得到了相应的报酬。锋利的铜兵器可以帮助他们捕获猎物;商人马拉战车的军事技术,可能也在这个时候输入了周族。

亶父以来三代人、近百年时间里,周人都在努力趋附商朝。按照传统婚俗,周族首领应当隔代迎娶姜姓的夫人。亶父的夫人就来自羌人,说明在他当年结婚时还没有背弃西方盟族。但他的儿子季历、孙子周昌(文王),两代人都是从东方迎娶夫人,这表明了他们投靠商朝的姿态。

周人宣称这两位夫人都是商人,甚至是商王之女。这只是他们对周边羌人的吹嘘。商人实行族内婚,严密保护着自己高贵血统的纯洁性,绝对不会将王室之女嫁给远方蛮夷。商人的姓是“子”,而季历和周昌的两位夫人,分别姓“任”和“姒”,她们只是来自臣服于商的外围小国而已。不过任、姒两位夫人的母国,还是比周人先进的多。在周人眼里,她们俨然是从天界下凡的女神一般,后世史诗中充满了对她们的歌颂声,甚至称她们为“大任”、“大姒”(《诗经·大雅·思齐》、《史记·周本纪》)。

两代东方新娘给周族上层带来了巨大变化。丈夫可以不懂妻子家族的语言,但母亲必然会全面影响儿子一代。东方文化随她们来到西部,最神秘、“先进”的当属甲骨占卜之术,它融合文字、占算和沟通鬼神的通灵术于一身,被商人发挥到了极致。其中,对卜骨纹路进行解读和运算的部分属于“八卦”。到文王周昌老年时,开始痴迷于这种来自东方的神秘运算技术。由此,周人和古中国的命运开始发生转折。

吃人且崇尚祭祀的商朝是怎么完蛋的

文王野心:八卦

文王周昌年幼时就继承了族长之位。实际上,他的父亲季历很可能早夭而没有当过族长。季历的妻子、周昌的母亲大任来自东方,商朝显然支持幼年周昌继任周族之长。他成年后继续从东方迎娶妻子大姒,也是沿袭祖父亶父以来投靠商朝的政策,同时保障自己的权威。

掌握甲骨占卜和八卦推算技术的,都是巫师家族,他们世代传承此职,将其作为家传绝技秘不示人。后世人传说,周文王在被商纣囚禁期间,将八卦推衍为六十四卦,这种说法也许有一定来历,但周昌接触和演算八卦的开端肯定更早。可以想象,当老年周昌对“八卦”发生兴趣后,肯定对占卜师软硬兼施,采用了各种手段,终于迫使他们交待出了卦象运算原理。

吃人且崇尚祭祀的商朝是怎么完蛋的

商、周时代,偶或有沉迷占卜之术的上层人士,但老年周昌的惊人之举,就是从中获得了背叛商朝、取而代之的启示。这显然远远超出了作为商人臣属的本分,而且背离了自祖父亶父以来的立国之本。

周昌究竟是如何推衍、论证的,现在已经不得而知。但现存《周易》中的《彖辞》部分,据说就是文王周昌所作,其中有些语言确实显露出不臣之心,比如“宜建侯”、“履帝位”、“建侯行师”这类语言,已经明显超出了臣子本分,充满反逆杀机(屯、履、豫卦)。

多个卦的《彖辞》都显示,“东北丧朋、西南得朋”。东北方不利而机会在西南。商人统治中心河北,正是周人的东北方,这无疑预示着和商王决裂之机已到,需要联络西部羌人、甚至西南方深山的各族为同盟军。而后来武王灭商时,西南民族蜀、髳、微等确实参战(坤、蹇、解卦);文王《彖辞》中出现最多的,是“利涉大川”一词——从关中到商都朝歌,必须渡过黄河,习惯山居的周人不习水性,这显然是老年周昌最关心的问题(需、讼、同人、蛊、大畜、益、鼎、涣、中孚等卦)。

沉溺在卦象演算中的周昌忽视了一点:他求教的占卜师来自商人控制的东方,他们和故乡的同行有密切联系。周人老族长的不臣之心,完全有可能通过占卜师的通信网传向朝歌,而商朝首席祭司又是商王的心腹。于是,商朝军队带走了老周昌。

吃人且崇尚祭祀的商朝是怎么完蛋的

卜骨 商 武丁时期

传河南安阳小屯出土

中国国家博物馆藏

《史记》等文献完全没说这是一场战事。也许商军像以往征收羌族人牲一样来到周族,顺便带走了周昌。从当时的实力对比看,老周昌的造反念头实为异想天开。所有周族人,包括他的儿子们——后来的武王发、周公旦等等,显然都被这个想法吓坏了。商人军队执法般轻而易举地带走周昌,足以说明周人被震慑之深:他们根本没有追随首领、对抗商人的实力和勇气。

周昌被捉走,把所有的难题都留给了儿子们。夫人大姒为周昌生了好几个儿子,长子伯邑考,次子周发、周旦此时已经成年。他们唯一能做的,就是去朝歌向商纣王求情,祈求他宽恕周昌因老迈糊涂而产生的妄念。

《史记》记载,几个从商朝叛逃到周的臣子(闳夭、散宜生等),带着礼物到商都祈求纣王。这显然不是全部实情:见到叛臣只会增加商纣王的愤怒,何况此时周族也难以吸引到商朝的投诚者。商纣是异常聪明的人,“知足以距谏,言足以饰非”,周昌的儿子们不出面,他肯定不会宽恕周人(《史记·殷本纪》)。

吃人且崇尚祭祀的商朝是怎么完蛋的

刻辞卜骨

殷墟博物馆藏

文王诸子这次去朝歌的屈辱经历,只是在他们灭商、夺取天下之后,才被隐讳了起来。事实上,他们在朝歌经历的远不止是委屈羞辱,更是如梦魇一般的血腥惨剧。

天邑商:朝歌鬼神世界

旧史的零星记载说,周昌长子伯邑考到朝歌之后,被商纣王处死且做成了肉酱。周昌在忍痛吃掉了儿子的肉之后,才获得释放(皇甫谧《帝王世纪》)。这确实显得过于荒唐,似乎只能流于野史。但有了今天殷墟的考古发现和甲骨文献,我们才知道,这种行为对于商人再也正常不过。

以往数十年里,周人一直在向商朝提供羌人俘虏。对于这些人在朝歌的命运,周人可能有一些模糊的了解,却不会有太具体的观感,因为西部并没有商人的人牲祭祀场。只有在老周昌和儿子们相继到达朝歌之后,才亲眼目睹了那些经自己之手送给商人的俘虏的下场。

按照甲骨文记载,商人用活人献祭的方法有很多种。比较常见的是“卯”祭,这个字是人或牲畜被掏空内脏之后、对半剖开悬挂的形状,如同今天屠宰流水线上悬挂的猪羊。事实上,羌人俘虏也确实常和牛、羊一起被杀死“卯”祭。

吃人且崇尚祭祀的商朝是怎么完蛋的

甲骨文拓片

其他献祭方式包括奉献人牲的内脏、鲜血、头颅。加工人牲方法有烧烤、滚汤炖烂、风干成腊肉等等,都有专门的甲骨文字。这都是加工食物的方法,因为他们就是奉献给神灵的饮食。按照习俗,神明享用祭品时也施加了祝福,所以典礼结束之后,献祭者将分享祭品。

这自然会得出一个惊悚的推论:商人,特别是上层商人,很有可能是食人族。但这并非只有考古证据。历史文献中除了伯邑考被做成肉酱;另一位对纣王有异心的小国君“鬼侯”也被做成了肉干,分赐给其他邦君为食。

按商人观念,异族的酋长、贵人是最高级的人牲,他们给这种酋长叫“方伯”,再多的普通人牲也抵不上一位方伯。周昌或者他的继承人,正是商人眼里的一位“羌方伯”。

吃人且崇尚祭祀的商朝是怎么完蛋的

甲骨文拓片

但这次被“用”的为什么是伯邑考,而不是他的弟弟武王发、周公旦,或者惹出这场风波的老周昌自己?

在犹太《旧约》里的上古时代,上帝最喜欢接受长子作为祭礼。商人未必有这种礼俗,但他们确实喜欢用青壮年男子或儿童献祭,极少用老年人(对某些特定的神则用青年女子)。而且,商人习惯用占卜选择祭品,他们应当对伯邑考、周发、周旦等兄弟进行了认真考察和占算,来确定谁最适合做成肉酱。毕竟,用来祭祀的牛、羊事先也要认真检查,看它们的毛色、肥瘦,以及有没有疤痕、暗病,这种记载在《春秋》中屡见不鲜。老周昌的儿子们如何经历过这一关,他们的感受如何?旁人将永远无法得知。

无论如何,老周昌重获自由。而且,他和儿子们还有了意外收获。

吃人且崇尚祭祀的商朝是怎么完蛋的

吃人且崇尚祭祀的商朝是怎么完蛋的

青铜甗里装着蒸熟的人头,

殷墟不止一次出土过这种祭祀用品

首先,商纣王对他们的悔过非常满意,尤其是周昌吃下自己儿子肉的表现。这大概象征了他衷心归化于商人文明世界的姿态。纣王授予周昌“西伯”身份,让他代表商朝管理更大范围的西方事务。

还有,在这次朝歌之旅中,周昌父子获得了面对面观察商人高层的机会。除了那些足以让人疯狂的血腥祭祀,他们还发现,商朝远不是他们在西陲时想象的“天邑商”——如同仙界般悬浮在天空的神圣都市。这里虽然富丽堂皇,但所有的人,从商纣王到他的兄弟子女亲人,都和周人一样普通,没有任何神圣之处。

最关键的是,商人世界并非一个团结的整体。和任何一位族长、首领一样(甚至更加严重),纣王身边充斥着心怀不满的兄弟和宗族成员,他的儿子们为争夺继承权明争暗斗。闳夭、散宜生等向周人暗送秋波的商朝臣子,应当是在这时和周昌父子们建立联系的。周武王灭商之后扶植的傀儡、商纣之子武庚,此时肯定也对周人进行了试探拉拢,更不用说商纣那些早已心怀不满的叔伯兄弟们,比如稍后被处死的比干。在这些人看来,周族人和他们那些西方亲属羌人部族,也许是可以利用的潜在力量。如果商纣王一意孤行、不尊重这些贵族的利益,就有必要联络异族,里应外合发动政变。

商纣王和他身边的觊觎者们,都没有想到扶植周族可能带来的危险。

商人称霸中原已长达六百年,从没有外来威胁可以动摇它的统治。而且,商人一致认为,天界的上帝、诸神主宰着人间一切祸福命运。已经死去的历代商王、贵族,也都进入天界成为神灵,拥有大小不一的神力。那些神灵非常“现实”,只保佑向他们献祭的人。奉献的人牲、牛羊越多,诸神就越高兴,会保证献祭者享受人间的一切。

商王最重要的工作,就是向天地、山川、祖先之神不停献祭,祭祀日程表排得满满当当,如同营养师的菜单。在甲骨文记载中,商王会一次宰杀、奉献三千名人牲,以及一千头牛。能够保存到现在的甲骨文只是九牛一毛,这肯定不是商人规模最大的祭祀。

由于商王垄断了向诸神祭祀的权力,也就独享了诸神的福佑,理所当然要征服、统治大地上的所有民族。当然,这也是为了给诸神提供更多的祭品。

在这种思维方式下,商人自然成为了一个以纵欲著称的民族。向神明献祭的人和民族就可得到天佑,于是不必顾及什么道德戒律,更不必担心未来的忧患。《史记》记载了纣王建造酒池肉林、男女裸体集体淫乱等种种荒唐行为。其实,这和他敲骨看髓的故事一样,都是将整个商族的丑恶集中到了一个人身上。种种酷刑、血腥的杀祭,都是商人集体而非纣王一人的娱乐方式。

吃人且崇尚祭祀的商朝是怎么完蛋的

玉人

河南安阳小屯村出土

吃人且崇尚祭祀的商朝是怎么完蛋的

玉玦

河南安阳小屯村北出土

他们还从上到下沉溺在酗酒恶习之中,终日少有清醒的人。纣王在位以来,来自西方的人牲数量在减少,但作为酿酒原料的粮食在不断增加(周昌怠工以后,纣王正试图在东南方开辟新的人牲来源)。

商王之下的贵族们死后成为小神,但他们也必须保佑后世商王,不能只顾及自己的子孙。在纣王之前二百多年,商王盘庚刚刚把都城迁到朝歌,他身边的贵族们大都不满。盘庚将他们召集起来训话,公然威胁说:不要以为你们死去的祖先会帮助你们,因为他们都在我先王的身边,跟着享受了我奉献的祭品,所以会优先保佑我盘庚,不会纵容你们!

兹予大享于先王,尔祖其从与享之。作福作灾,予亦不敢动用非德。予告汝于难,若射之有志!

据说商人早期是经营畜牧和商业的民族,所以他们把被统治的人视同牲畜,并且用生意人的思维和诸神打交道(《山海经·大荒东经》,《世本·作篇》)。商纣王觉得天下是他一人的产业,其他商人贵族也认为王位只能在商人内部传承。周人只是他们的工具而已,永远没有爬到主人位子上的可能。

太公阴谋

在周昌父子们周旋活动于朝歌时,他们也许还遇见了一位后来共同参与改写历史的人物,就是太公吕尚——后世所谓的“姜太公”。他族姓为姜,属于周人的传统盟族,羌人。

吃人且崇尚祭祀的商朝是怎么完蛋的

姜太公画像

《史记》说太公吕尚是“东海上人”,在渭水边垂钓遇到文王而被重用。这种叙事模式来自《战国策》的说客故事,不足采信。更晚的野史小说《封神演义》,则有姜太公曾在朝歌城里卖面粉、当屠户的故事。在商周之际,世袭阶级身份是不可能改变的,根本不会有出身平民的暴发户。太公必然出自羌人中的吕氏部族,是一位典型“羌方伯”之子。

但这并不排除太公曾有在朝歌生活的经历。《史记》中记载确凿的,是姜太公在后来周人的灭商事业里作用巨大,特别是提供了许多阴谋秘计,“其事多兵权与奇计,故后世之言兵及周之阴权,皆宗太公为本谋。”这种阴谋算计,和羌人、周人在西陲山地的简单淳朴生活格格不入。只有“文明”世界才能塑造出如此阴沉工于心计的人。

那么,出身羌人上层的太公吕尚,为何有着如此复杂难以捉摸的经历,并最终和周人走到了一起?

结合周人以往为商朝所作的工作,可以推测,太公作为羌人吕氏部族的首领之子,可能是被周人俘获或者诱捕,然后作为人牲送到了朝歌。那时的太公和文王都还年轻。但某些变故使他侥幸保住了性命(比如占卜结果并不适合作祭品等),便在朝歌城内作为一名贱民生活下来,直到见到了被押解来的老周昌和追随而来的儿子们。

吃人且崇尚祭祀的商朝是怎么完蛋的

朝歌,今河南省淇县

如此的话,老年太公和周昌在朝歌城内的再次会面,一定极富戏剧性,特别是在老周昌父子们经历了作为“羌方伯”的种种遭遇、伯邑考被“用”之后。这次相见的细节已混淆在种种传说中无法复原,但结局很清晰:这些有着同样惨痛经历的人达成共识,太公谅解了周族人以往的暴行,认可了老周昌的灭商梦想——虽然动机来自他未必理解的八卦推算。他悄悄和周昌父子们一起回到了西部,共同投身到灭商大业中。

带着在朝歌的惊悚、悲哀、新知和收获,老周昌和剩余的儿子们回到了故乡。他们离开时只有忧虑绝望,归来时却已经团结一致,带领全族投入了这桩豪赌事业:翦商。这个事业已经裹挟了包括周人在内、从东方商都到西部远山的各种政治势力,一旦开启就不可能中止,如同置身深山峡谷中的漂流之舟,或者苦撑到辽阔富饶的新家园,或者在激流乱石中撞得粉身碎骨。

这桩事业中,新加盟的太公吕尚为周人提供了极大帮助。司马迁《史记》记载,太公给文王周昌、武王周发父子策划的,都是阴谋诡计、密室之谋,大多没有记载下来。但他能给周人的教益不止于此。

和周人、羌人相比,商人的文明更加发达,分工专业化程度和生产效率更高。以太公可能在朝歌城内从事过的屠宰业为例(倒不仅是来自《封神演义》的戏说,在很多早期文明中,屠夫职业确实与贱民身份密切相关),商都的这个产业早已脱离了小作坊经营阶段。屠宰完的人牲肉、骨利用很充分,不同部位、器官被分拣归类,进入下一轮生产环节。在1930年代发掘的殷墟手工工场区内,有专门加工人腿骨的作坊,经过初步拣选的成年人腿骨被捆扎在一起,等待下一步精细加工,可能是制作束头发的骨簪。在其他的商代作坊区中,还有专门用人头盖骨制作碗的遗迹。周人不会这样利用人骨,但这种分工、专门化的生产方式,则是太公能够带来的真正进步。

吃人且崇尚祭祀的商朝是怎么完蛋的

骨笄

殷墟博物馆藏

吃人且崇尚祭祀的商朝是怎么完蛋的

吃人且崇尚祭祀的商朝是怎么完蛋的

骨簪

河南安阳小屯村殷墟妇好墓出土

此外,年轻的周发(武王)还娶了太公的女儿,周公旦可能也娶了另一位姊妹。由此,周人重续了和羌人的世代婚姻,两个亲缘部族终于在灭商大业之下团结起来。

下篇

朝歌城的经历、长兄的惨死,显然给武王造成了无法愈合的精神创伤。他的后半生都无法摆脱失眠和噩梦的困扰。

周命维新

从朝歌返回之后,老周昌对翦商事业非常乐观。他的创意终于得到了儿子和族人的响应,他们看到了商人内部的裂痕,还获得了太公为代表的羌人同盟军。再加上卦象显示的各种预兆——目前族人们还不懂如此高深的玄机,但他们早晚会为之折服——翦商大业注定前途光明。

周昌甚至按照朝歌的排场给自己加了王位。从此,他才成了和商纣王平等的王、历史上的“周文王”。当然,这只是在周人的小范围内,悄悄瞒着商纣王的耳目。

吃人且崇尚祭祀的商朝是怎么完蛋的

明·朱天然《历代古人像赞》中的周文王

从朝歌回来之后,文王的身体还算康健,记忆力却迅速下降。后来周人史诗说他“不知不识,顺帝之则”,其实是典型的老年痴呆症状(《诗经·大雅·文王》)。

这些已经不重要,因为他有限的时间和智力,都已投入了将八卦演算为六十四卦的工作,这也许是他解除丧子之痛的唯一方式。后世卦师们的衣食之源——《周易》由此产生。

但这对于翦商事业没有任何助益,具体工作都由儿子们进行。除了丧命商都的伯邑考,现在成年的只有周发和周旦。对于老周昌一意孤行开创的这桩事业,他们依旧视为畏途。

和庞大、发达的商王朝相比,周族力量毕竟太弱小了。周旦(周公)性情柔弱,从不敢质疑父亲的决策,但也无法胜任太多建设性工作。周发则努力担负起这桩事业,这应当是他被文王指定为继承人的重要原因。

周昌父子的翦商事业,已经被古代经学家、现代历史学者讲述过无数遍。他们举族迁往更适合农业种植的平原地区,借着商纣王授予的“西伯”头衔,拉拢、团结周边羌人等部族,对不愿服从的部族、方国则进行武力征服。

周人扩张非常迅速,他们的势力甚至开始伸展到关中之外。被征服者提供了衣食资财,使周族男子得以从生计劳碌中解脱出来,组建全民皆兵的武装。周人传统的氏族、家支都被打散,青壮年在军事单位中重新编组。

在扩张过程中,周人还创立了“大学”,也叫辟雍或明堂。这个最早的大学的事业,不是学习研究文化,而是对所有周人男子进行军事训练,最基本的必修课是射箭,最先进、难度最高的则是驾驶战车作战。

吃人且崇尚祭祀的商朝是怎么完蛋的

北京国子监辟雍 李乾朗 绘

在经典文献的描述中,辟雍是一座环水的高大建筑,其实就是护城河环绕的武装堡垒。周王和儿子们都居住在堡垒中。这座辟雍成为周人征服南北西东的力量之源:

镐京辟雍,自西自东,自南自北,无思不服。皇王烝哉!——《诗经·大雅·文王有声》

从朝歌返回之后,文王周昌又活了九年。他去世后,周发即位自称武王,但仍然继续文王的纪年。按照他的解释,父亲的在天之灵依旧指导着翦商大业。

周公解梦

但武王周发始终生活在恐惧和焦虑中。

朝歌城的经历,特别是长兄伯邑考的惨死,给他造成了无法愈合的精神创伤。再加上翦商事业的压力,担心失败的恐惧,使他的后半生都无法逃脱失眠和噩梦的困扰。

《逸周书》中以多个以“寤”为题的篇章,都记载了武王的恶梦之痛(《寤儆》《和寤》《武寤》《武儆》)。他常常辗转终夜无法入眠,黎明时分恍然睡去,却又梦到翦商之谋泄露、商纣王震怒,联络好的盟友们都不敢反抗,整个周族旋即遭受灭顶之灾:

呜呼,谋泄哉!今朕寤,有商惊予。欲与无□,则欲攻无庸,以王不足,戒乃不兴,忧其深矣!——《逸周书·寤儆》

每次他从恐惧中醒来,都要派身边的小臣去请弟弟周公,向他讲述梦里的惨状,以及对谋商事业能否成功的忧虑。商王家族世代向上帝献祭,他们肯定能得到上天的保佑,试图翦商是否是逆天悖伦之举?

对于这种恶梦,周公也只能尝试用梦来缓解。他宽慰说,他们的母亲大姒曾梦到商都朝歌生满了荆棘,这就是上天降下的商人将亡之兆。虽然上帝享受了历代商王的祭祀奉献,但他不应该因为这种小小的实惠而偏袒商王。

吃人且崇尚祭祀的商朝是怎么完蛋的

明·朱天然《历代古人像赞》中的周公

为了使自己的解释圆满,周公一次次进行发挥和阐释:王的使命,应当是使天下所有的人生活在和平、公正之中,这就是所谓“德”。上帝应该只保佑有“德”之人,替换掉没有“德”的君王或王朝,以有德之人代替之。只要武王努力修“德”,就一定能在上帝福佑之下战胜商王(《太平御览》引《周书·程寤》,《逸周书·大开武、小开武》)。

武王从未能真心信服这种解释,恶梦一直陪伴他到成功灭商以至去世。如果真有那位全知全能的上帝,长兄伯邑考为什么还会惨死在朝歌?

他宁可相信实力决定一切。只有在战场上彻底消灭商朝军队,周人才能从恐惧中解脱出来。所以武王真正信赖重用的是岳父太公。每天晚上,他都在和岳父密谋富国强兵的种种方案,拉拢周边小邦、分化商人高层的种种策略。

但密谋结束之后,他依旧会辗转反复无法入眠,朝歌人祭场的一幕幕在眼前挥之不去,惨死兄长的魂灵随时会降临他的卧室。每次从恶梦中挣扎而醒时,窗外已开始泛白,弟弟周公正守候在榻边。

周公名“旦”,字形是半轮太阳正从地平线上升起,意为清晨。他确实是武王在每个恶梦之晨看到的第一个人。武王的侍卫亲随——“小子御”早已习惯,看到他失眠和恶梦,不待指令也会向周公求助。

吃人且崇尚祭祀的商朝是怎么完蛋的

青铜刀 西周

中国国家博物馆藏

吃人且崇尚祭祀的商朝是怎么完蛋的

青铜戈 西周

中国国家博物馆藏

于是,武王在周公的宽慰鼓励中稍稍振作,开始新一天的工作。史书没有记载,周公自己是否逃脱了噩梦的纠缠,以及他自己是否相信那些关于“德”的说教。但每个黎明前被兄长召唤的时刻,他都从容清醒如白日。周公显然已认真考虑过自己的定位:他无力承担父亲开启的正义而疯狂的事业,也无法给死去的长兄报仇。但这个使命和它带来的压力,注定要由他们兄弟二人一起承受。

他对“德”的阐释,只是作为普通人的美好愿望:他们不想无故被杀或者杀人,只渴望生活在一位圣明君王统治下的安定中。但和所有普通人不一样的,是他的兄长周发必须成为那位有“德“君王。不然整个周族将死无葬身之地。

如果说武王的使命是成为帝王、翦商和建设人间秩序,那么他周公旦的使命,就是做这位帝王的心理辅导师,塑造和维护他的伟人形象,如此便于愿足矣。

牧野鹰扬

文王死去两年之后,武王终于集结兵力,发动了对商朝的进攻。

但是,当他们到达黄河边后,忽然又停止进军,班师撤退。第一次出征草草结束。

周人和盟友都不理解武王的想法。其实,武王曾多次和太公、周公秘密讨论:以周人现有的兵力,完全无法对抗商军,要征集更多的部族做同盟军,则势必泄露翦商之谋,这显然是一个两难的处境:

“余夙夜维商,密不显,谁和?”(《逸周书·大开武》)

在两者间权衡取舍许久之后,武王终于决心发起这个冒险之举:公开与商朝决裂,并发动一次有限的试探进攻。这是他向所有被商朝统治的部族发出的振臂一呼:已经有人率先揭竿而起,亮出你们立场的时候到了!

当独夫暴君得意之时,似乎所有人都屈服于他的淫威。但只要第一个、第二个反对者站出来,他们身后会立即涌现一支追随者大军。被血腥人祭摧残已久的部族们纷纷赶来投靠周人。沿途加入周军的“诸侯”——部族和小国,其实多数不过是新石器水平的农业聚落——多达八百个。

这些未经统一训练的乌合之众是无法作战的。所以武王及时退回了关中。他需要时间把这些新盟友们锻造成一支更大的军队。

商纣王本该用雷霆之怒来惩戒周人的叛逆,如同十二年前逮捕文王一样。但他立刻发现,哪怕在商朝内部,他的权威也在迅速下降。对他公开表示不满的高官和亲人越来越多,推翻他的阴谋正在宫廷中酝酿。他忙于扑灭朝歌城内的反对派,处死了叔父比干,关押囚禁了更多的人。越来越多的商朝臣僚叛逃入周,带来了朝歌反对派们求援的呼声。

又经历了几百个不眠之夜后,周武王发动了真正的远征。西部联军沿着当年文王被捉入朝歌之路前进。

刚刚压平国内反对派的商纣王也集结起了大军,准备一举荡平周人和所有的叛逆民族。双方在朝歌城外的原野——牧野集结,即将发起决战。

吃人且崇尚祭祀的商朝是怎么完蛋的

牧野之战示意图

这个彻底改变中国历史、再造华夏文明的日子,在文王周昌被抓到朝歌的十三年之后,公元前1046年二月一日的凌晨。双方军队连夜集结备战。连绵篝火映红了旷远夜空,人和牲畜的走动喧哗声终夜不休。

严冬即将过去,淡淡晨雾飘散在原野间,枯草上凝结着闪亮霜露。当天空现出幽深的蓝色——这个武王每每从恶梦中惊醒的时刻,双方军队列阵完毕。

周人和他们的同盟军,总共四万五千人;至于商纣王集结的军队,则像树林一样多的无法计算,“殷商之旅,其会如林”(《诗经·大雅·大明》),后来的说法是共有七十万人。而且新的部队还在源源不断开来。

据说,商人内部的反对者已经约定,在两军接战之前倒戈,向纣王发起攻击。但随着两军距离越来越近,他们迟迟没有动静。或许他们也被商人自己的庞大兵力吓坏了。

周人联军列成方阵,向殷商的矛戟丛林走去。他们因为紧张而越来越拥挤,盾牌互相碰撞挤压,每走几步都要停下来重整队列。前排敌人的面貌越来越清晰,紧张气氛陡然加剧,联军将士终于再也无法挪动脚步。

一方是统治中原六百年的主人,一方世世代代为主人提供人牲祭品,这将是一场实力对比悬殊的屠杀。弱势一方随时都会在恐惧中崩溃奔逃。

武王最后的阵前动员:

今日之事,不过六步七步,乃止齐焉,夫子勉哉!——《史记·周本纪》、《尚书·牧誓》

就在这短暂而沉寂的对峙之间,一小群联军战士挤出队列,向殷商军阵走去。带领这百十人走在最前面的,是年过七旬的的权术家、以老谋深算著称的太公吕尚。没人知道,他何以忽然抛弃了所有阴谋、诈术、诡计,像一介武夫般怒发冲冠直向敌阵。

吃人且崇尚祭祀的商朝是怎么完蛋的

武王伐纣

也许他只想改变羌人作为人牲悬挂风干的命运,他在朝歌已经看得太多。

在后世周人的史诗中,太公在那个清晨变成了一只鹰盘旋在牧野上空。他面前的敌军阵列瞬间解体,变成了互相砍杀混战的人群。武王的部队旋即启动,三百五十辆战车冲向商纣的中军王旗之处……

当淡淡阳光穿透晨雾,洒向原野间的纵横尸骸时,六百年商王朝已经终结。

维师尚父,时维鹰扬。涼彼武王,肆伐大商,会朝清明。——《诗经·大雅·大明》

纣师虽众,皆无战之心,心欲武王亟入。纣师皆倒兵以战,以开武王。武王驰之,纣兵皆崩,畔纣。纣走……——《史记·周本纪》

新商人

周人和他们的同盟军开进了朝歌城。

商纣王已经在绝望中自焚而死。除了纣王亲党,所有势力都在他的倒台中获得了满足。王宫的仓库都已空空如也,据说纣王将所有宝物堆在身边点燃殉葬,但从灰烬中只寻找出几块“天智玉”。太公建议武王不要追查宝物的去向:投诚的商人显贵多是些唯利是图之辈,应当犒劳一下他们。周军继续向各地进发,征讨顽抗的商军,倒戈的商朝贵族则充当向导。

平定商朝全境不是问题,周武王和周公、太公焦虑的,是让商朝上层接受被征服的事实。之前双方的秘密联络中,商人上层只是把这次战争看做一次联合铲除商纣的权宜之举,之后的商人仍旧将保有自己的王朝。局势至此,周人显然不会承认这点。

吃人且崇尚祭祀的商朝是怎么完蛋的

鸟尊 西周

山西博物院藏

吃人且崇尚祭祀的商朝是怎么完蛋的

鸟盖人足盉 西周

山西博物院藏

在熟悉商人典礼的太公主持下,武王在朝歌举行了向上帝献祭的仪式,如同商人以往的所有仪式一样,被砍下的头颅是敬献给上帝的礼物,只是这次的头颅换成了烧焦的商纣王、以及他的妃嫔和亲信们,而奉献祈福者换成了周武王,十三年前的人牲伯邑考的弟弟。

之后,武王向商朝臣工训话,宣布商王朝从此被周王朝取代,享用过祭礼的上帝也转而成为周族的保护神。

武王用了商人最熟悉的交易逻辑来论证:上帝此举并非心血来潮的冲动,以往虽然是历代商王献祭,但祭品中的谷物是由周人先祖——姜嫄之子后稷培育的,所以上帝心中早已对周族青睐有加,将商人的天下转托给周人:

在商先哲王,明祀上帝,亦维我后稷之元谷,用告和、用胥饮食,肆商先哲王,维厥故,斯用显我西土!——《逸周书·商誓》

商纣的儿子武庚被任命为新商王。几个月后,商地逐渐稳定,武王留下三位刚成年的少弟——管叔、蔡叔、霍叔等驻扎商都、监视武庚朝廷,自己带主力班师西归。

纣王的脑袋、还有他曾重用的所有臣子都被押解到了关中。武王在自己的都城镐京再次举行祭天典礼,宣告他正式平定了中土,成为上帝在人间的唯一代理人。

武王要抚慰父亲的屈辱、长兄的惨死。实际上,在向商人复仇的过程中,他已经变成了一个不折不扣的新商人。

这个典礼仪式也完全按照商人的惯例进行:纣王的一百名幸臣被押解到祭台下,用斧钺砍断手脚,任由他们在血水里翻滚挣扎。他们喊叫的声音越大,挣扎翻滚的越剧烈,就说明奉献给上帝的祭礼越丰盛。

还有在牧野战场上顽抗的武将、商人核心氏族的四十名族长,他们被剥光衣服,投入到沸水翻滚的大鼎中(《逸周书·世俘》)。

然后,武王身穿天子之服,在音乐声中走上祭坛,向上帝和祖先之灵汇报灭商过程。生的、熟的人牲躯体被抬上祭坛,正式奉献给上帝和周人列祖列宗。纣王和妻妾们的头颅、战争中斩获敌军的耳朵,都被堆放在巨大的柴堆之上焚烧,焦香的烟火气是上帝最喜欢的食物——这是商人的说法。

除了这些惊悚的祭品,山川天地诸神还要享用一些稍为正常的食物:宰杀了五百零四头牛奉献给上帝和周先祖;还有二千七百零一只猪、羊、狗,作为奉献给山川、土地诸小神的祭品。

吃人且崇尚祭祀的商朝是怎么完蛋的

猪尊 西周

山西博物院藏

吃人且崇尚祭祀的商朝是怎么完蛋的

刖人守囿车 西周

山西博物院藏

按照商人的仪轨举行完所有典礼,武王周发合理合法地成为了人间的新统治者。

但他仍旧不能摆脱失眠和恶梦的困扰。

他再次巡游新占领的疆域,试图找到上帝转而福佑自己的迹象,却始终未能如愿。当武王登上西山、俯瞰朝歌城,发现自己还生活在昔日恐惧的回忆中。他的健康状况每况愈下,在灭商当年的年底终于一病不起。

当武王再次经历过一个漫长的失眠之夜后,小子御陪着周公旦出现在卧榻前。武王说起了自己还没来得及完成的事业:

那些曾追随纣王作恶的商臣和部族,至今尚未全部铲除,随时可能发起反攻;自己的长子周诵还不到十岁,其余的尚在襁褓之中,根本无法治理新兴的王朝;除了周公之外,诸位弟弟都还年轻,只有周公能够接手治理这个新王朝。此事没有其他选择,所以连占卜都没必要了。

而且,在周公即位之后,朝歌城必须毁灭,那里是罪恶的大本营;父兄们在那里遭受的患难血泪要随之一起埋葬。武王已经为周公选好了新都城基址:在位居天下之中的河南平原上、一个小山环抱、三水汇流的盆地内。武王甚至给这座还在脑海中的新城起了名字:“度邑”,周人由尘世升入天堂的过渡之城。

以往宽慰从噩梦中惊醒的武王时,周公总是引经据典滔滔不绝,这次他却一句话也说不出,只能跪坐在榻前俯身哭泣,任泪水打湿衣裾(《逸周书·度邑》)。

二人商谈的具体过程已经湮灭。但当武王去世时,继位的仍是少年成王,周诵。周公以叔父身份辅政,宣布了营建度邑的决定,只是改名为洛邑——他意识到了天界与人世间不可逾越的界限。朝歌城中所有的居民,从贵族到工匠、贫民,都要迁徙到这座尘世新都(今洛阳市)。

吃人且崇尚祭祀的商朝是怎么完蛋的

周代徙都图

周公制度

叛乱立刻在东方爆发。管、蔡、霍三兄弟质疑周公表面推让王位,实际上却掌控着朝廷实权,这种虚伪的把戏只能欺骗一个孩子。

三人是文王朝歌之难后长大的一代新人,没有当年惊弓之鸟的凄惶经历,视周人的天下为理所当然。朝歌繁华富丽,生活比周人旧地舒适得多,商王的宫阙和种种排场,正应由他们享用,怎能轻易付之一炬?他们联合新商王武庚起兵,要保住这块商人的最后天堂。

周公和关中故地的周人已经预计到了商人的反抗,但没有想到自己的青年们被东方世界同化得如此迅速。军队再次向东方开去。腐化的军队不堪一击,管叔战败身死,蔡叔、霍叔被俘,武庚逃亡到了北方戎狄之中。

朝歌城被夷平为废墟。文王、伯邑考、武王和周公的所有梦魇都永远埋葬于斯。

周公开始颁布他的新政令。所有新政的出发点,就是往昔那些清晨他开导兄长的关于“德”的说法。这些说法对武王从未发挥药效,但周公如今有了全面推行它的机会。

杀人祭祀的风习被严令禁止,甚至宰杀牛羊也不能超过十二头。周公开始营建新洛阳,奠基时的祭礼只有两头牛;次日拜祭土地之神,用了牛、羊、猪各一头。

不仅如此,周公还要消灭有关朝歌的一切,自己和兄长遭受过的梦魇都要永远深埋。既然不能斩杀尽所有的殷商遗民,就只能修改他们的记忆,让他们自以为和别的民族没有任何区别。商王的甲骨档案库早已随着朝歌焚烧一尽;其他各种文献记载也被秘密审查、销毁。

吃人且崇尚祭祀的商朝是怎么完蛋的

龙纹玉环 西周

山西博物院藏

吃人且崇尚祭祀的商朝是怎么完蛋的

龙凤纹玉圭形饰 西周

山西博物院藏

周公还开始重新编纂历史。新的周公版历史说:商人和其他民族没有任何区别,他们的王朝也是禀受天命所建,历代商王和宰辅们都仁慈智慧、兢兢业业。只是末世的纣王丧心病狂,才导致了商王朝的终结。至于周族,也自然没有了为商朝充当帮凶的污点。

商人几百年的血腥暴行都归于纣王一人,他负荷着千百万人的罪恶,被涂抹成了完全丧失理性的疯子,以至孔子的学生子贡怀疑:关于商纣暴虐的很多说法都是后世人的虚构:

子贡曰:“纣之不善,不如是之甚也。是以君子恶居下流,天下之恶皆归焉。”——《论语·子张》

周公五百年后的孔子就是商人后代,他和子贡等弟子们传承的,却是被周公修改过的知识。人们或许能感到,纣王恶行的传说过于虚妄,但不知道这后面隐去的事实是何等恐怖。

这正是周公的目的,他不想后人也生活在恐惧和仇恨中,虽然他和兄长已终生无法摆脱。

还有,民族的隔阂必须打破。商人的族内婚被严厉禁止,所有贵族都不得在本族内结婚,而应当与其他部族、方国的上层联姻。为了巩固新的周王朝,周公还把周人、羌人分封到新占领的东方,让他们在各地建立新诸侯国。商人也都被拆散分配到这些新邦国中,他们将和各地的土著民族通婚混血,互相同化,形成新的世袭统治阶级。

混血、统一、开放的新华夏民族由此诞生。周人、商人、羌人的划分永远成为历史。

吃人且崇尚祭祀的商朝是怎么完蛋的

玉鼓 西周

山西博物院藏

周公继续完善着他的道德理想。他制定了种种礼节,希望让人们学会控制欲望,把社会规训得和善、节制、长幼有序。这些说教和规范形成了种种儒家经书,被统称为“周礼”。

当初激发父亲翦商灵感的八卦、六十四卦,也要重新进行阐释,消除那些野心和投机的成分。据说《周易》的《爻辞》是周公所写,它与文王名下的《彖辞》区别极大,不再鼓励任何投机和以下犯上的非分之想,全是一位君子应当如何朝乾夕惕、完成社会角色的励志说教。周公兄弟们从未能理解父亲对八卦的狂热。那个冒失之举虽然最终收获巨大,但毕竟给他们的家庭和国族带来了太多磨难和风险。如果再次面临这个选择,他们恐怕没有勇气投身于斯。

商人和神灵做交易的理论,也要做彻底修改。给神灵、祖先的献祭只是表达虔诚敬意,不需要、也不允许无限丰厚。神灵不再是贪得无厌的嗜血饿鬼,而是保佑有德者、惩戒无德者的最高仲裁,维系着周公倡导的人间道德体系。

在商人的功利、血腥、残暴已然登峰造极之后,周公创建了一套全新文化:节制欲望、善待他人、克己复礼、勤勉拘谨。这就是正在形成的新华夏族的样板品格。

吃人且崇尚祭祀的商朝是怎么完蛋的

《周礼》

周公还以身作则,每次面见年少的侄子成王时,他都战战兢兢如对严父,虽然他是成王事实上的监护人。每向成王表达完自己的意见,或者听成王说出每句话,周公都要以头触地、长跪稽首许久。

至于逐渐长大的成王,和所有青年们一样,开始萌生叛逆心理,对这些繁缛礼节和道德说教渐渐不满。而且周公一直掌握大权,在反对者看来,这无疑是虚伪和言行不一的表现。据说在数年间,成王曾命令周公居住在洛阳,不得到关中朝觐。最后,可能是周公奉还大政、交出所有权力之后,他才与侄子和解,回自己封邑度过晚年。

他委实无法向侄子解释自己这种对道德的近乎病态的依赖:这是他和父亲、兄长生命中的不能承受之痛,已无从向年轻一代谈起,就像伯邑考的死因不能触及一样。

周公在归政后不久死去,埋葬在文王和武王的陵墓之旁。最后岁月里,他和侄子成王关系如何,史书完全没有记载,但从他死时的寂寥来看,侄子显然还对这位道德楷模心存芥蒂。

吃人且崇尚祭祀的商朝是怎么完蛋的

玉人 西周

山西博物院藏

周公的道德事业是成功还是失败?恐怕言人人殊。但他彻底埋葬商都记忆的努力无疑是成功的,至少在考古学家的铲子掘开殷墟之前是如此。

尾声

经过十几年历史记载的空白之后,35岁的周成王忽然病重弥留,命悬一线。

但他仍按照天子之仪轨,挣扎着梳洗、穿戴起最庄重的冕服,端坐到朝堂之上,对臣工们发表了临终训话。他历数祖父文王、父亲武王以来的功业和教诲,告诫太子和臣工永保勤勉,不要丧失先辈们的翦商大业。

在臣僚们看来,这番景象恍然周公重生。

显然,在独自为政之后,成王渐渐理解了叔叔的某些用心:

王曰:“呜呼!疾大渐,惟几,病日臻。既弥留,恐不获誓言嗣,兹予审训命汝:昔君文王、武王宣重光,奠丽陈教,则肄肄不违,用克达殷、集大命。在后之侗,敬迓天威,嗣守文、武大训,无敢昏逾……”——《尚书·顾命》

临终训话结束之后,臣僚退去。成王挣扎着脱下了礼服,回到病榻上。次日,成王去世,太子康王继位。

华夏历史沿着武王和周公修改后的轨迹继续前行,直至今日。

吃人且崇尚祭祀的商朝是怎么完蛋的

四羊方尊 商

作者附言:两位大学同窗为本文提供了帮助,首先是芝加哥大学人类学博士、中国社科院历史所的林鹄师兄,他在本文酝酿阶段贡献了许多有见地的想法,本文第一节殷墟考古部分的文字,就直接来自他的著作;北大历史系的韩巍教授审读了全文,并提出了宝贵意见。在此一并致谢。

要重视意识形态领域的斗争。
一、“从历史上看,塑造哪个阶级的英雄形象,由哪个阶级的代表人物作为文艺舞台的主人,
是政治斗争在文艺上的集中反映,是文艺为哪个阶级的政治路线服务的主要标志。古往今来,每个阶级都用文艺塑造本阶级的英雄形象,
宣传本阶级的世界观,以达到按照本阶级的面貌改造世界的目的。哪个阶级的英雄形象占领文艺舞台,标志着由哪个阶级在文艺领域实行专政。
”——《京剧革命十年》红旗杂志1974年第七期;
二、“你是资产阶级文艺家,你就不歌颂无产阶级而歌颂资产阶级;你是无产阶级文艺家,你就不歌颂资产阶级而歌颂无产阶级和劳动人民:两者必居其一。
”——毛泽东《在延安文艺座谈会上的讲话》1942年五月

「毛主席在《讲话》中指出:“为什么人的问题,是一个根本的问题,原则的问题。
”这就是文艺工作的方向问题。文艺是为千百万工农兵服务,还是为一小撮剥削阶级服务?是为保卫无产阶级专政服务,
还是为一小撮反革命修正主义分子的复辟活动服务?这是毛主席的无产阶级革命文艺路线和反革命修正主义文艺黑线斗争的焦点。
文化界十七年来两条路线斗争的实践告诉我们:被反革命修正主义文艺黑线长期控制和影响的文化界,要解决的最根本的问题还是个方向的问题。
这个根本的方向问题解决了,其他的一切争论问题就可以迎刃而解了。」
——《毛主席〈在延安文艺座谈会上的讲话〉是无产阶级文化大军的建军纲领》红旗杂志1967年第8期如果这个问题不解决。

毛主席语录:
一、“世界上从有历史以来,没有不搞实力地位的事情。
任何阶级、任何国家,都是要搞实力地位的。搞实力地位,这是历史的必然趋势。国家是阶级统治的机关,军队是阶级的实力。
只要有阶级,就不能不搞军队。
”——《读苏联〈政治经济学教科书〉的谈话(节选)》 1959年;
二、“马克思主义的道理千条万绪,归根结底,就是一句话:造反有理。几千年来总是说:压迫有理,剥削有理,造反无理。
自从马克思主义出来,就把这个旧案翻过来了,这是一个大功劳。这个道理是无产阶级从斗争中得来的,而马克思做了结论。
根据这个道理,于是就反抗,就斗争,就干社会主义。
”——《在延安各界庆祝斯大林六十寿辰大会上的讲话》1939年

(1) 点乘和叉乘

【点乘】:
定义:a·b=|a|·|b|cos<a,b> 【注:粗体小写字母表示向量,<a,b>表示向量a,b的夹角,取值范围为[0,180]】
几何意义:是一条边向另一条边的投影乘以另一条边的长度.
v1和v2向量的点乘运算:相应元素的乘积的和:v1( x1, y1,z1) * v2(x2, y2,z2) = (x1x2) + (y1y2) + (z1*z2);

给定一个向量u和v,求u在v上的投影向量,如下图。

假设u在v上的投影向量是u’,且向量u和v的夹角为theta。一个向量有两个属性,大小和方向,我们先确定u’的大小(即长度,或者模),从u的末端做v的垂线,
那么d就是u’的长度。而u’和v的方向是相同的,v的方向v/|v|也就是u’的方向。

再求d的长度。

注意 : 结果不是一个向量,而是一个标量。
性质1: ab = |a||b|Cos(θ) ,θ是向量a 和向量 b之间的夹角。
性质2: a
b = b*a 满足乘法交换律

关于这里的计算需要插播一个点,当两个单位向量的长度都是1的时候,向量的点乘就是他们夹角的余弦值。在游戏开发中会有归一化的操作,然后直接求夹角的情况,如果对这个知识点没有了解,可能看不懂夹角计算的原理。
我们通过点积公式可以看出来a,b都是标量,都是正数,余弦值会根据角度有正负变化。
当(0-90)°的时候,余弦值是正数,整个点乘公式都是正的。
当90°的时候,余弦值为0,整个公式为0。
当(90-180)°的时候,余弦值是负数,整个公式为负的。
利用这个性质,我们可以根据点乘的正负,做一些判断了。

Unity项目应用:
1.根据点乘计算两个向量的夹角。<a,b>= arccos(a·b / (|a|·|b|))
2.根据点乘的正负值,得到夹角大小范围,>0,则夹角(0,90)<0,则夹角(90,180),可以利用这点判断一个多边形是面向摄像机还是背向摄像机。
3.根据点乘的大小,得到向量的投影长度,反应了向量的长度关系。

【叉乘】:
定义:c = a x b,其中a b c均为向量
几何意义是:得到一个与这两个向量都垂直的向量,这个向量的模是以两个向量为边的平行四边形的面积
推导

v1和v2向量的叉乘运算:相应元素的乘积的和:v1( x1, y1,z1) x v2(x2, y2, z2) = {(y1z2 - y2z1),(x2z1 - x1z2),(x1y2-x2y1)};

性质1:c⊥a,c⊥b,即向量c与向量a,b所在平面垂直
性质2:模长|c| = |a||b| sin<a,b>
性质3:(数学上)满足右手法则, a x b = -b x a,所以我们可以使用叉乘的正负值来判断a,b的相对位置,即b是处于a的顺时针还是逆时针方向。
叉乘的右手定则是用来确定叉乘积的方向的。

Unity项目应用:
1.根据叉乘得到a,b向量的相对位置,和顺时针或逆时针方位。
2.得到a,b夹角的正弦值,计算向量的夹角(0,90),可以配合点乘和Angle方法计算出含正负的方向。
3.根据叉乘大小,得到a,b向量所形成的平行四边形的面积大小,根据面积大小得到向量的相对大小。

简单的说: 点乘判断角度,叉乘判断方向。
形象的说: 当一个敌人在你身后的时候,叉乘可以判断你是往左转还是往右转更好的转向敌人,点乘得到你当前的面朝向的方向和你到敌人的方向的所成的角度大小。

简单来说,在两个物体的位置关系判断中。
点乘可以判断出目标物体在我的前方还是后方。大于零在前方,小于零在后方。
叉乘可以判断出目标物体在我的左边还是右边。大于零在右方,小于零在左方。

在计算机图形学中。
点乘可以用来计算夹角余弦值。
叉乘可以用来计算平面法向量。

=========================================================分割线=========================================================
=========================================================分割线=========================================================
(2)Lua 原表
在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作(比如相加)。
因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。
当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。 __add 等即时字段,其对应的值(往往是一个函数或是 table)就是”元方法”。
有两个很重要的函数来处理元表:
setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table): 返回对象的元表(metatable)。

Lua table有一套hashmap的查找机制,如果访问一个表中并不存在的字段,不会立即返回nil,而是先触发一套查找机制,也是我们用以实现面向对象的方法。
简单的描述:元表就是用于查找的备用表。

【构成类】:

元方法 __index
简单的描述:是当table中一个元素不存在的时候,会触发寻找元表的__index元方法,如果不存在,则返回nil,如果存在,则返回结果。

【只读不写表】:
1,利用元方法__newindex的特性(对字段进行赋值的时候如果表中没有则执行__newindex中的方法),新建一个空表代替原来的表,并对空表进行元方法重写实现table的相应功能。
这样对任何字段的修改,在空表中肯定都找不到,只能只能统一的入口方法__newindex,我们不处理就能实现只读。
2,再利用__index元方法保存原来的表, 因为取值的时候如果找不到就会去__index中取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function const(t)
local temp = t --temp以upvalue的形式存储t
local ret = {}
local mt = {
--__index = t, 或者这样
    __index = function(t,k)
      return temp[k]
    end,
    __newindex = function()--空函数覆盖newindex

    end,

    __metatable = false,
  }
  setmetatable(ret,mt)
  return ret
end

【table的 弱引用和强引用】:

一.强引用table
lua中的table是引用类型,更准确地说,是强引用类型。如下第二段代码,在内存中有一个{name = “123”}的table,并用a和b[1]指向它,然后置空a,此时就只剩下b[1]指向它了。
这种引用方式和我们所认知的引用是一样的。值得一提的是,这里的a = nil为什么不等同于{name = “123”} = nil呢,意思是将指向的这个表删掉呢?
因为lua是具备自动内存管理的,我们只管创建,删除操作是lua自动进行的,因此这里的a = nil并不是删除表,而是指将a对这张表的引用去掉,当没有地方引用这张表时,这张表就会被lua自动清掉。

1
2
3
4
5
6
7
8
9
10
11
a = {name = "123"}
b = a
a.name = "456"
print(b.name)--456


a = {name = "123"}
b = {}
b[1] = a
a = nil
print(b[1].name)--123

二.弱引用table
如上所说,一般我们创建的table都是强引用table,即key和value都是强引用,强引用可以防止他们指向的对象被回收。如果一个对象存在强引用,那么它就不会被回收。而相对地,就有弱引用,弱引用不能防止对象被回收。
如果一个对象只存在弱引用,那么它就会 被回收。lua中通过弱引用table来实现弱引用。弱引用table有三种形式:
1.key值弱引用。设置方法为setmetatable(b, {__mode = “k”})
2.value值弱引用。设置方法为setmetatable(b, {__mode = “v”})
3.key和value值弱引用。设置方法为setmetatable(b, {__mode = “kv”})
当一个key或者value被收集时,整个key以及对应的value都会从table中移除。

下面新增一句代码,表示b的value对其指向的对象的引用是弱引用,而b的key对其指向的对象的引用仍然是强引用(对于table来说,其key和value可以指向任何类型的对象,除了key不能指向nil)。
{name = “123”}这个table只存在弱应用b[1],所以被回收。

1
2
3
4
5
6
7
8
9
10
a = {name = "123"}
b = {}
setmetatable(b, {__mode = "v"}) --add
b[1] = a
a = nil
collectgarbage()
for k,v in pairs(b) do
print(k) --无
print(v) --无
end

=========================================================分割线=========================================================
=========================================================分割线=========================================================
(3)Unity协程原理

使用过Unity的同学一定知道,Unity提供了一套协程机制,简直不要太好用。但是这个协程依赖于Unity引擎,离开Unity就无法使用。
那有没有办法实现不依赖Unity的协程呢?答案是当然阔以。 所谓实现一个协程,就是实现一个迭代器的容器!

什么是IEnumerator

在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,”(那么就能够被迭代)”;
调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。
这个只能向前移动的特性就是我们协程需要用到的特性。所谓实现一个协程,就是实现一个迭代器的容器!
具体函数:

1
2
3
4
5
6
7
8
9
10
11
12
using System.Runtime.InteropServices;
namespace System.Collections
{
[ComVisible(true)]
public interface IEnumerator
{
object Current { get; } // 当前正在访问的对象,只读

bool MoveNext(); // 移动到下一个元素,我们的协程通过此函数来控制迭代器的到底要不要移动下一个代码段.如果迭代器返回false,则证明迭代器已经执行完毕,此时协程也应该执行完毕
void Reset(); // 重置函数
}
}

总结:
就是线程通过调度器检测条件 使用 IEnumerator 中的MoveNext方法移动 和 yield让出权限

=========================================================分割线=========================================================
=========================================================分割线=========================================================
(4)C# GC原理

Ngui 裁剪原理
怎么把模型放到UI前
摄像机层级管理
怎么拆分模型实现边走边打
渲染管线流程
帧同步状态同步
弱联网
资源管理框架
数据结构
数组和列表

1 最近学习了下 AB资源加密下面出来记录下
随着Unity的普及,现在已经越来越多的项目使用AB包,但是大部分都是没有加密的。比如某IP侵权的app,可以通过或者Unity内置的AssetBundleBrower直接看到ab包的所有资源
更甚者可以通过AssetStudio直接看到ab包的所有资源,图片,音频,动画,文本,CG视频等等(蒙皮动画目前不行),并且导出。
如果会写点代码,还可以在unity中,直接实例化出来,然后另存为Prefab,这样虽然无法获得fbx(其实fbx开源,自己反推写入数据也可以导出fbx),但是我们可以获得完整的Prefab,
设置内含了蒙皮信息。这样可以轻松获得所有的数据,贴图,音频,视频,模型,材质,蒙皮,动画等等

2 看了下AB资源的加密方式大概有几种
2.1 unity中国区提供的一种加密方式
2.2 对AB包的二进制文件进行加密操作
2.3 使用offest加密
还要其他的吧 暂时没了解

3 我看来看下对AB包的二进制文件进行加密操作

3.1 加密就是用秘钥跟ab文件异或处理,解密的时候也是再做一次异或处理即可,只要不被别人拿到秘钥就问题不大。
1
2
3
4
5
6
7
8
9
10
public static void Encypt(ref byte[] targetData, byte m_key)
{
//加密,与key异或,解密的时候同样如此
int dataLength = targetData.Length;
for (int i = 0; i < dataLength; ++i)
{
targetData[i] = (byte)(targetData[i] ^ m_key);
}
}

3.2 用算法加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
    [MenuItem("AssetBundle/AssetBundle_Window")]
public static void BuildAssetToWindow()
{
string outPath = Application.dataPath + "/../BuildAssetBundle/Window/";
CreateDir(outPath);
BuildPipeline.BuildAssetBundles(outPath,BuildAssetBundleOptions.ForceRebuildAssetBundle, BuildTarget.StandaloneWindows);
AssetDatabase.Refresh();
}
public static void CreateDir(string path)
{
DirectoryInfo info = new DirectoryInfo(path);
if (!info.Exists)
{
info.Create();
}
}

private IEnumerator EncryptionAB()
{
WWW www = new WWW("file:///D:\\fcj\\unity209\\VuforiaStudy\\BuildAssetBundle\\Window\\testab");
yield return www;
if (www.isDone)
{
if (www.error == null)
{
byte[] bytes = www.bytes;
for (int i = 0; i < bytes.Length; i++)//恺撒加密
{
bytes[i] += 1;
}
File.WriteAllBytes("D:\\fcj\\unity2018\\VuforiaStudy\\BuildAssetBundle\\Window\\myab_Encryption.assetbundle", bytes);
}
}
}

private IEnumerator DecryptAB()
{
WWW www = new WWW("file:///D:\\fcj\\unity2018\\VuforiaStudy\\BuildAssetBundle\\Window\\myab_Encryption.assetbundle");
yield return www;
if (www.isDone)
{
if (www.error == null)
{
byte[] bytes = www.bytes;
for (int i = 0; i < bytes.Length; i++)//恺撒解密
{
bytes[i] -= 1;
}
AssetBundle ab = AssetBundle.LoadFromMemory(bytes);
GameObject go = ab.LoadAsset("Sphere") as GameObject;//注意这里是预制体名字
Instantiate(go, Vector3.zero, Quaternion.identity);
ab.Unload(false);
}
}
}

```
3.3 自定义加密
比如他自己随机改数据里面的值 或者加上数据标头等等

```c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

/// <summary>
/// ab资源字节加密
/// </summary>
public class AssetBundleEncrypt {

private readonly static byte[] AssetHead = new byte[]
{
0x55, 0x6E, 0x69, 0x74,
0x78, 0x00, 0x32, 0x30,
};

private readonly static byte[] RiverHead = new byte[]
{
0x74, 0x69, 0x61,
};

private static Dictionary<byte, bool> helpDic = new Dictionary<byte, bool>();

// 加密
public static byte[] EncryptAsset(byte[] bytes, bool needNewByts = false)
{
var result = bytes;
if (needNewByts)
{
result = new MemoryStream(bytes).ToArray();
}
if (result.Length < 2048)
{ // 小于2048直接不加密
return result;
}
helpDic.Clear();
int mid = (AssetHead.Length - RiverHead.Length) / 2;
byte[] encryptBytes = new byte[mid * 2];

for (int i = 0, length = AssetHead.Length - RiverHead.Length; i < length; i+=2)
{
byte pos = 0;
byte value = 0;
pos = (byte)(Random.value * 255);
while (helpDic.ContainsKey(pos))
{
pos = (byte)(Random.value * 255);
}
helpDic.Add(pos, true);
if (i < mid)
{
value = result[pos + AssetHead.Length];
result[pos + AssetHead.Length] = (byte)(Random.value * 255);
}
else
{
value = result[result.Length - pos - 1];
result[result.Length - pos - 1] = (byte)(Random.value * 255);
}
encryptBytes[i] = pos;
encryptBytes[i + 1] = value;
}

for (int i = 0, length = RiverHead.Length; i < length; i++)
{
result[i] = RiverHead[i];
}
for (int i = RiverHead.Length, length = RiverHead.Length + encryptBytes.Length; i < length; i++)
{
result[i] = encryptBytes[i - RiverHead.Length];
}
return result;
}


// 解密
public static byte[] UnEncryptAsset(byte[] bytes, bool needNewByts = false)
{
var result = bytes;
if (needNewByts)
{
result = new MemoryStream(bytes).ToArray();
}
if(result.Length < 2048)
{ // 小于2048直接不加密
return result;
}

int mid = (AssetHead.Length - RiverHead.Length) / 2;
for (int i = 0, length = AssetHead.Length - RiverHead.Length; i < length; i+= 2)
{
byte pos = result[RiverHead.Length + i];
byte value = result[RiverHead.Length + i + 1];
if (i < mid)
{
result[pos + AssetHead.Length] = value;
}
else
{
result[result.Length - pos - 1] = value;
}
}
for (int i = 0, length = AssetHead.Length; i < length; i++)
{
result[i] = AssetHead[i];
}
return result;
}
}


//加密调用
string publishTempPath = GetFullUrlPublish(Path.GetFileName(resPath));
var bytes = File.ReadAllBytes(resPath);
if (type.ToLower() == ".unity3d")
bytes = AssetBundleEncrypt.EncryptAsset(bytes);
File.WriteAllBytes(publishTempPath, bytes);

//解密调用
var bytes = www.downloadHandler.data;
if (GameVersion.isHotUpdate || GameVersion.isSplitAsset)
bytes = AssetBundleEncrypt.UnEncryptAsset(bytes);
AssetBundle bundle = AssetBundle.LoadFromMemory(bytes);

最近策划需要一个雷达战力图

RadarChart.cs 有兴趣的自己看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RadarChart : MonoBehaviour
{
private MeshRenderer mMeshRenderer = null;
private MeshFilter mMeshFilter = null;
private Material mMat = null;
private Mesh mMesh;

public Color mainColor = new Color(0.4f, 0.6f, 1f);
public Color outLineColor = new Color(1f, 1f, 1f, 0.3f);
public Color addColor = Color.white;
public float alpha = 0.3f;


// 参数
private int VERTICES_COUNT; //网格模型顶点数量
public Vector3[] vertices;
private Vector3[] vec3OutLineList;
//三角形数组
int[] triangles;
public float scale;
public float radius = 45f;
public float outLineSize = 1f;
public GameObject obj;

public MeshRenderer meshRenderer
{
get
{
if (mMeshRenderer == null)
{
mMeshRenderer = GetComponent<MeshRenderer>();
if (mMeshRenderer == null)
{
mMeshRenderer = gameObject.AddComponent<MeshRenderer>();
}
}
return mMeshRenderer;
}
}

public MeshFilter meshFilter
{
get
{
if (mMeshFilter == null)
{
mMeshFilter = GetComponent<MeshFilter>();
if (mMeshFilter == null)
{
mMeshFilter = gameObject.AddComponent<MeshFilter>();
}
}
return mMeshFilter;
}
}

public Material material
{
get
{
if (mMat == null)
{
mMat = new Material(ShaderManager.instance.LoadShader("UI/ColorAdd", "ColorAdd"));
mMat.SetColor("_MainColor", mainColor);
mMat.SetColor("_AddColor", addColor);
mMat.SetFloat("_Alpha", alpha);
}
return mMat;
}
}

public Mesh mesh
{
get
{
if (mMesh == null)
{
mMesh = new Mesh();
mMesh.name = "UIRadarChart";
}
return mMesh;
}
}
void Start()
{
meshRenderer.material = material;
FillMeshRadarChart();
}

void Update()
{
//UpdateRadarChart();
}

float GetRadians(float angle)
{
return Mathf.PI / 180 * angle;
}

void FillMeshRadarChart()
{
if (!Application.isPlaying)
{
return;
}

VERTICES_COUNT = vertices.Length;
int triangles_count = VERTICES_COUNT - 1;

triangles = new int[triangles_count * 3];

//设定原点坐标
vertices[0] = new Vector3(0, 0, 1);
//首个在x轴上的坐标点
vertices[1] = new Vector3(radius, 0, 1);

//每个三角形角度
float everyAngle = 360 / triangles_count;
for (int i = 2; i < vertices.Length; i++)
{
var angle = GetRadians(everyAngle * (i - 1));
vertices[i] = new Vector3(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle), 1);
}

int idx = 0;
int value = 0;
for (int i = 0; i < triangles.Length; i++)
{
if (i % 3 == 0)
{
triangles[i] = 0;
value = idx;
idx++;
}
else
{
value++;
if (value == VERTICES_COUNT)
value = 1;

triangles[i] = value;
}
}
UpdateRadarChart();
}

void DrawOutLine()
{
//obj.transform.localPosition = new Vector3(0, 0, -1);
MeshFilter mf = obj.GetComponent<MeshFilter>();
if (mf == null)
{
mf = obj.AddComponent<MeshFilter>();
}

MeshRenderer mr = obj.GetComponent<MeshRenderer>();
if (mr == null)
{
mr = obj.AddComponent<MeshRenderer>();
}

Material mat = new Material(ShaderManager.instance.LoadShader("UI/ColorAdd", "ColorAdd"));
mat.SetColor("_MainColor", outLineColor);
mr.material = mat;
Mesh meshOut = new Mesh();
meshOut.vertices = vec3OutLineList;
meshOut.triangles = triangles;
mf.mesh = meshOut;
}

Vector2 GetPreVec(int index)
{
int tmpIndex = (index - 1) < 1 ? vertices.Length - 1 : (index - 1);
return new Vector2(vertices[tmpIndex].x, vertices[tmpIndex].y);
}

Vector2 GetCurVec(int index)
{
return new Vector2(vertices[index].x, vertices[index].y);
}

Vector2 GetNextVec(int index)
{
int tmpIndex = (index + 1) >= vertices.Length ? 1 : index + 1;
return new Vector2(vertices[tmpIndex].x, vertices[tmpIndex].y);
}

Vector2 GetNormal(Vector2 dir)
{
return new Vector2(dir.y, -dir.x);
}

Vector3 GetIntersect(Vector2 s1, Vector2 d1, Vector2 s2, Vector2 d2)
{
Vector2 e1 = s1 + d1;
Vector2 e2 = s2 + d2;

float a1, a2, b1, b2, c1, c2;

a1 = e1.y - s1.y;
b1 = s1.x - e1.x;
c1 = (e1.x * s1.y) - (s1.x * e1.y);

a2 = e2.y - s2.y;
b2 = s2.x - e2.x;
c2 = (e2.x * s2.y) - (s2.x * e2.y);

float c = (a1 * b2 - a2 * b1);

float x = (b1 * c2 - b2 * c1) / c;
float y = (a2 * c1 - a1 * c2) / c;

return new Vector3(x, y, 0);
}

void UpdateRadarChart()
{
Vector3[] tmps = new Vector3[vertices.Length];
Vector3[] tmpo = new Vector3[vertices.Length];

float lineWidth = outLineSize;
Vector2 cur, next, pre, da, db, na, nb;
for (int i = 0; i < vertices.Length; i++)
{
tmps[i] = vertices[i] * vertices[i].z * scale;

// 描边顶点
cur = GetCurVec(i);
next = GetNextVec(i);
pre = GetPreVec(i);
da = (cur - pre).normalized;
db = (cur - next).normalized;
na = GetNormal(da);
nb = GetNormal(-db);
tmpo[i] = this.GetIntersect(pre + (na * lineWidth), da, next + (nb * lineWidth), db);
}
// 描边顶点
vec3OutLineList = tmpo;
DrawOutLine();
// 正常顶点
mesh.vertices = tmps;
mesh.triangles = triangles;
meshFilter.mesh = mesh;


}
}

或者可以弄个纹理贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteInEditMode]
public class WuJiaoXingRender : MonoBehaviour
{
public static Vector3 posC = new Vector3(0, 0, 0); //中心
public static Vector3 pos1 = new Vector3(0, 1, 0); //最上
public static Vector3 pos2 = new Vector3(1f, 0.236f, 0); //右上
public static Vector3 pos3 = new Vector3(0.627f, -1f, 0); //右下
public static Vector3 pos4 = new Vector3(-0.627f, -1f, 0); //左下
public static Vector3 pos5 = new Vector3(-1f, 0.236f, 0); //左上

public static Vector2 uvC = new Vector2(0.5f, 0.5f); //中心
public static Vector2 uv1 = new Vector2(0.5f, 1f); //最上
public static Vector2 uv2 = new Vector2(1f, 1 - 0.38f); //右上
public static Vector2 uv3 = new Vector2(0.8f, 1 - 1); //右下
public static Vector2 uv4 = new Vector2(0.2f, 1 - 1); //左下
public static Vector2 uv5 = new Vector2(0f, 1 - 0.38f); //左上

private bool isInit = false;

public Texture2D texture; //五角星贴图
[Range(0.1f,1)]
public float rate1 = 1f;
[Range(0.1f, 1)]
public float rate2 = 1f;
[Range(0.1f, 1)]
public float rate3 = 1f;
[Range(0.1f, 1)]
public float rate4 = 1f;
[Range(0.1f, 1)]
public float rate5 = 1f;

private MeshFilter meshFilter;
private MeshRenderer meshRender;
private Mesh mesh;

private Vector2[] uvs = new Vector2[15];
private Vector3[] verts = new Vector3[15];
private int[] indexes = new int[15];
private Color[] colors = new Color[15];

private Vector2[] initUvs = new Vector2[15];
private Vector3[] initVerts = new Vector3[15];

// Use this for initialization
void Start()
{
if (isInit)
return;
if (meshFilter == null)
meshFilter = this.gameObject.AddComponent<MeshFilter>();
if (mesh == null)
{
CreateMesh();
meshFilter.sharedMesh = mesh;
}
meshRender = this.gameObject.AddComponent<MeshRenderer>();
meshRender.sharedMaterial = new Material(Shader.Find("Unlit/WuJiaoXingRender"));
meshRender.sharedMaterial.SetTexture("_MainTex", texture);
isInit = true;
}



#if UNITY_EDITOR
[ContextMenu("CreateEditor")]
private void CreateEditor()
{
Start();
}
#endif

private void CreateMesh()
{
verts[0] = pos1;
verts[1] = pos2;
verts[2] = posC;

verts[3] = pos2;
verts[4] = pos3;
verts[5] = posC;

verts[6] = pos3;
verts[7] = pos4;
verts[8] = posC;

verts[9] = pos4;
verts[10] = pos5;
verts[11] = posC;

verts[12] = pos5;
verts[13] = pos1;
verts[14] = posC;

uvs[0] = uv1;
uvs[1] = uv2;
uvs[2] = uvC;

uvs[3] = uv2;
uvs[4] = uv3;
uvs[5] = uvC;

uvs[6] = uv3;
uvs[7] = uv4;
uvs[8] = uvC;

uvs[9] = uv4;
uvs[10] = uv5;
uvs[11] = uvC;

uvs[12] = uv5;
uvs[13] = uv1;
uvs[14] = uvC;

for (int i = 0; i < 15; i++)
{
initUvs[i] = uvs[i];
initVerts[i] = verts[i];
colors[i] = Color.red;
indexes[i] = i;
}

mesh = new Mesh();
mesh.name = "WuJiaoXingRender";
mesh.vertices = verts;
mesh.colors = colors;
mesh.uv = uvs;
mesh.SetTriangles(indexes, 0);
}

// Update is called once per frame
void Update()
{
if (!isInit)
return;

updateRate(0, rate1);
updateRate(1, rate2);
updateRate(2, rate3);
updateRate(3, rate4);
updateRate(4, rate5);
}

private void updateRate(int index, float rate)
{
int outIndex1 = index * 3;
int outIndex2 = index * 3 - 2;
if (outIndex2 < 0)
{
outIndex2 += 15;
}
Vector3 pos = initVerts[outIndex1];
pos *= rate;
verts[outIndex1] = pos;
pos = initVerts[outIndex2];
pos *= rate;
verts[outIndex2] = pos;
mesh.vertices = verts;
}
}


可以参考下 我写了个粗糙版

新增一个 UI_CG.cginc文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#ifndef UI_CG
#define UI_CG
inline half4 GreyUI(sampler2D _MainTex,sampler2D _AlphaTex,half2 texUV,half4 vColor)
{
half4 alphaColor = tex2D(_AlphaTex,texUV);
half4 mainColor = tex2D(_MainTex, texUV);
mainColor.a *= alphaColor.r;

float t1 = step(0.001,vColor.r - 0.49);
float t2 = step(0.001,0.51 - vColor.r);
half tmp = t1 * t2;
//half tmp = step(0.001,vColor.r - 0.49) * step(0.001,0.51 - vColor.r);
half grey = dot(mainColor.rgb, half3(0.299, 0.587, 0.114));
mainColor = tmp * half4(grey,grey,grey,mainColor.a) + (1 - tmp) * mainColor * vColor;
return mainColor;
}
#endif

修改图片的shader
Transparent Colored
修改textrue的shader
Unlit/Transparent Colored

1
2
3
4
5
half4 frag (v2f IN) : COLOR
{
return GreyUI(_MainTex,_AlphaTex,IN.texcoord,IN.color);
// return tex2D(_MainTex, IN.texcoord) * IN.color;
}

1 什么是PRB,有什么用

基于物理的渲染过程,是一种着色和渲染技术,用于更精确的描述光如何与物体表面互动,PBR的优势在于其通过精确的物理计算公式,可以准确的得到各种光照环境下的效果。

2 PBR分类

基于金属的工作流Metal / Roughness
基于镜面反射的工作流Specular / Glossiness

3 PBR中 BRDF公式大概

输出颜色 = 散射光占比 * (表面颜色/PI)+ 镜面反射比例 *( DFG / 4 * max(dot(法线,视线),0) * saturate(dot(法线,光线)) ) * (光线 dot 法线)

4 PBR散射光占比

散射光占比kD:为了保持能量守恒,散射光占比 和 镜面光占比之和不能超过1.0    kD = float3(1.f, 1.f, 1.f) - kS    金属度越高,散射越少 kD = kD * (1.f - metallic);

5 镜面反射比例

镜面光占比kS 镜面光占比为菲涅尔结果  kS = F     DFG 已经包含

6 DFG

6.1
    D 法线分布函数(Normal Distribution Function):估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。这是用来估算微平面的主要函数。
    F 菲涅尔方程(Fresnel Rquation):菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。菲涅尔效果就是当视线与法线夹角越大折射效果减弱而反射效果增强,
        拿水来说我们近处的水透明见底可理解为光线发生了全折射而零反射,远处的水波光粼粼可理解为光线发生了零折射而全反射。
    G 几何函数(Geometry Function):描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。

6.2
    法线分布函数(Normal Distribution Function ),描述了描述了微观表面上的表面法线m的统计分布,即具有正确朝向的微表面法线浓度

    基本性质:
    >>>> 1.法线分布函数作为微平面密度的度量,应始终为正值
    >>>> 2.微表面总面积不小于对应宏观表面总面积
    >>>> 3.任何方向上微观表面投影面积与宏观表面投影面积相同
    >>>> 4.观察方向若为法线方向( v =n ) ,则其积分可以归一化

    各种模型:
    >>>> Berry [1923]
    >>>> Beckmann [1963]
    >>>> Phong [1973]
    >>>> Blinn-Phong [1977]
    >>>> ABC [1989]
    >>>> GGX [2007] / Trowbridge-Reitz [1975]
    >>>> Shifted Gamma Distribution,SGD [2012]
    >>>> Trowbridge-Reitz(GTR)[2012]
    >>>> Student’s T-Distribution , STD [2017]
    >>>> Exponential Power Distribution , EPD [2017]等

    GGX模型:
    >>>> denom = (dot(N,H))的平方 * ((a的平方 - 1.0) + 1.0)
    >>>> GGX = a的平方/ (PI * denom的平方)
    >>>> 事件代码
1
2
3
4
5
6
7
8
9
10
11
12
// GGX正态分布函数
float DistributionGGX(float3 N,float3 H,float roughness)
{
float a2=roughness*roughness;
a2=a2*a2;
float NdotH=saturate(dot(N,H));
float NdotH2=NdotH*NdotH;

float denom=(NdotH2*(a2-1.0)+1.0);
denom = UNITY_PI*denom*denom;
return a2/denom;
}
6.3
    菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。菲涅尔效果就是当视线与法线夹角越大折射效果减弱而反射效果增强
    cosTheta = clamp(dot(halfDir,viewDir),0,1)
    F0 = 不同采材质的反射率
1
2
3
4
5
// 菲涅尔简化方程代码
float3 FresnelSchlick(float cosTheta,float3 F0)
{
return F0 + (1.0-F0) * pow(1.0-cosTheta,5.0);
}
6.4
    几何遮蔽函数:它主要是用来模拟微平面的一个点有没有被遮住
    为了有效的估算几何部分,需要将观察方向(几何遮蔽(Geometry Obstruction))和光线方向向量(几何阴影(Geometry Shadowing))都考虑进去。我们可以使用史密斯法(Smith’s method)来把两者都纳入其中
    Smith 几何遮蔽函数G:
    >>>> worldNormal,viewDir,worldLightDir,_Roughness
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// Smith 几何遮蔽函数
float3 GeometrySchlickGGX(float NdotV,float roughness)
{
float r = roughness + 1.0; //直接光照K值
float k = r*r/8.0;

float denom = NdotV * (1.0-k) + k;
return NdotV/denom;
}

float GeometrySmith(float3 N,float3 V,float3 L,float roughness)
{
float NdotV = saturate(dot(N,V));
float NdotL = saturate(dot(N,L));
float ggx1 = GeometrySchlickGGX(NdotV,roughness);
float ggx2 = GeometrySchlickGGX(NdotL,roughness);

return ggx1 * ggx2;
}

7 总结
结束: ( kD * albedo / UNITY_PI + specular) * NdotL
自己老是容易忘记 年级大了?