2020-06-03

Quasar + SSR + Quill WYSIWYG Editor

透過 npm 安裝 quill editor
npm install vue-quill-editor

在 Quasar SSR 模式之下需要另外設定 boot file,首先在 boot 資料夾內新增 quill.js 檔案 :
import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'

import 'quill/dist/quill.core.css' // import styles
import 'quill/dist/quill.snow.css' // for snow theme

Vue.use(VueQuillEditor, /* { default global options } */)

export default ({ app, router, store, Vue }) => {
  //init axios setup

  //interceptors setup
  //...
  
}

設定 quasar.conf.js 新增 quill 設定 :
...

// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://quasar.dev/quasar-cli/cli-documentation/boot-files
boot: [

  'i18n',
  'axios',
  { path:'quill', server:false },
],

...

在 component 內使用 quill editor
<template>
  <q-page>
    <quill-editor v-model="html_content"></quill-editor>
  </q-page>
</template>

2020-05-08

NodeJS 後端取得 Google Play Games 玩家資訊


玩家在 Android 裝置登入 Google Play Games 之後可以取得 Server Auth Code (參照 這裡),後端可利用 Server Auth Code 取得玩家的 Access Token,再由 Access Token 取得玩家資訊 (取得玩家資訊的 API 參照 這裡)。

底下是 NodeJS 利用 Server Auth Code 取得 Access Token 的實作 :
var querystring =require('querystring')
var https =require('https')

var post_data =querystring.stringify({
  grant_type:'authorization_code',
  code:/*CLIENT 給的 SERVER AUTH CODE*/,
  client_id:/*Google API Console 取得的 client id*/,
  client_secret:/*Google API Console 取得的 client secret*/,
  redirect_uri:'',
})

var post_options ={
  host:'oauth2.googleapis.com',
  port:443,
  path:'/token',
  method:'POST',
  headers:{
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': Buffer.byteLength(post_data)
  }
}

var post_req =https.request(post_options, (res)=>{
  res.setEncoding('utf8')
  var body =''
  res.on('data', (chunk)=>{
    body +=chunk
  })
  res.on('end', ()=>{
    console.log(body)
  })
})
post_req.write(post_data)
post_req.end()

執行之後應該會顯示類似內容 :
{
  "access_token": "ya29.a0Ae4lvC3sSLZwqGO8iSHsnzErS_GbBc021o9Q3FhDV_bb0mu3ukKF-_D5bf1xtvIWTFEAZLRODhlfdxetMrUEfQtt5jqhsH8oUrBlGll575VnLND_WGovv98y_tzVhceh6Ti42qrpdO9A6vKtWUGaHeOtxSb71KgP0U8",
  "expires_in": 3599,
  "refresh_token": "1//0es-vhoIE4URRCgYIARAAGA4SNwF-L9IrM22Sj9e2zM-SJpjgocfM4wcYhF2i8rRossKhSxCGj1nDEVy7BmSeWGmhl4",
  "scope": "https://www.googleapis.com/auth/games_lite",
  "token_type": "Bearer"
}

有了 access_token 就可以用來取得玩家資訊 :
var player_id ='玩家的 ID'
var api_key ='Google API Console 取得的 API Key'
var get_options ={
  host:'www.googleapis.com',
  port:443,
  path:'/games/v1/players/'+player_id+'?key='+api_key,
  method:'GET',
  headers:{
    Authorization: 'Bearer '+access_token,
    Accept: 'application/json'
  }
}
var get_req =https.request(get_options, (res)=>{
  res.setEncoding('utf8')
  var body =''
  res.on('data', (chunk)=>{
    body+=chunk
  })
  res.on('end', ()=>{
    console.log(body)
  })
})
get_req.end()

範例程式裡提到的 Client ID, Client Secret 和 API Key 可以由 Google API Console 取得,參考如下圖 :


其中 A. 是 API Key,C. 下載的 JSON 檔案裡包含 Client ID/ Client Secret,這裡要注意的是 B. OAuth 用戶端 ID 的類型是網路應用程式。

2020-01-08

Bootstrap 4 Navbar Style 調整

此文記錄在 Vue 架構下調整 Bootstrap 4 Navbar style,讓它與 Bootstrap 3 Navbar 相同。調整項目如下 :

1. 點選 Dropdown Menu 時不出現藍色外框 ( 似乎是 Safari 上才會出現 )。
2. 被滑鼠指標 hover 的 Dropdown Menu 項目顯示藍底白字。
3. Active Navbar 項目顯示深底色白字。
4. Dropdown Menu 增加陰影。

下圖是 Bootstrap 4 Navbar 在 Safari 上點選 Dropdown Menu 時的擷圖 :
下圖是調整後的 Navbar 擷圖 :

1. 安裝 sass-loader 與 node-sass
npm install sass-loader
npm install node-sass
2. 在 main.js 檔案所在資料夾新增檔案 bootstrap.scss,檔案內容如下 :
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";

$dropdown-link-hover-color: $light;
$dropdown-link-hover-bg:    $blue;
.dropdown-item{
  padding: .2rem 1.5rem !important;
}

.nav-link.active {
  background:$gray-900;
}

