2019年11月25日 星期一

運用 Viewport 修正 Vulkan 的Y軸繪圖空間

運用 Viewport 修正 Vulkan 的Y軸繪圖空間 

前言

  最近在學習 Vulkan ,但發現到 Vulkan 的Y軸繪圖空間跟 Direct3D 與 OpenGL 不一樣,幸運地,是有辦法將會Y軸繪圖空間修正回來,在此把學習的過程做個紀錄。

內容

  首先先來了解 Vulkan 的Y軸繪圖空間有何不一樣,如下圖
Y軸繪圖空間的差異

可以在圖中看到 Vulkan 的Y軸繪圖空間是從"下到上",但舊的 API 的Y軸繪圖空間卻是從"下到上",如果是從舊的 API 整合 Vulkan 到繪圖引擎就會發生繪製的結果上下顛倒的問題,信運地,在經過 google 後找到  [ Sascha Willems ] Flipping the Vulkan viewport ,裡面有提到解決的方法。

  解決的方法是透過 Viewport 來修正Y軸的繪圖空間,但要用這個方法時必須開啟"VK_KHR_maintenance1",這是一個 Device extension ,要在 VkDeviceCreateInfo 裡的啟動。在啟動"VK_KHR_maintenance1"後,就可以利用 Viewport  來修正Y軸了,如何修正呢?在 VkViewport::height 裡填上"負"的高度,這樣整個Y軸繪製空間就會變成"下到上",如果沒開啟"VK_KHR_maintenance1"填上負值會在 Debug 訊息回報該值不能是負值。填完"負"的高度得到的結果如下圖
填完"負"的高度的繪製結果

圖中 Render result 的部分是繪製的結果但這次的Y軸是"下到上",藍色的部分是最後會被寫到 Render target 的區域,所以還要將繪製的結果校正到這個區域才能達到目的,如何校正呢?在 VkViewport::y 裡加一個"正"的高度就可以達到目的。最後提供一個轉換的範例如下
void SetUpViewportAndCorrectYAxis(VkViewport& viewport,float x,float y,float width,float height,float minDepth,float maxDepth)
{
  viewport.x = x;
  viewport.y = -y + height;
  viewport.width = width;
  viewport.height = -height;
  viewport.minDepth = mixDepth;
  viewport.maxDepth = maxDepth;
}

參考資料

[ Sascha Willems ] Flipping the Vulkan viewport

2019年11月18日 星期一

開啟Vulkan的 Debug mode

開啟Vulkan的 Debug mode

前言

  最近在入門 Vulkan ,發現開啟 Debug mode 有一些麻煩,不像 Direct3D 只要設定旗標就可以開啟, Vulkan 需要比較多的步驟,在此把學習的過程做個紀錄。

內容

  在創建 VkInstance 會透過 VkInstanceCreateInfo 來輸入參數, VkInstanceCreateInfo 的定義如下
typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkInstanceCreateFlags       flags;
    const VkApplicationInfo*    pApplicationInfo;
    uint32_t                    enabledLayerCount;
    const char* const*          ppEnabledLayerNames;
    uint32_t                    enabledExtensionCount;
    const char* const*          ppEnabledExtensionNames;
} VkInstanceCreateInfo;

在 ppEnabledLayerNames 的陣列裡要加入"VK_LAYER_KHRONOS_validation",而 ppEnabledExtensionNames 的陣列要加入"VK_EXT_debug_utils",這兩個缺一不可,由於剛入門所以有踩到坑,我踩得坑是指加入"VK_EXT_debug_utils",會發生什麼事呢?我會告訴你行為不可預期, VkInstance 雖然可以正常產出,但其它的 API 可能會出問題,我發生問題狀況是 vkCreateGraphicsPipelines() 喚起時一定 Access violation 。在 pNext 的參數要設定VkDebugUtilsMessengerCreateInfoEXT ,範例如下
VkInstanceCreateInfo createInstanceInfo;
//...
//
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo;
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
debugUtilsMessengerCreateInfo.pNext = NULL;
debugUtilsMessengerCreateInfo.flags = 0;
debugUtilsMessengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
debugUtilsMessengerCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
                                            VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
                                            VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
