cs程序 加license_手游体积云:完全程序化生成,可穿越

abf0e0f4ffee0f468e05055ae5016a79.png

尝试实现了一个适用于手游的体积云,可以像飞机一样穿越云层,而且不用手工放置:

(视频有音效,建议选择超清)

手游体积云,可穿越,程序化生成

单个云的形态:(视频22秒,无声,建议选择超清)

手游体积云,可穿越,程序化生成

体积云可以简单地用几个相互穿插的透明Quad(方片)来实现,但是overdraw的问题也会随之而来。在我们优化性能、解决overdraw时,一定要考虑到移动平台GPU的特点:带宽低、对数据精度严格。

基本的实现比较简单直观,我会在第1节快速讲完。今天的主角是性能优化,会在第2节详细介绍。然后我想赋予云更多的动态,还有程序化生成,这些都会在第3节讲到。关于“穿越”的一些细节在第4节。

我经验尚浅,有些结论太过绝对与片面,本文仅是我个人粗浅的见解,以抛砖引玉,如有疏漏希望大家多多加以指正,十分感谢。

1.基本实现

传统的实现云的方式是Ray-Marching。但是与PC或主机不同的是,Ray-Marching对手机来说计算量太大了。所以我们只能另寻出路。

“The Witness”这个游戏里的云,就是简单地把几个Quad穿插在一起,我用抓帧工具把模型抓了出来:

60617fc5a57d7b5aef72262017b81ffe.png

在每个Quad上画上相同的透明纹理:

b03ed6f15a16f2e8e9c4bd5d6ea911ff.png

然后这一堆Quad就变成了这样:

3e39a825a9f0190a8aa155ea611811e4.png

我们可以称它为“云单元”。如果好几个云单元紧紧放在一起,就得到了:

056c51b7543c9fddfe792cecd68e8487.png

到这一步,云仍然看起来很粗糙,后面“The Witness”也还有更多的步骤去改进它。但是与“The Witness”不同的是,我是在移动平台上做实现,这时候帧率变得非常低。在profile之后,我发现我不得不先解决overdraw的问题,在那之后才能去做更多的效果。

2.性能优化历险记

如果场景中没有透明物体,fragment着色阶段的工作负荷是被屏幕分辨率限制的,fragment阶段的工作量是和屏幕分辨率成比例的。透明物体带来了overdraw,fragment着色的负荷一下子变得不可控起来。Overdraw意味着好几个fragment被draw到同一个像素上去了,于是fragment阶段就容易形成渲染瓶颈。

要战胜overdraw,有两个办法:一个是少draw一些fragment,另一个是减轻每个fragment着色的工作负荷。

①少draw一些fragment

现在用的这个mesh固然是由方片组成的,但是实际上画上去的内容(纹理)却是个圆形,那么方片的四个角明显是被浪费了。这些角落是完全透明的,但是仍然要消耗渲染性能。我们可以把角落的部分削掉,留下一个圆形的片。

ffad3d99e464f7860ee96a7eb9f2f1d2.png

但是有人可能就迷惑了,虽然这样少draw了一些fragment,但是要draw更多的顶点啊!?这样做真的值吗?当然值了。因为根据渲染流水线的原理,现在瓶颈位于fragment着色的阶段。就算是让顶点着色的工作负荷加重一点,整体的渲染速度仍然是由fragment着色来决定的。另外,我用的不是一个完美的圆形,我用的是十二边形,没有增加太多顶点。

②减轻每个fragment着色的工作负荷

在移动设备上,GPU和显存之间的带宽是很受限的,所以GPU从显存里取纹理数据是很消耗性能的。我决定做一次大胆的尝试:把纹理采样替换成程序化生成的噪声,也就是说Perlin噪声是在fragment shader里实时计算出来的。这种做法听起来是有点激进,但是profile之后,我发现这种做法把渲染时间减少了三分之一,甚至外观也更真实了。

b466c44a13701e2549d6ffaa6f7dbdb5.png

然而事情到这里还没完。

PC和主机平台会在内部把fixed和其他低精度类型转换成float,但是移动设备会区分不同精度的数据类型。它们区分是因为精度的确会影响性能的。所以被把生成噪声的函数里所有的float都改成了fixed,这个时候的渲染时间刚好变为了最开始的一半。

3.更丰富的动态,以及程序化生成

云的动态极其复杂,但是可以总结为三类:

第一,云作为一个整体移动。

第二,云会蠕动、拉伸、甚至扭曲。这都属于它自身形状的改变,我们可以通过移动顶点来模拟。

(Cloud.shader)

// move the cloud around as a whole
o.vertex.xy += sin(_Time.y * _FlowSpeed) * 3.0;
// move every vertex around individually, and make their movement different from each other, which makes the cloud look like wriggling
o.vertex.xyz += (sin(_Time.w * _WriggleSpeed + (v.vertex.x + v.vertex.y + v.vertex.z) * _WriggleVertexDivergence) + 1.0) * _WriggleMagnitude;