@media (min-width: 576px) {
  .navbar {
    padding-top: 0px !important;
    padding-bottom: 0px !important;
  }
}
.navbar-nav .nav-item .nav-link {
  padding-top: 1rem !important;
  padding-bottom: 1rem !important;

  padding-left: 1rem !important;
  padding-right: 1rem !important;
}

.nav-item a.active.focus,
.nav-item a.active:focus,
.nav-item a.focus,
.nav-item a:active.focus,
.nav-item a:active:focus,
.nav-item a:focus {
    outline: 0;
    outline-color: transparent;
    outline-width: 0;
    outline-style: none;
    box-shadow: 0 0 0 0 rgba(0,123,255,0);
}

@import '~bootstrap/scss/bootstrap';
3. main.js 檔案調整如下 :
import Vue from 'vue'
import App from './App.vue'

import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import './bootstrap.scss'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Vue 2.6 + Bootstrap 4

1. 首先安裝 vue-cli ( 安裝的版本是 4.1.2 )
npm install -g @vue/cli

2. 利用 vue-cli 建立 project
vue create my-vue-project
出現 present 選項時直接選擇預設選項 default (babel, eslint),完成之後會建立一個 my-vue-project 資料夾,在資料夾內執行 npm run serve 之後用瀏覽器連線至 http://localhost:8080/,成功的話可以看到 welcome to vue 的預設畫面。

3. 安裝 bootstrap,在 my-vue-project 資料夾執行 : ( 安裝的版本是 4.4.1 )
npm install bootstrap