debugUtilsMessengerCreateInfo.pfnUserCallback = myDebugCallBack;
debugUtilsMessengerCreateInfo.pUserData = NULL;
//
createInstanceInfo.pNext = &debugUtilsMessengerCreateInfo;
//
//call vkCreateInstance() to create
//...

在設定 pfnUserCallback 時需要一個 Function pointer ,可以設定成以下( 在Windows 平台)
VkBool32 myDebugCallBack(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,VkDebugUtilsMessageTypeFlagsEXT messageType,const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData,void *pUserData)
{
  char prefix[64] = "";
  char *message = (char *)malloc(strlen(pCallbackData->pMessage) + 5000);
  //
  if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) 
  {
    strcat(prefix, "VERBOSE : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) 
  {
    strcat(prefix, "INFO : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) 
  {
    strcat(prefix, "WARNING : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) 
  {
    strcat(prefix, "ERROR : ");
  }
  //
  if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) 
  {
    strcat(prefix, "GENERAL");
  } 
  else 
  {
    if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) 
    {
      strcat(prefix, "VALIDATION");  
    }
    if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) 
    {
      if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) 
      {
        strcat(prefix, "|");
      }
      strcat(prefix, "PERFORMANCE");
    }
  }
  sprintf(message, "%s - Message Id Number: %d | Message Id Name: %s\n\t%s\n", prefix, pCallbackData->messageIdNumber,
  pCallbackData->pMessageIdName, pCallbackData->pMessage);
  if (pCallbackData->objectCount > 0) 
  {
    char tmp_message[500];
    sprintf(tmp_message, "\n\tObjects - %d\n", pCallbackData->objectCount);
    strcat(message, tmp_message);
    for (uint32_t object = 0; object < pCallbackData->objectCount; ++object) 
    {
      if (NULL != pCallbackData->pObjects[object].pObjectName && strlen(pCallbackData->pObjects[object].pObjectName) > 0) 
      {
        sprintf(tmp_message, "\t\tObject[%d] - %s, Handle %p, Name \"%s\"\n", object,
                ConvertVkObjectType(pCallbackData->pObjects[object].objectType),
                (void *)(pCallbackData->pObjects[object].objectHandle), pCallbackData->pObjects[object].pObjectName);
      } 
      else 
      {
        sprintf(tmp_message, "\t\tObject[%d] - %s, Handle %p\n", object,
                ConvertVkObjectType(pCallbackData->pObjects[object].objectType),
                (void *)(pCallbackData->pObjects[object].objectHandle));
      }
      strcat(message, tmp_message);
    }
  }
  if (pCallbackData->cmdBufLabelCount > 0) 
  {
    char tmp_message[500];
    sprintf(tmp_message, "\n\tCommand Buffer Labels - %d\n", pCallbackData->cmdBufLabelCount);
    strcat(message, tmp_message);
    for (uint32_t cmd_buf_label = 0; cmd_buf_label < pCallbackData->cmdBufLabelCount; ++cmd_buf_label) 
    {
      sprintf(tmp_message, "\t\tLabel[%d] - %s { %f, %f, %f, %f}\n", cmd_buf_label,
              pCallbackData->pCmdBufLabels[cmd_buf_label].pLabelName, pCallbackData->pCmdBufLabels[cmd_buf_label].color[0],
              pCallbackData->pCmdBufLabels[cmd_buf_label].color[1], pCallbackData->pCmdBufLabels[cmd_buf_label].color[2],
              pCallbackData->pCmdBufLabels[cmd_buf_label].color[3]);
      strcat(message, tmp_message);
    }
  }

  ::OutputDebugStringA(message);
  fflush(stdout);
  //
  free(message);

  // Don't bail out, but keep going.
  return false;
}

這個 Function 是從 Vulkan SDK 的範例裡拔出來的,紅字的部分是顯示 Debug 訊息的地方,請依不同的平台需求來改造。設定 VkInstanceCreateInfo.pNext 的部分要指向 VkDebugUtilsMessengerCreateInfoEXT ,這個用法相當特別,正常的 pNext 都會是 NULL。

  在 VkInstanceCreateInfo 裡設好參數並確實的創建 VkInstance 後需要取得 Debug mode 需要的 Function ,因為是 Extension ,所以要另外取得可以理解,範例如下
fCreateDebugUtilsMessengerEXT =(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(hInstance,"vkCreateDebugUtilsMessengerEXT");
fDestroyDebugUtilsMessengerEXT =(PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(hInstance,"vkDestroyDebugUtilsMessengerEXT");
fSubmitDebugUtilsMessageEXT =(PFN_vkSubmitDebugUtilsMessageEXT)vkGetInstanceProcAddr(hInstance,"vkSubmitDebugUtilsMessageEXT");
fCmdBeginDebugUtilsLabelEXT=(PFN_vkCmdBeginDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdBeginDebugUtilsLabelEXT");
fCmdEndDebugUtilsLabelEXT=(PFN_vkCmdEndDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdEndDebugUtilsLabelEXT");
fCmdInsertDebugUtilsLabelEXT=(PFN_vkCmdInsertDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdInsertDebugUtilsLabelEXT");
fSetDebugUtilsObjectNameEXT=(PFN_vkSetDebugUtilsObjectNameEXT)vkGetInstanceProcAddr(hInstance,"vkSetDebugUtilsObjectNameEXT");

這每個都是 Function pointer ,範例沒宣告型別名稱,要型別名稱可以看式子的右側都有 cast。取得 Function pointer 後再利用 fCreateDebugUtilsMessengerEXT() 來創建 VkDebugUtilsMessengerEXT  ,範例如下
VkDebugUtilsMessengerEXT hDebugUtilsMessenger;
//Create...
fCreateDebugUtilsMessengerEXT(hInstance,&debugUtilsMessengerCreateInfo,NULL,&hDebugUtilsMessenger);

//Destroy...
fDestroyDebugUtilsMessengerEXT(hInstance,hDebugUtilsMessenger,NULL);

在創建完後就代表 Debug mode 開啟了。既然創建了 VkDebugUtilsMessengerEXT ,在程式最後要記得透過fDestroyDebugUtilsMessengerEXT() 來銷毀 VkDebugUtilsMessengerEXT 。

  最後抱怨一下, 這個啟動過程的設計我覺得非常糟糕,流程只有兩個,創建 VkInstance 與創建 VkDebugUtilsMessengerEXT ,但兩個流程的參數都需要 VkDebugUtilsMessengerCreateInfoEXT ,設定的方法還很特別,不看範例很難記得。還有在啟動"VK_LAYER_KHRONOS_validation"與"VK_EXT_debug_utils"的部分也設計得很糟,Layer 與 Extension 的啟動有相依性,錯了也不報錯。Call back 的部分我覺得明明官方就可以給預設,但就是要自己寫一個,規格還有點複雜,這部份的設計也很奇怪。

參考資料

[ 書籍 ] Vulkan 應用開發指南,出版商:人民郵電,ISBN-13:9787115506801

2019年11月11日 星期一

關於 ID3D12GraphicsCommandList::SetDescriptorHeaps()

關於 ID3D12GraphicsCommandList::SetDescriptorHeaps()

前言

  最近在研究 Direct3D12 ,手邊的資料有 Direct3D12 的範例與 [ 書籍 ] DirectX 12 3D 游戲開發實戰,出版商:人民郵電出版社,ISBN-13:9787115479211 ,本以為就入門來說應該很充足,但在實務上會碰到一些不知道在做什麼的 Function ,就像是 ID3D12GraphicsCommandList::SetDescriptorHeaps() ,這個 Function 不論是 [ MSDN ]ID3D12GraphicsCommandList::SetDescriptorHeaps method 或是手邊的資料都沒有說得很清楚,都使是在範例裡有寫這個步驟,但都不特別解釋,所以我就對這個 Funciton 做了研究,在此做個紀錄。

內容

  ID3D12GraphicsCommandList::SetDescriptorHeaps() 這個 Function 在介面上有些設計不良,
介面如下
void SetDescriptorHeaps(
  UINT                 NumDescriptorHeaps,
  ID3D12DescriptorHeap * const *ppDescriptorHeaps
);

介面看起來會讓人很直覺得可以輸入很多個 ID3D12DescriptorHeap ,但是事實上是輸入的 DescriptorHeap 是有限制的!每個 ID3D12DescriptorHeap 在 Create 的時候會需要 D3D12_DESCRIPTOR_HEAP_TYPE,定義如下
typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
{
    D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,    // Constant buffer/Shader resource/Unordered access views
    D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER,        // Samplers
    D3D12_DESCRIPTOR_HEAP_TYPE_RTV,            // Render target view
    D3D12_DESCRIPTOR_HEAP_TYPE_DSV,            // Depth stencil view
    D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES       // Simply the number of descriptor heap types
} D3D12_DESCRIPTOR_HEAP_TYPE;

這個 Type 會決定 DescriptorHeap 的型態, Create 完後不能更改,而在 ID3D12GraphicsCommandList::SetDescriptorHeaps() 輸入的每個 DescriptorHeap 必須是不同的  D3D12_DESCRIPTOR_HEAP_TYPE ,加上這個 Function 其實只處理"輸入"的部分,所以D3D12_DESCRIPTOR_HEAP_TYPE_RTV 與 D3D12_DESCRIPTOR_HEAP_TYPE_DSV 就不會用到,所以實際上最多只能輸入兩個 DescriptorHeap !這些在 MSDN 上都沒有很詳述的說明,我是在輸入同一種 Type 後會出現 Debug 的錯誤訊息才知道這個 Fuction 不能輸入不同 Type 的 DescriptorHeap 。個人認為理想的介面應該如下
void SetDescriptorHeaps(
  ID3D12DescriptorHeap * pDescriptorHeapsType_CBV_SRV_UAV;
  ID3D12DescriptorHeap * pDescriptorHeapsType_SAMPLER;
);

  接著來說一下喚起的時機,如下圖
SetDescriptorHeaps() 的喚起時機

如中上方的部分說明只能接收兩種 DescriptorHeap ,接著下方的 Draw call 是指每次的繪圖會透過 SetGraphicsRootDescriptorTable() 時要注意 GPUDescriptorHandle 必須來自 SetDescriptorHeaps() 的 DescriptorHeap ,手邊的資料也都沒明說這樣的規則,這是經過多次的實驗得來的經驗。如果需要切換 DescriptorHeap 的話,要注意切換的時機要發生在 Draw call 與 Draw call 之間,不能發生在 Draw call 的裡面,舉個例來說,如果一個 Draw call 裡需要做兩次 SetGraphicsRootDescriptorTable() ,但期望來自兩個 DescriptorHeap( D3D12_DESCRIPTOR_HEAP_TYPE 是一樣的),這個時候發生需要在同一個 Draw call 裡喚起兩次 SetDescriptorHeaps(),如以下範例
ComPtr<ID3D12DescriptorHeap> m_myHeap1;
ComPtr<ID3D12DescriptorHeap> m_myHeap2;
//Init heap1 and heap2
//...
//
ID3D12DescriptorHeap* ppHeaps[] = { m_myHeap1.Get()};
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(0,m_myHeap1->GetGPUDescriptorHandleForHeapStart() );
ppHeaps[0] = m_myHeap2.Get();
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(1,m_myHeap2->GetGPUDescriptorHandleForHeapStart() );
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);

這個做法目前不確定是否為"正規"用法,因為這個用法在 NVIDIA GeForce GTX1050 的顯卡時可以正常工作,但在 Intel HD Graphics 630 時無法正常工作,所以不確定是否可以這樣用,
說明文件也沒說明是否可這樣用,這部分目前的解決方案是絕不在 Draw call 中切換 DescriptorHeap ,這樣可以取得較好的兼容性。

參考資料

[ MSDN ]ID3D12GraphicsCommandList::SetDescriptorHeaps method
[ 書籍 ] DirectX 12 3D 游戲開發實戰,出版商:人民郵電出版社,ISBN-13:9787115479211

2019年11月4日 星期一

在 Windows 平台建置 LLVM 專案

在 Windows 平台建置 LLVM 專案

前言

  最近買了 [ 書籍 ] LLVM編譯器實戰教程,出版發行:機械工業出版社,ISBN-13:9787111631972,想要入門 LLVM,但發現 LLVM 專案在 Windows 平台建置時不是很直覺,所以在此做個紀錄。

內容

  LLVM 的專案在 Windows 平台是用 CMake 來產生專案,可以到 CMake 官網 下載。接著是關於 LLVM 的專案如何下載?可以在 LLVM 官網  下載,如下圖
LLVM 官網的下載

圖中可以看到有很多的專案,但那些專案是需要下載的呢?其實我也不知道,因為我剛入門並不熟悉,但官方有提供 Git 的位址,可以一次下載整個 LLVM 專案, Git 的位置如下
https://github.com/llvm/llvm-project.git

透過這個 Git 可以取得 LLVM 整個專案的原始碼。

  在取得原始碼專案後,接著就是用 CMake 來產生專案,在設定好 Source code 與 binaries 的位置後,請新增兩個 Entry ,如下圖
新增 CMAKE_INSTALL_PREFIX


新增 LLVM_TARGETS_TO_BUILD

CMAKE_INSTALL_PREFIX 的位置是指建置完的 .h 、 .lib 與工具的執行檔要放的位址,增加完兩個 Entry 後,就按下 Configure 設定編譯器,接著按下 Generate ,就可以產生專案如下圖
產生專案

在圖中"1"的位置會看到紅字,請不要理會它,這是正常現象,接著是圖中"2"的位置會看到一大推 not found ,這也是正常現象,小小的抱怨一下,這種建置訊息會留下"看似"有問題的訊息實在不是很欣賞的建置。

  接著開啟用 CMake 產生的方案後,建置 INSTALL 專案,如下圖
建置 INSTALL 專案

建置完後就可以在之前 CMAKE_INSTALL_PREFIX 的位置裡得到結果。

參考資料

[ 書籍 ] LLVM編譯器實戰教程,出版發行:機械工業出版社,ISBN-13:9787111631972
CMake 官網
LLVM 官網

2019年10月28日 星期一

將曲線( Curve )連起來

將曲線( Curve )連起來

前言

  續之前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) ,在該篇只是單純的繪製曲線( Curve ),如果需要彎曲變化較豐富的話,就需要增加控制點的數量,查詢資料發現貝茲曲線( ( Bezier curve )不只有二次方與三次方,可以到四次方、五次方...等,每次改變控制點數量就要換一種算式計算,實在不是個好方法,所以打算用多個三次方貝茲曲線( Cubic bezier curve )連起來達到增加控制點的方式來增加彎曲變化,在此把學習的過程做個紀錄。

內容

  雖然貝茲曲線( Bezier curve )不止到三次方,但每次增加控制點就要改變算式相當麻煩,所以採用"連"起來的方式來完成曲線,到底是怎麼連起來的呢?請看下圖
曲線( Curve )的連結

圖中是連結兩個三次方貝茲曲線( Cubic bezier curve),比較要注意的是連結後所需要控制點是7個,並不是8個,第一個曲線( Curve )的結尾會跟第二個曲線( Curve)的開頭共用同一個控制點。接著來看實作範例
HTML 的部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCurve"type="button" value="DrawCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
</body>
</html>

Javascript 的部分
let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.7,0.9,0.0],
  [-0.5,0.9,0.0],
  [-0.3,0.0,0.0],
  [-0.1,-0.6,0.0],
  [0.1,-0.6,0.0],
  [0.3,0.0,0.0],
  [0.5,0.9,0.0],
  [0.7,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
function UpdateCurveData(){
  let tagCurveData=[];
  for(let i=0;i<ctrlPointList.length;i+=3){
    if( (ctrlPointList.length - i) < 4)
      break;
    //
    let data=generateCubicBezierCurve(
      ctrlPointList[i],
      ctrlPointList[i+1],
      ctrlPointList[i+2],
      ctrlPointList[i+3],
      tagLerpValue);
    tagCurveData.push(...data);
  }
  let dataArray=generateLineListData(tagCurveData);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCurve").onclick=function(evt){
  UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  UpdateCurveData();
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}

執行結果如下
繪製控制點
繪製曲線( Curve )

這次的範例從 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 的範例更改而來,這次只說明不一樣的部分。這次的範例會"連"接三個三次方貝茲曲線( Cubic bezier curve ),所以控制點的數量改成10個,接著在 UpadteCurveData() 中,由於這次要把各個曲線( Curve )"連"起來,所以改成用迴圈的方式來產生。

參考資料

 [ Wiki ]貝茲曲線

 相關網站

  WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

2019年10月21日 星期一

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

前言

  續之前的 WebGL的繪製曲線( Curve ) ,該篇所繪製的曲線為二次貝茲曲線(Quadratic bezier curve),但常見的貝茲曲線是三次方貝茲曲線( Cubic bezier curve ),這次會製作繪製三次方貝茲曲線( Cubic bezier curve )的範例,在此做個紀錄。

內容

  二次貝茲曲線(Quadratic bezier curve)與三次方貝茲曲線( Cubic bezier curve )的差異在於前者的控制點是3個,後者的控制點是4個。為什麼說三次方貝茲曲線( Cubic bezier curve )比較常見呢?手上的繪圖軟體 Painter.NET 與 Blender 的貝茲曲線工具都是4個控制點,如下圖
Painter.NET 的曲線工具
Blender的曲線工具
接著就來看範例,如下
HTML 的部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCurve"type="button" value="DrawCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
</body>
</html>

Javascript 的部分
let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
function UpdateCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCurve").onclick=function(evt){
  UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  UpdateCurveData();
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}

執行結果如下
繪製控制點
繪製曲線


這次的範例是由 WebGL的繪製曲線( Curve ) 的範例改來,重複的部分就不再說明。這次多了generateCubicBezierCurve() ,由於控制點多了一個,所以參數多一個 p3 ,曲線的攻勢可以參考 [ Wiki ]貝茲曲線 ,裡面有對應的公式。 ctrlPointList 要多一個控制點,UpdateCurveData()換成用 generateCubicBezierCurve() 來產生資料,最後在 btnDrawCtrlPoints 的事件改了作法,上次直接使用寫3行推入陣列,這次改採使用迴圈的方法。

參考資料

[ Wiki ]貝茲曲線

相關文章

WebGL的繪製曲線( Curve )

2019年10月14日 星期一

WebGL的繪製曲線( Curve )

WebGL的繪製曲線( Curve )

前言

  續之前的 WebGL的繪製線段 ,該篇介紹了 WebGL 的基本繪製線段的方法,這次要建立一個繪製曲線( Curve )的基本繪製環境,在此做個紀錄。

內容

  這次會製作一個繪製曲線的基本環境,範例會繪製一個二次貝茲曲線,二次貝茲曲線的知識可以在 [ Wiki ]貝茲曲線 取得。範例可以在曲線( Curve )與控制點線段做切換,方便比對差異,範例如下
HTML 的部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCurve"type="button" value="DrawCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
</body>
</html>

Javascript 的部分
let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [0.0,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
function UpdateCurveData(){
  let data=generateBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  data.push(...ctrlPointList[0]);
  data.push(...ctrlPointList[1]);
  data.push(...ctrlPointList[2]);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCurve").onclick=function(evt){
  UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  UpdateCurveData();
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}

範例的的執行結果如下
執行結果

在按下 DrawCtrlPoints 的按鍵後可以看到控制點線段,如下圖
控制點線段

在按下 DarwCurve 的按鍵後可以看到曲線( Curve ),如下圖
曲線( Curve )

 DarwCurve 按鍵有個可以調的數值,這個數值可以控制線段的精細度,各種精細度的比對如下
左上精細度為10,右上的精細度為8,左下的精細度為5,右下的精細度為3,可以看到精細度兌現對的影響程度

這次的範例是從 WebGL的繪製線段 的範例改來,所以只解說改的地方,在createDynamicBuffer() 的地方和上次不一樣,這次是用 size 來絕決定 Vertex buffer 的大小,主要是要給一個不容易爆的值,3000可以塞1000個點,如果不夠可以再改大。generateLineListData() 可以將頂點的資料轉成 WebGL 的 drawArrays() 所需要的頂點排列,其實內容很簡單,除了第一個頂點與最後一個頂點外都要複製一次。generateBezierCurve() 是負責產生二次貝茲曲線,輸入3個頂點與精細度後產生頂點,如果對二次貝茲曲線不熟可以參考
 [ Wiki ]貝茲曲線  。剩下就是因應這次的需求所增加的UI事件,由於期望每次改變精細度的時候能馬上反應,所以刻意留一個 updateCurveData() 可以馬上反應資料到畫面。

  本次的範例只是做為一個實驗場,由於對曲線( Curve )不是很熟,需要實驗場來輔助理解曲線( Curve ),下次會嘗試繪製三次貝茲曲線。

參考資料

[ Wiki ]貝茲曲線

相關文章 

WebGL的繪製線段