第三,水蒸气在云的内部流动,这一点通过流动UV来体现。另外,这里的"UV"不是用来采样纹理的,而是程序化生成噪声所需要的输入参数。

// generate perlin noise in real-time,
fixed perlinNoise = (cnoise(texcoord * 4.0 + _Time.x * 10.0) + 1) * 0.5;

云复杂的动态(无声)

接下来自动地把“云单元”放置在一起,这里我还是用Perlin噪声来实现。我在一块方形区域生成云单元,根据水平位置坐标来生成Perlin噪声。这个噪声值会被阈值,通过阈值的地方可以放置云单元,没通过的地方不可以。但是这个时候的云看起来太“整齐”了,整齐划一地放在网格上,就像一个士兵方阵。那么怎么把它们搞得“乱糟糟”一点呢?所以我在位置、缩放和旋转上都加了随机值。我尤其希望他们的垂直位置(y)形成差异,这样云就看起来厚一点。至于缩放,水平方向(xz)的缩放要比垂直方向的缩放要拉伸得更多,为的是让云看起来更平坦。

(GenerateClouds.cs)

bool OkToPlace (int x, int y)
{
float xCoord = (float)x / oneSideAmount * shapeScale;
float yCoord = (float)y / oneSideAmount * shapeScale;
float sample = Mathf.PerlinNoise(xCoord, yCoord);
// determines what proportion of the sky is covered by clouds if (sample < CoverageRate)
return true;
return false;
}

void Generate()
{
oneSideAmount = totalRange * density;
Vector3 position;
Vector3 scale;
Random.InitState(42);
for (int x = 0; x < oneSideAmount; x++)
{
for (int y = 0; y < oneSideAmount; y++)
{
if (OkToPlace(x, y))
{
Transform cloud = Instantiate(cloudPrefab);
position.x = ((float)x / (float)oneSideAmount) * (totalRange * 8) - (totalRange * 4);
position.z = ((float)y / (float)oneSideAmount) * (totalRange * 8) - (totalRange * 4);
position.y = ((float)Random.Range(-255, 256) / 512f) * thickness + height;
float xRand = ((float)Random.Range(-127, 128) / 512f);
float zRand = ((float)Random.Range(-127, 128) / 512f);
float yRand = Mathf.Min(xRand, zRand);//make the cloud look more flat float scaleRand = ((float)Random.Range(-127, 128) / 512f);
float currentScale = cloudScale * (scaleRand + 1f);
scale.x = currentScale * (xRand + 1f);
scale.z = currentScale * (zRand + 1f);
scale.y = currentScale * (yRand + 1f) * 0.8f;// * 0.8f is to make the cloud look more flat cloud.localPosition = position;
cloud.localScale = scale;
cloud.localRotation = Quaternion.Euler(0, (float)Random.Range(0, 180), 0);
// join the newly created cloud into the CloudGroup parent object cloud.SetParent(transform, false);
// no need for clouds to cast or receive shadows cloud.GetComponent<Renderer>().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
cloud.GetComponent<Renderer>().receiveShadows = false;
}
}
}
}

这个时候从上朝下看是这样的:

a4f78287d0484315c2347481c39af033.png

4.还有几个重要的小Trick

我们希望的是,这个云在中心处紧实,在边缘虚化。所以我们用半径来让噪声值衰减:(Cloud.shader)

fixed procedualTex(fixed2 texcoord)
{
// holistic transparency, set to 0.2 to make clouds look less stuffy when looked from outside
const fixed trasparency = 0.20;
// to make texcoords vary from the range of [-0.5, 0.5]
fixed x = texcoord.x - 0.5;
fixed y = texcoord.y - 0.5;
// make this piece of cloud dense in the middle and fade out towards the edge.
// By writting (x * x + y * y) I wanted to represent the radius, but we don't have to be so rigorous so I left out the square-root,
// and doing square-root is computationally intensive for shaders, by the way.
fixed attenuation = max(((0.25 - (x * x + y * y)) * 4.0 * trasparency), 0.0);
// generate perlin noise in real-time,
fixed perlinNoise = (cnoise(texcoord * 4.0 + _Time.x * 10.0) + 1) * 0.5;
//fixed perlinNoise = (cnoise(texcoord * 4.0 + _Time.y) + 1) * 0.5;
return perlinNoise * attenuation + attenuation * attenuation;
}

从侧边看一片mesh它会看起来像是一个片,这样云有很多尖刺。最直接的解决办法就是,如果法线没有朝向我们,就让云淡去。这个解决方案来自王阳

(Cloud.shader)