4. 安裝 jquery 及 popper : (jquery 版本是 3.4.1, popper.js 版本是 1.16.0
npm install jquery
npm install popper.js

5. 在 vue 上測試 bootstrap :
a. 首先在 main.js import bootstrap,main.js 檔案修改如下 :
import Vue from 'vue'
import App from './App.vue'

//新增 bootstrap import
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
b. 在 components 資料夾內新增加一個 vue component,檔名為 BootstrapTest.vue,檔案內容如下 :
<template>
  <div class="container-fluid">
    <!-- Modal -->
    <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            ...
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
            <button type="button" class="btn btn-primary">Save changes</button>
          </div>
        </div>
      </div>
    </div>

    <!-- Button Test -->
    <div class="card">
      <div class="card-body">
        <ul class="list-group list-group-flush">
          <li class="list-group-item">      
            <h5 class="card-title">Button Test</h5>
            <button type="button" class="btn btn-primary">Primary</button>
            <button type="button" class="btn btn-secondary">Secondary</button>
            <button type="button" class="btn btn-success">Success</button>
            <button type="button" class="btn btn-danger">Danger</button>
            <button type="button" class="btn btn-warning">Warning</button>
            <button type="button" class="btn btn-info">Info</button>
            <button type="button" class="btn btn-light">Light</button>
            <button type="button" class="btn btn-dark">Dark</button>
            <button type="button" class="btn btn-link">Link</button>
          </li>
          <li class="list-group-item">
            <h5 class="card-title">Dropdowns / Input</h5>
            <div class="btn-group">
              
                <div class="row">
                  <div class="col-sm-6">
                    <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                      Action
                    </button>
                    <div class="dropdown-menu">
                      <a class="dropdown-item" href="#">Action</a>
                      <a class="dropdown-item" href="#">Another action</a>
                      <a class="dropdown-item" href="#">Something else here</a>
                      <div class="dropdown-divider"></div>
                      <a class="dropdown-item" href="#">Separated link</a>
                    </div>

                  </div>
                  <div class="col-sm-6">
                    <div class="input-group">
                      <div class="input-group-prepend">
                        <span class="input-group-text" id="basic-addon1">@</span>
                      </div>
                      <input type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="basic-addon1">
                    </div>

                  </div>
                </div>
              

            </div>
          </li>
          <li class="list-group-item">
            <h5 class="card-title">Modal</h5>
            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal">
              Launch demo modal
            </button>
          </li>
          <li class="list-group-item">
            <h5 class="card-title">Toast</h5>
            <button type="button" class="btn btn-primary" v-on:click="showToastr">
              Show Toast
            </button>
          </li>
        </ul>
      </div>
    </div>

    <!-- Toast -->
    <div id="toast" class="toast" style="position: absolute; top: 10px; right: 10px; display:none;">
      <div class="toast-header">
        <strong class="mr-auto">Bootstrap</strong>
        <small class="text-muted">11 mins ago</small>
        <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="toast-body">
        Hello, world! This is a toast message.
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery'
export default {
  methods: {
    showToastr: function () {
      $('#toast').toast('show')
      $('#toast').show()
    }
  },
  mounted(){
    $('.toast').toast({
      autohide:false
    })
  }
}
</script>

c. 把預設的 HelloWorld.vue 替換成 BootstrapTest.vue,App.vue 檔案修改如下 :
<template>
  <div id="app">
    <!-- <img alt="Vue logo" src="./assets/logo.png"> -->
    <!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
    <BootstrapTest/>
  </div>
</template>

<script>
// import HelloWorld from './components/HelloWorld.vue'
import BootstrapTest from './components/BootstrapTest.vue'

export default {
  name: 'app',
  components: {
    BootstrapTest
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
此時瀏覽器應該能自動 reload 並顯示畫面 :
此時可以測試看看各元件顯示,Modal/Toast 運作是否正常,都能運作的話那 Bootstrap 應該是成功 import 了。

2018-11-04

Konva.js + Spot light effect (Masking)

這篇文章提供利用 Konva.js 實現 Spotlight 效果之範例。

++
底圖 + 80%填充摭黑 + Cutout Spotlight

Konva.js 部分之程式碼 :

...

<div id="stage-parent" style="width: 50%;">
  <div id="scene_container">
  </div>
</div>

...

<script>

  var stage = new Konva.Stage({
    container: 'scene_container',
    width: 800,
    height: 600
  })

  var stage_scale =1
  var fitStageIntoParentContainer =function() {
    var container = document.querySelector('#stage-parent')

    // now we need to fit stage into parent
    var containerWidth = container.offsetWidth
    // to do this we need to scale the stage
    stage_scale = containerWidth / 800

    stage.width(800 * stage_scale)
    stage.height(600 * stage_scale)
    stage.scale({ x: stage_scale, y: stage_scale })
    stage.draw()
  }
  window.addEventListener('resize', fitStageIntoParentContainer)
  fitStageIntoParentContainer()

  //
  // background layer
  //
  var layer = new Konva.Layer()
  stage.add(layer)

  var bgImg = new Konva.Image({
    width: 800,
    height: 600
  })
  layer.add(bgImg)
  var bgImgObj = new Image()
  bgImgObj.onload = function() {
    bgImg.image(bgImgObj)
    layer.draw()
  }
  bgImgObj.src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJG-1Bt_pth4w6CLtGXS5W6SY8vGtrQ6Jk3VOo4dY6NinzpcW4XAQpZi2VQFm-IQs3FwGC7IptmNtFraJHEH8Zj4SQQxJ_TXo054m7WLxfgOO8dS_iuvwXyqxdp15M5anhSxwv08m8j-Cg/s1600/DSC06624m.jpg'

  //
  // mask layer
  //
  var layer_mask =new Konva.Layer()
  stage.add(layer_mask)
  var opacity_rect = new Konva.Rect({
    x: 0,
    y: 0,
    width: 800,
    height: 600,
    fill: 'black',
    draggable: false,
    opacity: 0.8
  })
  layer_mask.add(opacity_rect)

  var transparent_circle = new Konva.Circle({
    x: 400,
    y: 300,
    radius: 200,
    fill: 'black',
    draggable: true,
    globalCompositeOperation: 'destination-out'
  })
  layer_mask.add(transparent_circle)
  layer_mask.draw()

</script>
Konva.js 執行結果 Demo (Spotlight 可拖曳) :

2018-08-23

Unity 3D + V-HACD

碰撞偵測在物理動力模擬是不可缺少的功能,兩個複雜多邊形物體在執行碰撞偵測時需要做很多幾何運算,因此若要遊戲的畫面能夠流暢顯示,遊戲中的碰撞偵測就必須要能達到 real-time 的等級,目前能夠快速執行碰撞偵測的演算法[1][2]幾乎都是基於 Convex Collider 去計算(Unity 3D 使用的物理引擎 PhysX 也包括在內)。

現實世界裡大部份物體都不屬於 Convex 多邊形物體 (Nonconvex Polyhedra),這些 Nonconvex 多邊形物體也稱為 Concave 多邊形物體,為了解決無法對 Concave Collider 進行碰撞偵測的問題,可以把 Concave 多邊形分解成多個 Convex 多邊形。V-HACD[3]就是一個分解 Concave 多邊形的演算法,它所產生的 Convex 多邊形並非完全等於原始多邊形,它的目標是以少量的 Convex 多邊形去近似原始的多邊形,能在不失真以及產生的 Convex 多邊形數目上取得平衡。


需要 7611 個 Convex 多邊形來組合成原始多邊形 (來源[3])

Unreal Engine 4 也使用 V-HACD 來分解 Concave 多邊形。(來源[4])

底下的步驟敘述如何編譯 V-HACD 並在 Unity 中使用 (macOS/Windows) :
1. V-HACD plugin for Unity macOS
1.1 xcode 編譯 vhacd
這裡取得 V-HACD 程式原始碼並確認已有安裝 python 及 cmake 之後,在 install 資料夾裡執行指令 (它會在 build/mac 底下建立 xcode 專案)
./run.py --cmake

執行成功之後在 build/mac 資料夾開啟專案檔案 VHACD.xcodeproj,然後編譯出 Library (Scheme 選擇 ALL_BUILD>My Mac),編譯完成後會看到靜態連結檔 build/mac/VHACD_Lib/Release/libvhacd.a。

1.2 建立 Bundle 專案
在 xcode 另外建立新的 Bundle 專案。

選擇 macOS > Bundle

Linked Frameworks and Libraries 新增剛產生的 libvhacd.a

General > Linked Frameworks and Libraries 新增 libvhacd.a

ps. 後最編譯時若出現下列錯誤訊息則 Linked Frameworks and Libraries 再新增 OpenCL.framework
Undefined symbols for architecture x86_64:
  "_clBuildProgram", referenced from:
      VHACD::VHACD::OCLInit(void*, VHACD::IVHACD::IUserLogger*) in libvhacd.a(VHACD.o)
...

OpenCL.framework and libvhacd.a

Search Paths 的 Header Search Paths 新增指向 vhacd 原始碼路徑 src/VHACD_Lib/public,Library Search Paths 新增指向 vhacd 產生靜態連結Lib的路徑 build/mac/VHACD_Lib/Release。

指向 Header Search Path 及 Library Search Path 的相對路徑

新增 Header File stdafx.h :
#ifndef stdafx_h
#define stdafx_h

#ifdef _WIN32
  #include "targetver.h"

  #define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
  // Windows Header Files:
  #include <windows.h>
  #define MODULE_API __declspec(dllexport)
  #define STD_CALL __stdcall
#else
  #define MODULE_API
  #define STD_CALL
#endif

// TODO: reference additional headers your program requires here
#include "VHACD_api.h"
#include "VHACD.h"
#include "ConvexDecompTool.h"
#include <string.h>
#include <stdio.h>


#endif /* stdafx_h */

新增 Header File targetver.h :
#ifndef targetver_h
#define targetver_h

#include <SDKDDKVer.h>

#endif /* targetver_h */

新增 Header File ConvexDecompTool.h :
#ifndef ConvexDecompTool_h
#define ConvexDecompTool_h

#include "stdafx.h"

using namespace VHACD;

class FVHACDProgressCallback : public IVHACD::IUserCallback
{
private:
  CBFunc tmp_cb_func;
  
public:
  FVHACDProgressCallback(CBFunc pCBFunc);
  ~FVHACDProgressCallback() {};
  
  void Update(const double overallProgress, const double stageProgress, const double operationProgress, const char * const stage,  const char * const operation);
};

#endif /* ConvexDecompTool_h */

新增 Cpp File ConvexDecompTool.cpp :
#include "stdafx.h"

FVHACDProgressCallback::FVHACDProgressCallback(CBFunc pCBFunc){
  tmp_cb_func =pCBFunc;
}

void FVHACDProgressCallback::Update(const double overallProgress, const double stageProgress, const double operationProgress, const char * const stage,  const char * const operation)
{
  
  //process callback info.
  if (tmp_cb_func !=nullptr){
    char buff[255];
    memset(buff, sizeof(char)*255, 0);
    int strlen =sprintf(buff, "%s", stage);
    
    tmp_cb_func(overallProgress, stageProgress, operationProgress, strlen, buff);
  }
  
};
ConvexDecompTool 的作用是接收 v-hacd 處理時的進度及狀態資料,我們會將它傳回給 Unity 並且在 v-hacd 處理時顯示出來。

新增 Header File VHACD_api.h :
#ifndef VHACD_api_h
#define VHACD_api_h

#include "stdafx.h"

extern "C" {
  typedef void (STD_CALL *CBFunc)(double overallProgress, double stageProgress, double operationProgress, int str_sz, char* str_buff);
}

#endif /* VHACD_api_h */
此處的 CBFunc 對應到 Unity C# 的 delegate,可以利用此 Callback Function 將處理進度及資訊傳給 Unity。

新增 Cpp File VHACD_api.cpp :
#include "stdafx.h"

using namespace VHACD;

static void InitParameters(IVHACD::Parameters &VHACD_Params, unsigned int InHullCount, unsigned int InMaxHullVerts, unsigned int InResolution)
{
  VHACD_Params.m_resolution = InResolution; 
  VHACD_Params.m_maxNumVerticesPerCH = InMaxHullVerts; 
  VHACD_Params.m_concavity = 0;
  VHACD_Params.m_maxConvexHulls = InHullCount;
  VHACD_Params.m_oclAcceleration = false;
  VHACD_Params.m_minVolumePerCH = 0.003f;
  VHACD_Params.m_projectHullVertices = true;
};

struct DecomposedHullData{
  int pts;
  int tris;
  double* pts_arr;
  int* tris_arr;
};

DecomposedHullData* dhd =nullptr;
int num_dhd_sz =0;

extern "C" {
  MODULE_API void DisposeHullData(){
    if (dhd !=nullptr){
      for (int i=0;i<num_dhd_sz;++i){
        delete[] dhd[i].pts_arr;
        delete[] dhd[i].tris_arr;
      }
    }
    delete[] dhd;
    dhd =nullptr;
    num_dhd_sz =0;
  }

  MODULE_API int GetHullSize(){
    return num_dhd_sz;
  }

  MODULE_API bool GetTrianglesData(int idx, int* tri_num, int** tri_data){
    if (idx< num_dhd_sz && dhd !=nullptr){
      *tri_num =dhd[idx].tris;
      *tri_data =dhd[idx].tris_arr;
      return true;
    }
    return false;
  }

  MODULE_API bool GetVerticesData(int idx, int* vertices_num, double** vertices_data){
    if (idx< num_dhd_sz && dhd !=nullptr){
      *vertices_num =dhd[idx].pts;
      *vertices_data =dhd[idx].pts_arr;
      return true;
    }
    return false;
  }

  //hull_count range (2~64)
  //max_tri_count range (6~32)
  //resolution range (10000 ~ 1000000)
  MODULE_API bool DecomposeMeshToHulls(
    unsigned int hull_count, 
    unsigned int max_tri_count, 
    unsigned int res, 
    const float* const Verts, 
    const unsigned int NumVerts, 
    const unsigned int* const Tris, 
    const unsigned int NumTris, 
    CBFunc pcb){
    IVHACD* InterfaceVHACD =CreateVHACD();
    
    IVHACD::Parameters VHACD_Params;
    InitParameters(VHACD_Params, hull_count, max_tri_count, res);
    
    FVHACDProgressCallback* VHACD_Callback =new FVHACDProgressCallback(pcb);
    VHACD_Params.m_callback =VHACD_Callback;
    
    bool bSuccess =InterfaceVHACD->Compute(Verts, NumVerts, Tris, NumTris, VHACD_Params);

    if (bSuccess){
      //iterate over each result hull
      unsigned int NumHulls =InterfaceVHACD->GetNConvexHulls();

      DisposeHullData();
      dhd =new DecomposedHullData[NumHulls];

      for (int HullIdx=0; HullIdx<NumHulls; HullIdx++){
        IVHACD::ConvexHull Hull;
        InterfaceVHACD->GetConvexHull(HullIdx, Hull);

        dhd[HullIdx].pts =(int)Hull.m_nPoints;
        dhd[HullIdx].tris =(int)Hull.m_nTriangles;
        dhd[HullIdx].pts_arr =new double[3*Hull.m_nPoints];
        dhd[HullIdx].tris_arr =new int[3*Hull.m_nTriangles];
        memcpy(dhd[HullIdx].pts_arr, Hull.m_points, sizeof(double)*3*Hull.m_nPoints);
        memcpy(dhd[HullIdx].tris_arr, Hull.m_triangles, sizeof(unsigned int)*3*Hull.m_nTriangles);

      }

      num_dhd_sz =NumHulls;
    }else{
      DisposeHullData();
    }
    
    InterfaceVHACD->Clean();
    InterfaceVHACD->Release();
    
    delete VHACD_Callback;
    VHACD_Callback =nullptr;
    
    return bSuccess;
  }
}
其中 GetHullSize, GetTrianglesData, GetVerticesData 的作用是取得處理完成的資料,DecomposeMeshToHulls 的作用是初始化並呼叫執行 V-HACD。
將上述檔案加入至專案然後執行編譯即可取得 Unity 可呼叫之 bundle 檔案。

2. V-HACD plugin for Unity Windows
2.1 VisualStudio 編譯 vhacd
這裡取得 V-HACD 程式原始碼並確認已有安裝 python 及 cmake 之後,修改在 install 資料夾裡的 run.py 檔案,將第 11 行改成 :
cmd = "cmake -A x64 " + arg + " ../" + src_dir
-A x64 讓 cmake 產生 x64 平台的專案。

cmake 在安裝時選擇 Add CMake to the system PATH for all users 或者 Add CMake to the system PATH for the current user 之後在執行 run.py 時比較不會有問題。

執行 run.py (它會在 build/win32 底下建立 VisualStudio Solution)
py run.py --cmake

執行成功之後在 build/win32 資料夾開啟專案檔案 VHACD.sln,然後編譯出 Library (記得把 vhacd 和 testVHACD 的 C/C++ -> Code Generation -> Runtime Library 設定成 Multi-threaded),編譯完成後會看到靜態連結檔 build/win32/VHACD_Lib/Release/vhacd.lib。

Runtime Library 選擇 Multi-threaded 以避免之後 Unity 載入 DLL 時發生無法載入問題。

2.2 建立 DLL 專案 (Visual C++ -> Windows Desktop -> Dynamic-Link Library (DLL) )
將專案目前的 solution platform 改成 x64。在 C/C++ -> General -> Additional Include Directories 新增路徑指向 V-HACD 資料夾 src\VHACD_Lib\public。

新增路徑指向 V-HACD public header 資料夾 (圖中的路徑要換成你電腦中的路徑)

C/C++ -> Code Generation -> Runtime Library 改成 Multi-threaded。
Linker -> General -> Additional Library Directories 新增路徑指向 V-HACD 資料夾 build\win32\VHACD_Lib\Release。

新增路徑指向 V-HACD lib 檔案資料夾 (圖中的路徑要換成你電腦中的路徑)

Linker -> Input -> Additional Dependencies 新增 lib 名稱 vhacd.lib (此檔名是 2.1 編譯產生的檔案)

新增 vhacd.lib 檔名

將 1.2 列出的檔案 stdafx.h,targetver.h,VHACD_api.h,ConvexDecompTool.h,ConvexDecompTool.cpp,WHACD_api.cpp,加入至專案中,並將檔案 ConvexDecompTool.cpp 的 sprintf 改成 sprintf_s最後執行編譯即可獲得 Unity 可呼叫使用的 DLL 檔案

3. Unity C#
3.1 設定 bundle/dll 檔案
把 VHACD_Bundle.bundle 或 VHACD_DLL.dll 複製到 Unity Assets 資料夾 (bundle 及 dll 檔案是 1.2 及 2.2 編譯產生的檔案,可能因為專案命名不同而產生不同檔名),並且統一命名 VHACD_Lib,其 platform 設定參考如圖


VHACD_Lib.bundle platform 設定

VHACD_Lib.dll platform 設定

3.2 VHACD_Lib.cs 檔案
底下是 VHACD_Lib 在 Unity 裡簡單的使用範例 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using UnityEditor;
using System.Runtime.InteropServices;

public class VHACD_Lib : MonoBehaviour {

  [DllImport("VHACD_Lib")]
  private static extern void DisposeHullData();

  [DllImport("VHACD_Lib")]
  private static extern bool DecomposeMeshToHulls(
    uint hull_count, 
    uint max_tri_count, 
    uint resolustion, 
    float[] verts, 
    uint num_verts, 
    int[] tris, 
    uint num_tris,
    VHACD_CBFunc_Handler pHandler
    );

  [DllImport("VHACD_Lib")]
  private static extern int GetHullSize();

  [DllImport("VHACD_Lib")]
  private static extern bool GetTrianglesData(int idx, ref int tri_num, ref System.IntPtr tri_data);

  [DllImport("VHACD_Lib")]
  private static extern bool GetVerticesData(int idx, ref int vert_num, ref System.IntPtr vert_data);

  delegate void VHACD_CBFunc_Handler(double overallProgress, double stageProgress, double operationProgress, int str_sz, System.IntPtr str_buff);
  public static void VHACD_CB_Handler(double overallProgress, double stageProgress, double operationProgress, int str_sz, System.IntPtr str_buff){
    string data =Marshal.PtrToStringAnsi(str_buff);
    EditorUtility.DisplayProgressBar("V-HACD", data+" ("+(int)stageProgress+"%)", (float)overallProgress/100f);
  }

  MeshCollider[] _convex_mesh_colliders =null;
  Mesh[] _convex_mesh =null;

  public uint hulls =8;
  public uint tris_per_hull =16;
  public uint resolution =100000;

  float GetMax(Vector3 v){
    return Mathf.Max(Mathf.Max(v.x, v.y), v.z);
  }

  float GetMin(Vector3 v){
    return Mathf.Min(Mathf.Min(v.x, v.y), v.z);
  }
  
  // Use this for initialization
  void Start () {
    Debug.Log((int)(Mathf.Pow(8f, 1f/3f)+0.5f));
    Mesh m =GetComponent<MeshFilter>().sharedMesh;
    m.RecalculateBounds();
    if (GetMax(m.bounds.size)<1f || GetMin(m.bounds.size)<0.1f){
      EditorUtility.DisplayDialog("V-HACD", "Build Failed.\nThe largest dimension length is less then 1 unit, or the smallest length is less then 0.1.", "Ok");
      return;
    }

    float[] verts =new float[m.vertexCount*3];
    for (int i=0;i<m.vertexCount;++i){
      verts[i*3+0] =m.vertices[i].x;
      verts[i*3+1] =m.vertices[i].y;
      verts[i*3+2] =m.vertices[i].z;
    }

    GameObject parent =new GameObject();
    parent.name ="compound_collider";
    parent.transform.SetParent(gameObject.transform, false);

    int result_hull_num =0;
    bool success =DecomposeMeshToHulls(hulls, tris_per_hull, resolution, verts, (uint)m.vertexCount, m.triangles, (uint)(m.triangles.Length/3), VHACD_CB_Handler);
    if (success){
      result_hull_num =GetHullSize();
      _convex_mesh_colliders =new MeshCollider[result_hull_num];
      _convex_mesh =new Mesh[result_hull_num];
      for (int i=0;i<result_hull_num;++i){

        int vert_ct =0;
        System.IntPtr vert_arr_ptr =System.IntPtr.Zero;
        GetVerticesData(i, ref vert_ct, ref vert_arr_ptr);
        double[] vert_arr =new double[vert_ct*3];
        Marshal.Copy(vert_arr_ptr, vert_arr, 0, vert_ct*3);

        int tri_ct =0;
        System.IntPtr tri_arr_ptr =System.IntPtr.Zero;
        GetTrianglesData(i, ref tri_ct, ref tri_arr_ptr);
        int[] tri_arr =new int[tri_ct*3];
        Marshal.Copy(tri_arr_ptr, tri_arr, 0, tri_ct*3);
        
        List<Vector3> vert_list =new List<Vector3>();
      
        for (int j=0;j<vert_arr.Length/3;++j){
          vert_list.Add(new Vector3(
            (float)vert_arr[j*3+0],
            (float)vert_arr[j*3+1],
            (float)vert_arr[j*3+2]
          ));
        }

        Mesh tmp_m =new Mesh();
        tmp_m.name =GetComponent<MeshFilter>().sharedMesh.name+"_"+i;
        tmp_m.SetVertices(vert_list);
        tmp_m.SetTriangles(tri_arr, 0);
        tmp_m.RecalculateNormals();
        
        MeshCollider mc =parent.AddComponent<MeshCollider>();
        mc.sharedMesh =tmp_m;
        mc.convex =true;
        // mc.sharedMaterial =shared_physic_material;
        // mc.isTrigger =isTrigger;

        _convex_mesh_colliders[i] =mc;
        _convex_mesh[i] =tmp_m;
      }

    }else{
      //...

    }
    DisposeHullData();
    EditorUtility.ClearProgressBar();
  }
  
  // Update is called once per frame
  void Update () {
    
  }
}
VHACD_Lib.cs 在程式啟動時從 MeshFilter 取得 Mesh 資料,接著呼叫 DecomposeMeshToHulls 函數將這些資料轉成數個 Convex MeshCollider。其中 hulls 表示最多可產生的 Convex Mesh,tris_per_hull 表示每個 Convex Mesh 最多三角面數,resolution 表示 VHACD 在執行時解析原始 Mesh 使用的方格數量。此外要注意的是原始的 Mesh 有數值大小的限制,若是太小則在 Import 時,將 Model Meshes Scale Factor 設定更大的倍率。

以 bunny 作為例子,使用 Unity 內建 MeshCollider 產生的 ConvexCollider 與 V-HACD 計算產生的 Compound Collider 比較如下圖 :

內建工具產生的 Convex Collider

V-HACD 計算產生的 Colliders

將 V-HACD 計算產生的 Colliders 視覺化 (各別著色)


ref 1: https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
ref 2: https://en.wikipedia.org/wiki/Gilbert–Johnson–Keerthi_distance_algorithm
ref 3: https://github.com/kmammou/v-hacd
ref 4: https://forums.unrealengine.com/unreal-engine/announcements-and-releases/36410-engine-features-preview-4-02-2015

2015-12-25

Facebook BUCK with multidex

程式開發後期通常會遇見 Building Apps with over 65k methods 的問題,底下是利用 BUCK 執行編譯時遇到此問題的解決方法 (啟用 multidex 功能之設定檔的筆記) 。

BUCK :
...

# setup multidex jar
prebuilt_jar(
  name = 'support_multidex',
  binary_jar = 'com.android.support/multidex/1.0.0/jars/classes.jar',
  visibility = ['PUBLIC']
)

...

# include multidex jar
android_library(
  name = 'lib',
  srcs = glob(['src/**/*.java',...]),
  deps = [
    ':support_multidex',
    ...
  ],
  visibility = ['PUBLIC'],
)

...

# build apk with multidex
android_binary(
  name = 'release',
  manifest = 'AndroidManifest.xml',
  use_split_dex = True,
  deps = [':lib'],
  package_type='release',
  ...
)

...


AndroidManifest (在 application tag 設定 android:name 屬性) :
<?xml version="1.0" encoding="utf-8"?>
<manifest 
  ... >
  <application 
    android:name="com.mypackage.name.App"
    ...>

    ...

  </application>
</manifest>
新增 App.java :
package com.mypackage.name;
public class App extends android.support.multidex.MultiDexApplication{
}

2014-10-19

Android DexClassLoader 續

之前介紹的方法讓我們的程式可以在執行時期動態載入其它的程式碼,後來發現 Facebook 的 Buck 當中也有類似的功能 ( Exopackage ),但它實作的方法和之前介紹的方法不同,底下敘述使用 Buck 的方法,並套用在前一篇範例上的流程 :

1. 製作 dex 檔案 :
與前一篇介紹的方法相同

2. 程式執行時期下載 dex 檔案  :
建立一個 com.example.dexclassloader 之 Android Application Project 並將主要 Activity (MainActivity.java ) 替換如下 :
package com.example.dexclassloader;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
 
public class MainActivity extends Activity {
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
   
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //install dex jar
    List<File> dexJars =new ArrayList<File>();
    File libFile =new File(getFilesDir()+"/PayloadActivity.dex.jar");
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), getDir("outdex", Context.MODE_PRIVATE), dexJars);
     
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }
 
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
  
 private void downloadDexFile(String url, String filename){
     try {
 
         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
          
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();
 
         byte[] buffer = new byte[1024];
         int len1 = 0;
          
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
          
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());
 
     }
 }
  
 private void loadAndInvokeMethod(){
  try{  
         //LOAD CLASS
         Class<?> payloadClass =this.getClassLoader().loadClass("com.example.PayloadActivity");
          
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
          
         //INVOKE METHOD
         starterFunc.invoke(null, this);
          
  }catch(Exception e){
    
  }
 }
 
}
觀察得知原本 overrideClassLoader 函數的部分改為呼叫 SystemClassLoaderAdder.installDexJars 函數,此外呼叫載入的 dex 裡的類別函數方法也變得簡單。前一篇的作法是建立一個自己的 ClassLoader 去替換原本的 ClassLoader,這篇的作法是直接去修改現有的 ClassLoader 因此也比上一篇的方法少了 MyClassLoader.java 檔案。

