补充最新情况到开头
最终在各位大佬帮助下发现了测试问题
- 常规二叉树对于 建树顺序比较敏感,不好的顺序会导致严重性能下降。均匀二叉树 对顺序不敏感
- 常规测试中用了set get对于密集算法属性会较大降低性能
因为性能并没有更好,所以本篇不值得看下去 到这里为止!除非是对其他类型二叉树设计感兴趣的可以看看,权当做个记录吧。
以下原文
很可能标题党一次了,因为写完算法后和网上搜到的版本对比发现快了260倍。根据一个码农的自觉,我知道肯定是哪里出了问题的,以往的设计也只能比经典算法快20%到50%(10年前在巨人开发征途时自己设计的二叉堆 比普通寻路用到的快50%,或5年前自己设计的凸多边形寻路比unity的navigation快30%等),所以我知道 百倍的数据肯定是哪里出了问题,虽然我空间多用2,3倍。但凌晨5点多的杭州加上年迈40的我确实一时发现不了,于是吸引一些大佬帮排查下疏漏的动机还是有的。
事情的起因
《生死狙击2》项目的3测显示很多内存或显存不足的用户 无法体验吃鸡,于是我设计了一版volume light probe代替 lightmap方式实现GI,同时避免了漫长的烘焙贴图工作流。当然这个方案也是我如这次均匀二叉树一样自己先想然后去对比资料。发现如果是先看到cod的分享资料那可能就放弃了,大佬帮我们估算那样需要一个人的大半年开发量,但我们4测与3测就隔着1个多月。最终我的简陋方案赶上了测试。
但是 留下了一个尾巴,大量不参与烘焙的物体 在实时阴影距离外会发亮,因为既没有shadowmask又没有用unity的lightprobe(他的probe 超大场景 流式加载动态组合太弱),可能很多人并不知道 lightprobe烘焙时候 不仅仅贡献间接光,同时还描述了遮挡关系(是否阴影内)UnityGetRawBakedOcclusions 函数内half4 probeOcclusion = unity_ProbesOcclusion;就是这个数据(延迟渲染下);所以我需要补上类似的功能,比较适合的是 体素阴影,就是 单位体积记录一个是否在阴影内数据 假设0或1吧。0.2米x0.2米x0.2米在实时阴影外的 精度足够了。直接用texture3d 存这些数据 在2x2公里可视范围内 显存开销很恐怖1w*1w*100 纹素(有办法实现高度只要20米范围,每个大区域给不同偏移高度),这个数据很恐怖自然就想到了 gpu端 八叉树 的数据查找了。
说起八叉树,很多人可能没实现过,但有一点不要误解,很很多高端的 数据结构一样 他并不是为了快 ,其实没什么复杂数据结构能比我们最初接触的连续内存数组偏移快速的。用它仅仅是他只记录0或1其中少量的那种 可以大量节省空间而已。
作为十多年主程序我也没写过,主要是前期长期开发2dmmo,后期又是手游3d小场景。本着用不上的先不自己写,不能自己写出来的就先不看原则。一直就这样错过他,仅仅根据名字脑部了原理。
为了在gpu实现 八叉树 ,先要在 cpu实现 八叉树 ,为了更清晰 先实现一维度的 二叉树再扩展维度。
意外的测试结果
我的版本6ms 网上的 1564ms 找到数量(foundCount)一致,我节点数(nodeCount)多一倍。我需要查找的节点数(checkCount)少很多
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using UnityEngine;
public class TreeTest : MonoBehaviour
{
void Start()
{
List<int> randomValues = new List<int>();
for (int i = 0; i < 32768; i++)
{
if (UnityEngine.Random.Range(0, 100) < 10) randomValues.Add(i);
}
//我写的均等二叉树
test1(randomValues);
//网上常见二叉树
test2(randomValues);
}
static int checkCount;
static int nodeCount;
private void test1(List<int> randomValues)
{
nodeCount = 0;
checkCount = 0;
JkNode root = new JkNode();
root.min = 0;
root.max = 32768-1;
foreach (var item in randomValues)
{
root.addData(item);
}
Stopwatch sw = new Stopwatch();
sw.Start();
int foundCount = 0;
for (int i = 0; i < 32768; i++)
{
if (root.check(i) == 1) {
foundCount++; }
}
sw.Stop();
print("time:"+sw.ElapsedMilliseconds);
print("foundCount:"+foundCount);
print("nodeCount:" + nodeCount);
print("checkCount:"+checkCount);
}
private void test2(List<int> randomValues)
{
nodeCount = 0;
checkCount = 0;
BinaryTree bt = new BinaryTree();
foreach (var item in randomValues)
{
bt.Insert(item);
}
Stopwatch sw = new Stopwatch();
sw.Start();
int foundCount = 0;
for (int i = 0; i < 32768; i++)
{
if (bt.Find(i) != null)
{
foundCount++;
}
}
sw.Stop();
print("time:" + sw.ElapsedMilliseconds);
print("foundCount:" + foundCount);
print("nodeCount:" + nodeCount);
print("checkCount:" + checkCount);
}
// Update is called once per frame
class JkNode
{
public JkNode()
{
nodeCount++;
}
public int flag;//0 表示数据false ;1 表示数据true ;2 只有一个子/孙节点 直接存 value 为true的子节点的index; 3 含有2个或以上子/孙节点 需要递归查 //优化内存 2 存入 -index 用<0判断
public int value;
public JkNode child0;
public JkNode child1;
public int min;
public int max;
internal void addData(int v)
{
if (max == min) flag = 1;
if (flag == 1) return;
if (flag == 0)
{
flag = 2;
value = v;
return;
}
if (flag == 2)
{
flag = 3;
addData(value);
addData(v);
value = 0;
return;
}
if (v < (min + max + 1) / 2)
{
if (child0 == null)
{
child0 = new JkNode();
child0.min = min;
child0.max = (max - min - 1) / 2 + min;
}
child0.addData(v);
}
else
{
if (child1 == null)
{
child1 = new JkNode();
child1.max = max;
child1.min = (max - min - 1) / 2 + min + 1;
}
child1.addData(v);
}
}
internal int check(int v)
{
checkCount++;
if (flag < 2) return flag;
if (flag == 2) return v == value ? 1 : 0;
if (v < (min + max + 1) / 2) return child0 == null ? 0 : child0.check(v);
return child1 == null ? 0 : child1.check(v);
}
}
public class Node
{
public int Item { set; get; } //节点数据
public Node LeftChild { set; get; } //左子节点的引用
public Node RightChild { set; get; } //右子节点的引用
public Node(int data)
{
nodeCount++;
this.Item = data;
}
}
public class BinaryTree
{
//表示根节点
public Node _root;
//查找节点
public Node Find(int key)
{
Node current = _root;
while (current != null)
{
checkCount++;
if (current.Item > key)
{//当前值比查找值大,搜索左子树
current = current.LeftChild;
}
else if (current.Item < key)
{//当前值比查找值小,搜索右子树
current = current.RightChild;
}
else
{
return current;
}
}
return null;//遍历完整个树没找到,返回null
}
//插入节点
public bool Insert(int data)
{
Node newNode = new Node(data);
if (_root == null)
{//当前树为空树,没有任何节点
_root = newNode;
return true;
}
else
{
Node current = _root;
Node parentNode = null;
while (current != null)
{
parentNode = current;
if (current.Item > data)
{//当前值比插入值大,搜索左子节点
current = current.LeftChild;
if (current == null)
{//左子节点为空,直接将新值插入到该节点
parentNode.LeftChild = newNode;
return true;
}
}
else
{
current = current.RightChild;
if (current == null)
{//右子节点为空,直接将新值插入到该节点
parentNode.RightChild = newNode;
return true;
}
}
}
}
return false;
}
}
}
本方案思路
写之前没有听过 写之后没查到 所以我叫这种我今晚脑洞出来的算法叫均匀二叉树,因为每个子节点都是区域中心平分创建的,不是经典二叉树 只区别大小分左右。
每个node 有 flag参数 有min和max范围 有value参数(可以和flag合并 为了可读性不合并)还有左节点 child0 右节点child1
实际测试时 32768 ,这里方便讲原理 用0-16范围,用0表示阴影外 1表示阴影内讲述
(以下描述可能不直观可先看图)
第一步 创建root这个node,min=0,max=15;当要addData(6)时,判断下 再左一半区域还是又一半区域,这里左是[0,7] 所以 要添加到 左节点,如果左节点不存在 就创建它,不存在子节点的节点 flag设置为0(表示这个节点没任何位置在阴影内) ,当节点min和max相等 已经不可再分的时候 flag设置为1 表示这个节点的数值在阴影内。
第二步 当一个节点只有一个子孙节点时 可以优化下 不去创建子节点直接用 value存入 阴影内位置,flag标记为2 查到这个子节点时候只要比较 查找位置是否与value相同,如果不同 附近不再有子节点了肯定就不在阴影内。
第三步 当一个节点flag=2,又需要设置一个阴影内数据时,需要把value和新数据都去创建子节点,自己flag 改为3,表示 自己有多个子孙节点 不帮助记录了 遍历子节点吧。
一图解千愁,4 8 9表示在阴影内,
当查询4的时候 首先在root的[0,15] 左半边所以去[0,7]节点,这个节点说 它记录了它这支唯一阴影内数据 value就是4 所以相等可判断在阴影内。如果查5 .就不等在阴影外。如果查11,会到[8,15]节点去查,然后又到[8,11]节点查,因为不存在[10,11]节点 所以判断不在阴影内。
实际测试代码最后的89没合并有点浪费空间。
对比经典二叉树图
ps:标题出意外导致没判断出是在写稿没自动保存,写完直接丢失,本来代码只写到4点多,结果写2次知乎 天亮了 惨痛!!!