fixed4 frag (v2f i) : SV_Target
{
// when this piece of cloud is looked from aside, it looks like a sharp piece, which is not supposed to exist in a real cloud
// we can let this piece of cloud fade out if it's normal is perpendicular to our view direction
const float fade = 0.5;
fixed3 worldNormal = normalize(i.worldNormal);
float3 worldViewDir = normalize(i.worldViewDir);
float rim = abs(dot(worldViewDir, worldNormal));
fixed tmp = step(fade,rim);

// when it approaches the camera's near clip plane, let it fade out,
// or the meshes will be sharply cut by the near clip plane
const half cutFade = 20.0;
half viewDistance = length(i.worldViewDir);
fixed cut = smoothstep(_NearClipPlane, _NearClipPlane + cutFade, viewDistance);


fixed alpha = procedualTex(i.texcoord);

// if the tmp approaches to 1.0, we output alpha, and if the tmp approaches 0.0, we output another term
// by doing this we can avoid IF operation in shader, which stalls the GPU a lot
alpha = alpha * tmp + (1.0 - tmp) * lerp(0.0, alpha, ((max(0, (rim - 0.1))) / (fade - 0.1)));
alpha *= cut;
return fixed4(1.0, 1.0, 1.0, alpha);
}

还有一个tip就是记得打开GPU instancing来减少draw-call,因为我们在哪里都用的是同一个mesh。

当穿越云层的时候

我注意到当摄像机埋在云里面的时候,会看到一些尖锐的片状物:

3e1c270935bfa3023281fce8ce113bbe.png

这是因为,mesh的面和摄像机的近裁剪平面相交时,mesh被硬生生截断了。所以我又在shader里用了个小trick,让云在接近摄像机的时候自动淡去。(代码就是上边的)

5.在低端手机上实测

性能优化的最终目的是兼容低端市场,扩大用户总数。所以我拿来一个低端手机:17年上市的红米Note4X来profile,能跑到40帧左右:

4349cb105f0cd6f5a97651237eb4bf81.png

源码与示例工程:

https://github.com/MarcusXie3D/MobileVolumetricCloud

参考:

  1. 游戏“The Witness”:

    http://www.artofluis.com/3d-work/the-art-of-the-witness/clouds/

  2. 程序化噪声的shader库:https://github.com/keijiro/NoiseShader

  3. 王阳大神的云:王阳:关于风格化云渲染的一些尝试

声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

作者:Marcus Xie

来源:https://zhuanlan.zhihu.com/p/120800393

More:【微信公众号】 u3dnotes

2c457a02f9cc6cd143954abdede59131.png

java中使用公钥密私钥解密原理. KeyGenerater类: public class KeyGenerater { private byte[] priKey; private byte[] pubKey; public void generater() { try { KeyPairGenerator keygen = KeyPairGenerator .getInstance("RSA"); SecureRandom secrand = new SecureRandom(); secrand.setSeed("www.川江号子.cn".getBytes()); // 初始化随机产生器 keygen.initialize(1024, secrand); KeyPair keys = keygen.genKeyPair(); PublicKey pubkey = keys.getPublic(); PrivateKey prikey = keys.getPrivate() pubKey = Base64.encodeToByte(pubkey.getEncoded()); priKey = Base64.encodeToByte(prikey.getEncoded()); System.out.println("pubKey = " + new String(pubKey)); System.out.println("priKey = " + new String(priKey)); } catch (java.lang.Exception e) { System.out.println("生成密钥对失败"); e.printStackTrace(); } } public byte[] getPriKey() { return priKey; } public byte[] getPubKey() { return pubKey; } } Signaturer 类: public class Signaturer { public static byte[] sign(byte[] priKeyText, String plainText) { try { PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.decode(priKeyText)); KeyFactory keyf = KeyFactory.getInstance("RSA"); PrivateKey prikey = keyf.generatePrivate(priPKCS8); // 用私钥对信息生成数字签名 Signature signet = java.security.Signature.getInstance("MD5withRSA"); signet.initSign(prikey); signet.update(plainText.getBytes()); byte[] signed = Base64.encodeToByte(signet.sign()); return signed; catch (java.lang.Exception e) { System.out.println("签名失败"); e.printStackTrace(); } return null; } } SignProvider 类: public class SignProvider { private SignProvider() { } public static boolean verify(byte[] pubKeyText, String plainText, byte[] signText) { try { // 解密由base64编码的公钥,并构造X509EncodedKeySpec对象 X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(Base64.decode(pubKeyText)); // RSA对称密算法 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 取公钥匙对象 PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec); // 解密由base64编码的数字签名 byte[] signed = Base64.decode(signText); Signature signatureChecker = Signature.getInstance("MD5withRSA"); signatureChecker.initVerify(pubKey); signatureChecker.update(plainText.getBytes()) // 验证签名是否正常 if (signatureChecker.verify(signed))return true; return false; } catch (Throwable e) { System.out.println("校验签名失败"); e.printStackTrace(); return false; } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值