(ps. 程式碼裡呼叫的 installDexJars 函數位於檔案 SystemClassLoaderAdder.java,可以在這裡取得)

3. MyApplication.java

完成步驟1, 2 之後程式就可以運作,但不算真正的完成。有時候啟動程式仍然會得到 java.lang.ClassNotFoundException 的錯誤訊息。

為何說是 "有時候" ,因為這跟使用者操作方式的流程有關,假若現在使用者在程式裡啟動了 Acitivty ,而這個 Activity 是屬於 dex 檔案後來載入的程式碼,啟動後在 Activity 的介面操作到一半切換到其它的 App,此時有可能因為系統記憶體不足,釋放原本在背景執行的 App 的記憶體,之後使用者欲切換到原來的 App,系統會自動重新啟動 App 並呼叫上次啟動的 Activity,此時的 ClassLoader 是系統建立的 ClassLoader 不是我們更改過的 ClassLoader 因此找不到 dex 檔案而發生 Class Not Found Exception。

幸好在系統啟動 Activity 之前我們還有機會讓系統執行我們的程式。那就是 Application 的 attachBaseContext 函數。

底下是新增加的程式 MyApplication.java :
package com.example.dexclassloader;

import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.io.File;

public class MyApplication extends Application
{
  public void installPayloadDexFile(){

    String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
    File libFile =new File(libPath);

    if (libFile.exists()==false)
      return;

    File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
    List<File> dexJars = new ArrayList<File>();
    dexJars.add(libFile);
    SystemClassLoaderAdder.installDexJars(getClassLoader(), optimizedDexOutputPath, dexJars);

  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
  }

