Play With KDTree

source code in this page lies here
s3.png
最近工作中实现了基于KDTree的3D空间划分,用于实时判断Mesh和Ray的相交,我现在依然惊讶于这个算法的优美和高效。

还是先从工作中逃离一会,试试用Clojure实现一下2D-KDtree,然后在quil框架来些与工作无关的东西看看。

先生成3000个点吧:


(def pts
  (repeatedly 3000 #(vec2 (q/random (q/width))
                          (q/random (q/height)))))

调用quil的point函数画出来看看

 (doall (map #(apply q/point %) pts))

111.jpg

好了, 现在我们可以将这些点通过KDTree算法来进行二维分割了:


(defn divide [pts rect depth]
  (when (>= (count pts) 1)
    (let [top-left (:p rect)
          axis ([:x :y] (mod depth 2))
          sort-pts (vec (sort-by #(axis %) pts))
          median (quot (count pts) 2)
          translate #(g/translate % (m/- top-left))
          left (map translate (subvec sort-pts 0 median))
          right (map translate (subvec sort-pts (inc median)))
          div-rects (divide-rect
                     rect axis
                     (get-in sort-pts [median axis]))]
      {:pts pts
       :rect rect
       :depth depth
       :left (divide left (div-rects 0) (inc depth))
       :right (divide right (div-rects 1) (inc depth))})))

这其实大致于Wikipedia描述的一致,第一个参数pts是当前待分割的点集,第二个参数rect则是该点集包围矩形,第三参数depth则是分割深度。针对当前点集将其以x轴或y轴分割成两部分子点集并得到他们各自的包围矩形,然后递归调用divide直至最后一个点,这样就得到了kdtree数据。

当中有个特殊的处理:

translate #(g/translate % (m/- top-left))
left (map translate (subvec sort-pts 0 median))
right (map translate (subvec sort-pts (inc median)))

这里会将分割出来的左子集和右子集的点映射到以其父包围矩形的Left-Top为坐标系原点来表示,换言之,每次分割我们都可以理解成得到一个子坐标系,这样做的目的主要是为了方便后面对每一层做缩放变换。

(def kdtree (divide pts (rect (q/width) (q/height)) 0)))

好了,这样就生成了该3000个点集的kdtree,我们可以检测看看深度为9的右子树的数据:

(assoc (get-in kdtree (repeat 9 :right)) :left "..." :right "...")

=> {:pts
     ([42.2945556640625 6.13824462890625]
      [50.94134521484375 40.2008056640625]
      [56.525146484375 41.60552978515625]
      [57.4437255859375 13.4390869140625]),
     :rect
     {:p [35.60064697265625 0.0],
      :size [36.08294677734375 52.22308349609375]},
      :depth 9,
      :left "...",
      :right "..."}

可以看到, 划分到第9层时只剩下4个点了。

当中每一层的包围矩形是我很感兴趣的,那么我们将该kdtree所有的包围矩形画出来看看, 代码很简单:

(defn draw-kdtree [tree]
  (when tree
    (q/push-matrix)  
    (let [rect (:rect tree)
          size (:size rect)]
      (q/translate (:p rect))
      (q/rect 0 0 (:x size) (:y size))
      (draw-kdtree (:left tree))
      (draw-kdtree (:right tree)))
    (q/pop-matrix)))

(draw-kdtree kdtree)

111.jpg

颜色和缩放 #

KDTree本身就呈现出很有意思的秩序感,我可以再加些颜色和缩放看看,假设想要针对每一层包围矩形实施中心缩放操作,并使其内部子包围矩形也会被相应缩放,貌似比较麻烦,但因为上面在生成KDTree数据的时候已经将每一层划分的包围矩形映射到子坐标系了,所以只需要针对每一层子坐标系做中心缩放即可。其次,要填充包围矩形很简单,直接挑选喜欢的颜色调用Fill接口就可以绘制填充色了, 这里我们只填充叶子节点的包围矩形,这两部操作的实现只需要将上面的draw-kdtree拓展为:

(defn draw-kdtree [tree]                      
  (when tree                                  
    (q/push-matrix)                           
    (let [rect (:rect tree)                   
          size (:size rect)]                  
      (q/translate (:p rect))
      (transform-center                       
       (m/div size 2)                         
       (q/scale (q/random 0.8 1.2)))
      (if (is-leaf tree)                      
        (do (q/fill (rand-color))
            (q/rect 0 0 (:x size) (:y size)))
        (do (draw-kdtree (:left tree))        
            (draw-kdtree (:right tree)))))    
    (q/pop-matrix)))

s3.png
s2.png
s1.png

 
8
Kudos
 
8
Kudos

Now read this

再来看看Flocking

Nature Of Code 这本书给我印象最深的就是Flocking示例, 当中的算法来源于Craig Reynolds 1986年 。 CodeOfNature很好的实现了该算法, 但是当中存在一些冗余的计算,以及为了清晰阐述Separation | Alignment | Cohesion这三条规则而采用分别遍历计算,对我来说,不够高效之余,还显得不够直观。这里我对该算法进行了简化。 Boid.prototype.steerToVel = function... Continue →