//	update mesh cache
        void UpdateMeshCache()
        {
            if (Time.realtimeSinceStartup < lastUpdateCacheTime + 1.0f)
            {
                return;
            }

            lastUpdateCacheTime = Time.realtimeSinceStartup;

            const float maxStayTime = 30.0f;               // 30s
            List <int>  rmList      = new List <int>();

            foreach (DictionaryEntry de in meshTable)
            {
                QuadtreeMesh mesh = de.Value as QuadtreeMesh;
                if (Time.realtimeSinceStartup > mesh.ts + maxStayTime)
                {
                    mesh.Destroy();
                    rmList.Add((int)de.Key);
                }
            }

            foreach (int nodeIndex in rmList)
            {
                meshTable.Remove(nodeIndex);
            }
        }
        //	request a node's mesh
        public bool RequestNodeMesh(int nodeIndex)
        {
            Debug.Assert(nodeIndex >= 0);

            //	Check in mesh cache
            QuadtreeMesh mesh = meshTable[nodeIndex] as QuadtreeMesh;

            if (mesh != null)
            {
                //	update time stamp
                mesh.ts = Time.realtimeSinceStartup;
                return(true);
            }

            //	Single thread loading
            mesh = CreateNodeMesh(nodeIndex);
            if (mesh != null)
            {
                //	Push to table
                meshTable.Add(nodeIndex, mesh);
                return(true);
            }
            else
            {
                Debug.Assert(mesh != null);
                return(false);
            }
        }
        QuadtreeMesh CreateNodeMesh(int nodeIndex)
        {
            try
            {
                QtreeNode node = terrain.qtree.GetQtreeNode(nodeIndex);

                int rowVertNum = terrain.vertexNumInNodeRow;
                int numVert    = rowVertNum * rowVertNum;

                //	Prepare temporary buffers
                if (tempVertNum != numVert)
                {
                    //	In fact, except the first time, the code should never run into here,
                    //	for all quadtree nodes should have same number of vertex in their inborn LOD mesh
                    tempVerts   = new VertexBuffer(numVert);
                    tempVertNum = numVert;
                }

                int   gridSize = terrain.gridSize;
                float miny     = 0.0f;
                float maxy     = 0.0f;

                //	Get mesh from height blender
                if (!terrain.gemBuilder.BuildQTreeNodeMesh(node.hmLeft, node.hmTop, node.gridStep,
                                                           rowVertNum, gridSize, tempVerts, out miny, out maxy))
                {
                    Debug.Assert(false);
                    return(null);
                }

                //	calculate mesh's aabb in local space
                Rect    rcLocal = node.CalcLocalArea(terrain.gridSize);
                Vector3 mins    = new Vector3(rcLocal.xMin, miny, rcLocal.yMin);
                Vector3 maxs    = new Vector3(rcLocal.xMax, maxy, rcLocal.yMax);

                //	Create mesh object
                QuadtreeMesh nodeMesh = new QuadtreeMesh(terrain, nodeIndex, mins, maxs)
                {
                    ts = Time.realtimeSinceStartup,
                };

                //	Create height map
                nodeMesh.heights = new float[numVert];
                for (int i = 0; i < numVert; i++)
                {
                    nodeMesh.heights[i] = tempVerts.verts[i].pos.y;
                }

                //	Create vertex buffer
                nodeMesh.CreateVertCB(tempVerts);

                return(nodeMesh);
            }
            catch
            {
                Debug.LogFormat("Failed to create node mesh {0}", nodeIndex);
                return(null);
            }
        }
        //	Get mesh for a quadtree node
        public QuadtreeMesh GetNodeMesh(int nodeIndex)
        {
            Debug.Assert(nodeIndex >= 0);

            QuadtreeMesh mesh = meshTable[nodeIndex] as QuadtreeMesh;

            return(mesh);
        }
        void CollectRenderNodes_r(int nodeIndex)
        {
            QtreeNode node = nodeBuf[nodeIndex];

            //	Only render node whose info is up-to-date
            if (node.updateCnt != updateCnt)
            {
                //	If a node isn't up-to-date, all it's children must not be.
                return;
            }

            //	do camera cull with node's AABB at first, but node may not have min-y and max-y
            //	at this moment if it's mesh wasn't built, so now we only do a conservative check
            //	with x and z axis.
            //	NOTE: we ONLY consider tranlation of terrain, but not rotation and scale
            Vector3 offset  = terrain.transform.position;
            Rect    rcLocal = node.CalcLocalArea(terrain.gridSize);
            Vector3 mins    = new Vector3(rcLocal.xMin + offset.x, -10000.0f, rcLocal.yMin + offset.z);
            Vector3 maxs    = new Vector3(rcLocal.xMax + offset.x, 10000.0f, rcLocal.yMax + offset.z);
            Bounds  aabb    = new Bounds();

            aabb.SetMinMax(mins, maxs);

            if (!GeometryUtility.TestPlanesAABB(camPlanes, aabb))
            {
                return;                    //	The whole node isn't visible
            }
            if (node.meshState == (byte)eNodeMeshState.LOAD_INBORN ||
                node.meshState == (byte)eNodeMeshState.LOD_GRADEUP)
            {
                //	Get index buffer according to neighbour's LOD grade
                int sideMask = 0;
                for (int i = 0; i < 4; i++)
                {
                    if (node.neighbour[i] < 0)
                    {
                        continue;
                    }

                    QtreeNode neighbour = nodeBuf[node.neighbour[i]];
                    if (neighbour.updateCnt == updateCnt && neighbour.areaLOD > node.areaLOD)
                    {
                        sideMask |= (1 << i);
                    }
                }

                //	This node's may be drawn
                ComputeBuffer _vertCB  = null;
                ComputeBuffer _indexCB = null;
                QuadtreeMesh  nodeMesh = null;

                if (node.meshState == (byte)eNodeMeshState.LOAD_INBORN)
                {
                    nodeMesh = terrain.meshMan.GetNodeMesh(nodeIndex);
                    if (nodeMesh != null)
                    {
                        _vertCB  = nodeMesh.vertCB;
                        _indexCB = terrain.trnRes.GetNodeIndexCB(sideMask, -1);
                    }
                }
                else if (node.meshState == (byte)eNodeMeshState.LOD_GRADEUP)
                {
                    //	LOD grade-up rendering, use parent's mesh
                    Debug.Assert(node.parent >= 0);
                    nodeMesh = terrain.meshMan.GetNodeMesh(node.parent);
                    if (nodeMesh != null)
                    {
                        _vertCB  = nodeMesh.vertCB;
                        _indexCB = terrain.trnRes.GetNodeIndexCB(sideMask, node.childPos);
                    }
                }

                if (_vertCB != null && _indexCB != null)
                {
                    //	do camera cull again with more precise aabb
                    mins.y = nodeMesh.aabb.min.y + offset.y;
                    maxs.y = nodeMesh.aabb.max.y + offset.y;
                    aabb.SetMinMax(mins, maxs);
                    if (!GeometryUtility.TestPlanesAABB(camPlanes, aabb))
                    {
                        return;                            // The whole node isn't visible
                    }
                    //	Push node to rendering collector
                    DRAWDATA drawData = new DRAWDATA()
                    {
                        node     = node,
                        nodeAABB = aabb,
                        vertCB   = _vertCB,
                        indexCB  = _indexCB,
                    };

                    curRenderer.PushDrawData(drawData);
                }
            }

            if (!node.IsLeaf())
            {
                //	Go on for children
                for (int i = 0; i < 4; i++)
                {
                    CollectRenderNodes_r(node.children[i]);
                }
            }
        }