  @Override
  protected final void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    installPayloadDexFile();
    
  }

  @Override
  public final void onCreate() {
    super.onCreate();
    installPayloadDexFile();
  }

  @Override
  public final void onTerminate() {
    super.onTerminate();

  }

  @Override
  public final void onLowMemory() {
    super.onLowMemory();

  }

  @Override
  public final void onTrimMemory(int level) {
    super.onTrimMemory(level);

  }

}

觀察可知程式在啟動 Activity 之前首先呼叫 MyApplication.attachBaseContext 然後檢查是否存在檔案 PayloadActivity.dex.jar ,若有則載入 dex 並修改目前的 ClassLoader。

4. AndroidManifest.xml

同前一篇介紹的,除了要預先定義 Activity 到 AndroidManifest.xml 之外,若要讓 Class MyApplication 生效,還須定義 MyApplication 在 application 標籤內 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />
 
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="com.example.dexclassloader.MyApplication" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
     
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 
</manifest>

ref:http://facebook.github.io/buck/article/exopackage.html
ref:http://developer.android.com/reference/android/app/Application.html
ref:http://stackoverflow.com/questions/9873669/how-do-i-catch-content-provider-initialize


2014-10-16

Android : 建立並安裝 自我簽署憑証 ( Self-Signed Certificate authority )

在安全性的需求之下我們使用 https 代替 http 連線,而 server 端開啟 https 服務需要設定相關的憑証,這些憑証要向具有公信力的機購購買,但在開發時期我們可以自己產生,根據這裡的資料,製作 X.509 version 3 的 self-signed CA certificates 可以避免大多數行動裝置無法安裝的問題,底下步驟是敘述製作以及在 Android 上安裝 self-signed CA certificates 的流程。

A. 製作 self-signed CA certificates

1. 建立一個檔案,例如 openssl.cnf,其內容如下 (openssl.cnf 檔案的重點在於 basicConstraints = CA:true 以及 alt_names) :
[req]
  distinguished_name = req_distinguished_name
  req_extensions = v3_req

[req_distinguished_name]
  countryName = Country Name (2 letter code)
  countryName_default = US
  localityName = Locality Name (eg, city)
  organizationalUnitName = Organizational Unit Name (eg, section)
  commonName = Common Name (eg, YOUR name)
  commonName_max = 64
  emailAddress = Email Address
  emailAddress_max = 40

[v3_req] 
  basicConstraints = CA:true
  keyUsage = keyEncipherment, dataEncipherment
  extendedKeyUsage = serverAuth
  subjectAltName = @alt_names

[alt_names]
  DNS.1   = *.mydomain1.com.tw
  DNS.2   = *.mydomain2.com.tw
  IP.1    = 192.168.11.1
其中 alt_names 裡的 DNS 以及 IP 就輸入這個憑証欲綁定的 FQDN 或 IP。

2. 使用底下的命令建立 key 跟 certificate :
openssl req -x509 -nodes -days 365 -newkey rsa:4096 -keyout ./privatekey.key -out ./certificate.crt -extensions v3_req -config ./openssl.cnf
建立的 privatekey.key 跟 certificate.crt 是給開啟 https 服務的伺服器使用。

B. 將 certificate.crt 轉換成 DER 格式 (供 Android 安裝使用) 

openssl x509 -in ./certificate.crt -outform der -out ./certificate_der.crt
可將 certificate_der.crt 擺在網路上,讓 Android 裝置的瀏覽器將它下載到手機中,直接點擊下載的檔案就可以開始安裝憑証。
(或者也可以擺在自己的 http server 上,將 crt 的 MIME type 設定成 application/x-x509-ca-cert 讓瀏覽器直接安裝憑証,或是將 crt 的 MIME type 設定成 application/octet-stream 讓瀏覽器把它視為檔案下載。)

ref:https://developer.android.com/training/articles/security-ssl.html
ref:http://www-01.ibm.com/support/knowledgecenter/#!/SSZH4A_6.0.0/com.ibm.worklight.help.doc/admin/c_ssl_config.html
ref:http://www.xinotes.net/notes/note/1094/
ref:http://stackoverflow.com/questions/7229361/how-to-force-mime-type-of-file-download
ref:http://davdroid.bitfire.at/faq/entry/importing-a-certificate
ref:http://www.akadia.com/services/ssh_test_certificate.html

2014-06-25

Android webview 處理 Line it intent

最近有個需求是要在 webview 上處理 LINE it 按鈕的連結,這裡做個筆記 :

1. 使用 shouldOverrideUrlLoading 攔截轉跳的 url 連結。
2. 攔截 intent:// 開頭的 url
3. 此時應該會有一個 intent 看起來像這樣 :

intent://msg/text/DUMMY_MESSAGE#Intent;scheme=line;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;package=jp.naver.line.android;end

4. 兩行搞定

Intent iuri =Intent.parseUri(url, 0);
startActivity(iuri);