mirror of
https://github.com/thorvg/thorvg.git
synced 2025-06-08 05:33:36 +00:00
Added ability to draw solid strokes
[issues 1479: Shape](https://github.com/thorvg/thorvg/issues/1479) In order to build you need third party libraries. Before you start please read this: [LearnWebGPU](https://eliemichel.github.io/LearnWebGPU/getting-started/hello-webgpu.html) Usage example: // init glfw glfwInit(); // create a windowed mode window and its opengl context glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); GLFWwindow* window = glfwCreateWindow(800, 800, "WebGPU base app", nullptr, nullptr); // get window size int width{}, height{}; glfwGetWindowSize(window, &width, &height); // init engine webgpu tvg::Initializer::init(tvg::CanvasEngine::Wg, 0); // create wg canvas auto canvasWg = tvg::WgCanvas::gen(); canvas_wg->target(glfwGetWin32Window(window), width, height); //Test for Stroke Dash for Arc, Circle, Rect auto shape = tvg::Shape::gen(); shape->appendArc(70, 600, 160, 10, 30, true); shape->appendCircle(70, 700, 20, 60); shape->appendRect(130, 710, 100, 40); shape->strokeFill(255, 0, 0); shape->strokeWidth(5); shape->strokeJoin(tvg::StrokeJoin::Round); shape->strokeCap(tvg::StrokeCap::Round); if (canvas_wg->push(std::move(shape)) != tvg::Result::Success) return; while (!glfwWindowShouldClose(window)) { // webgpu canvas_wg->draw(); canvas_wg->sync(); // pull events glfwPollEvents(); } // terminate engine and window tvg::Initializer::term(tvg::CanvasEngine::Wg); glfwDestroyWindow(window); glfwTerminate();
This commit is contained in:
parent
2f8c920654
commit
a3adbef2c2
15 changed files with 825 additions and 147 deletions
|
@ -1,9 +1,11 @@
|
|||
source_file = [
|
||||
'tvgWgGeometry.cpp',
|
||||
'tvgWgPipelineBase.cpp',
|
||||
'tvgWgPipelineEmpty.cpp',
|
||||
'tvgWgPipelineLinear.cpp',
|
||||
'tvgWgPipelineSolid.cpp',
|
||||
'tvgWgPipelineRadial.cpp',
|
||||
'tvgWgPipelineStroke.cpp',
|
||||
'tvgWgRenderData.cpp',
|
||||
'tvgWgRenderer.cpp',
|
||||
'tvgWgShaderSrc.cpp'
|
||||
|
|
85
src/renderer/wg_engine/tvgWgGeometry.cpp
Normal file
85
src/renderer/wg_engine/tvgWgGeometry.cpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2023 the ThorVG project. All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "tvgWgGeometry.h"
|
||||
|
||||
void WgVertexList::computeTriFansIndexes() {
|
||||
assert(mVertexList.count > 2);
|
||||
mIndexList.reserve((mVertexList.count - 2) * 3);
|
||||
for (size_t i = 0; i < mVertexList.count - 2; i++) {
|
||||
mIndexList.push(0);
|
||||
mIndexList.push(i + 1);
|
||||
mIndexList.push(i + 2);
|
||||
}
|
||||
}
|
||||
|
||||
void WgVertexList::appendCubic(WgPoint p1, WgPoint p2, WgPoint p3) {
|
||||
WgPoint p0 = mVertexList.count > 0 ? mVertexList.last() : WgPoint(0.0f, 0.0f);
|
||||
const size_t segs = 16;
|
||||
for (size_t i = 1; i <= segs; i++) {
|
||||
float t = i / (float)segs;
|
||||
// get cubic spline interpolation coefficients
|
||||
float t0 = 1 * (1.0f - t) * (1.0f - t) * (1.0f - t);
|
||||
float t1 = 3 * (1.0f - t) * (1.0f - t) * t;
|
||||
float t2 = 3 * (1.0f - t) * t * t;
|
||||
float t3 = 1 * t * t * t;
|
||||
mVertexList.push(p0 * t0 + p1 * t1 + p2 * t2 + p3 * t3);
|
||||
}
|
||||
}
|
||||
|
||||
void WgVertexList::appendRect(WgPoint p0, WgPoint p1, WgPoint p2, WgPoint p3) {
|
||||
uint32_t index = mVertexList.count;
|
||||
mVertexList.push(p0); // +0
|
||||
mVertexList.push(p1); // +1
|
||||
mVertexList.push(p2); // +2
|
||||
mVertexList.push(p3); // +3
|
||||
mIndexList.push(index + 0);
|
||||
mIndexList.push(index + 1);
|
||||
mIndexList.push(index + 2);
|
||||
mIndexList.push(index + 1);
|
||||
mIndexList.push(index + 3);
|
||||
mIndexList.push(index + 2);
|
||||
}
|
||||
|
||||
// TODO: optimize vertex and index count
|
||||
void WgVertexList::appendCircle(WgPoint center, float radius) {
|
||||
uint32_t index = mVertexList.count;
|
||||
uint32_t nSegments = 32;
|
||||
for (uint32_t i = 0; i < nSegments; i++) {
|
||||
float angle0 = (float)(i + 0) / nSegments * 3.141593f * 2.0f;
|
||||
float angle1 = (float)(i + 1) / nSegments * 3.141593f * 2.0f;
|
||||
WgPoint p0 = center + WgPoint(sin(angle0) * radius, cos(angle0) * radius);
|
||||
WgPoint p1 = center + WgPoint(sin(angle1) * radius, cos(angle1) * radius);
|
||||
mVertexList.push(center); // +0
|
||||
mVertexList.push(p0); // +1
|
||||
mVertexList.push(p1); // +2
|
||||
mIndexList.push(index + 0);
|
||||
mIndexList.push(index + 1);
|
||||
mIndexList.push(index + 2);
|
||||
index += 3;
|
||||
}
|
||||
}
|
||||
|
||||
void WgVertexList::close() {
|
||||
if (mVertexList.count > 1)
|
||||
mVertexList.push(mVertexList[0]);
|
||||
}
|
88
src/renderer/wg_engine/tvgWgGeometry.h
Normal file
88
src/renderer/wg_engine/tvgWgGeometry.h
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2023 the ThorVG project. All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _TVG_WG_GEOMETRY_H_
|
||||
#define _TVG_WG_GEOMETRY_H_
|
||||
|
||||
#include <cassert>
|
||||
#include "tvgMath.h"
|
||||
#include "tvgCommon.h"
|
||||
#include "tvgArray.h"
|
||||
|
||||
class WgPoint
|
||||
{
|
||||
public:
|
||||
float x;
|
||||
float y;
|
||||
|
||||
public:
|
||||
WgPoint() {}
|
||||
WgPoint(float x, float y): x(x), y(y) {}
|
||||
WgPoint(const Point& p): x(p.x), y(p.y) {}
|
||||
|
||||
WgPoint operator + (const WgPoint& p) const { return { x + p.x, y + p.y }; }
|
||||
WgPoint operator - (const WgPoint& p) const { return { x - p.x, y - p.y }; }
|
||||
WgPoint operator * (const WgPoint& p) const { return { x * p.x, y * p.y }; }
|
||||
WgPoint operator / (const WgPoint& p) const { return { x / p.x, y / p.y }; }
|
||||
|
||||
WgPoint operator + (const float c) const { return { x + c, y + c }; }
|
||||
WgPoint operator - (const float c) const { return { x - c, y - c }; }
|
||||
WgPoint operator * (const float c) const { return { x * c, y * c }; }
|
||||
WgPoint operator / (const float c) const { return { x / c, y / c }; }
|
||||
|
||||
WgPoint negative() const { return {-x, -y}; }
|
||||
void negate() { x = -x; y = -y; }
|
||||
float length() const { return sqrt(x*x + y*y); }
|
||||
|
||||
float dot(const WgPoint& p) const { return x * p.x + y * p.y; }
|
||||
float dist(const WgPoint& p) const {
|
||||
return sqrt(
|
||||
(p.x - x)*(p.x - x) +
|
||||
(p.y - y)*(p.y - y)
|
||||
);
|
||||
}
|
||||
|
||||
void normalize() {
|
||||
float rlen = 1.0f / length();
|
||||
x *= rlen;
|
||||
y *= rlen;
|
||||
}
|
||||
|
||||
WgPoint normal() const {
|
||||
float rlen = 1.0f / length();
|
||||
return { x * rlen, y * rlen };
|
||||
}
|
||||
};
|
||||
|
||||
class WgVertexList {
|
||||
public:
|
||||
Array<WgPoint> mVertexList;
|
||||
Array<uint32_t> mIndexList;
|
||||
|
||||
void computeTriFansIndexes();
|
||||
void appendCubic(WgPoint p1, WgPoint p2, WgPoint p3);
|
||||
void appendRect(WgPoint p0, WgPoint p1, WgPoint p2, WgPoint p3);
|
||||
void appendCircle(WgPoint center, float radius);
|
||||
void close();
|
||||
};
|
||||
|
||||
#endif // _TVG_WG_GEOMETRY_H_
|
|
@ -161,11 +161,11 @@ void WgPipelineEmpty::initialize(WGPUDevice device) {
|
|||
|
||||
// vertex attributes
|
||||
WGPUVertexAttribute vertexAttributes[] = {
|
||||
{ WGPUVertexFormat_Float32x3, sizeof(float) * 0, 0 }, // position
|
||||
{ WGPUVertexFormat_Float32x2, sizeof(float) * 0, 0 }, // position
|
||||
};
|
||||
// vertex buffer layout
|
||||
WGPUVertexBufferLayout vertexBufferLayout{};
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 3; // position
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 2; // position
|
||||
vertexBufferLayout.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vertexBufferLayout.attributeCount = 1; // position
|
||||
vertexBufferLayout.attributes = vertexAttributes;
|
||||
|
|
|
@ -223,11 +223,11 @@ void WgPipelineLinear::initialize(WGPUDevice device) {
|
|||
|
||||
// vertex attributes
|
||||
WGPUVertexAttribute vertexAttributes[] = {
|
||||
{ WGPUVertexFormat_Float32x3, sizeof(float) * 0, 0 }, // position
|
||||
{ WGPUVertexFormat_Float32x2, sizeof(float) * 0, 0 }, // position
|
||||
};
|
||||
// vertex buffer layout
|
||||
WGPUVertexBufferLayout vertexBufferLayout{};
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 3; // position
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 2; // position
|
||||
vertexBufferLayout.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vertexBufferLayout.attributeCount = 1; // position
|
||||
vertexBufferLayout.attributes = vertexAttributes;
|
||||
|
|
|
@ -221,11 +221,11 @@ void WgPipelineRadial::initialize(WGPUDevice device) {
|
|||
|
||||
// vertex attributes
|
||||
WGPUVertexAttribute vertexAttributes[] = {
|
||||
{ WGPUVertexFormat_Float32x3, sizeof(float) * 0, 0 }, // position
|
||||
{ WGPUVertexFormat_Float32x2, sizeof(float) * 0, 0 }, // position
|
||||
};
|
||||
// vertex buffer layout
|
||||
WGPUVertexBufferLayout vertexBufferLayout{};
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 3; // position
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 2; // position
|
||||
vertexBufferLayout.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vertexBufferLayout.attributeCount = 1; // position
|
||||
vertexBufferLayout.attributes = vertexAttributes;
|
||||
|
|
|
@ -27,11 +27,11 @@
|
|||
// WgPipelineDataSolid
|
||||
//************************************************************************
|
||||
|
||||
void WgPipelineDataSolid::updateColor(const RenderShape& renderShape) {
|
||||
uColorInfo.color[0] = renderShape.color[0] / 255.0f; // red
|
||||
uColorInfo.color[1] = renderShape.color[1] / 255.0f; // green
|
||||
uColorInfo.color[2] = renderShape.color[2] / 255.0f; // blue
|
||||
uColorInfo.color[3] = renderShape.color[3] / 255.0f; // alpha
|
||||
void WgPipelineDataSolid::updateColor(const uint8_t* color) {
|
||||
uColorInfo.color[0] = color[0] / 255.0f; // red
|
||||
uColorInfo.color[1] = color[1] / 255.0f; // green
|
||||
uColorInfo.color[2] = color[2] / 255.0f; // blue
|
||||
uColorInfo.color[3] = color[3] / 255.0f; // alpha
|
||||
}
|
||||
|
||||
//************************************************************************
|
||||
|
@ -207,11 +207,11 @@ void WgPipelineSolid::initialize(WGPUDevice device) {
|
|||
|
||||
// vertex attributes
|
||||
WGPUVertexAttribute vertexAttributes[] = {
|
||||
{ WGPUVertexFormat_Float32x3, sizeof(float) * 0, 0 }, // position
|
||||
{ WGPUVertexFormat_Float32x2, sizeof(float) * 0, 0 }, // position
|
||||
};
|
||||
// vertex buffer layout
|
||||
WGPUVertexBufferLayout vertexBufferLayout{};
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 3; // position
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 2; // position
|
||||
vertexBufferLayout.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vertexBufferLayout.attributeCount = 1; // position
|
||||
vertexBufferLayout.attributes = vertexAttributes;
|
||||
|
|
|
@ -34,7 +34,7 @@ struct WgPipelineSolidColorInfo {
|
|||
struct WgPipelineDataSolid: WgPipelineData {
|
||||
WgPipelineSolidColorInfo uColorInfo{}; // @binding(1)
|
||||
|
||||
void updateColor(const RenderShape& renderShape);
|
||||
void updateColor(const uint8_t* color);
|
||||
};
|
||||
|
||||
class WgPipelineBindGroupSolid: public WgPipelineBindGroup {
|
||||
|
|
243
src/renderer/wg_engine/tvgWgPipelineStroke.cpp
Normal file
243
src/renderer/wg_engine/tvgWgPipelineStroke.cpp
Normal file
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Copyright (c) 2023 the ThorVG project. All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "tvgWgPipelineStroke.h"
|
||||
#include "tvgWgShaderSrc.h"
|
||||
|
||||
//************************************************************************
|
||||
// WgPipelineBindGroupStroke
|
||||
//************************************************************************
|
||||
|
||||
void WgPipelineBindGroupStroke::initialize(WGPUDevice device, WgPipelineStroke& pipelinePipelineStroke) {
|
||||
// buffer uniform uMatrix
|
||||
WGPUBufferDescriptor bufferUniformDesc_uMatrix{};
|
||||
bufferUniformDesc_uMatrix.nextInChain = nullptr;
|
||||
bufferUniformDesc_uMatrix.label = "Buffer uniform pipeline Stroke uMatrix";
|
||||
bufferUniformDesc_uMatrix.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
|
||||
bufferUniformDesc_uMatrix.size = sizeof(WgPipelineMatrix);
|
||||
bufferUniformDesc_uMatrix.mappedAtCreation = false;
|
||||
uBufferMatrix = wgpuDeviceCreateBuffer(device, &bufferUniformDesc_uMatrix);
|
||||
assert(uBufferMatrix);
|
||||
|
||||
// bind group entry @binding(0) uMatrix
|
||||
WGPUBindGroupEntry bindGroupEntry_uMatrix{};
|
||||
bindGroupEntry_uMatrix.nextInChain = nullptr;
|
||||
bindGroupEntry_uMatrix.binding = 0;
|
||||
bindGroupEntry_uMatrix.buffer = uBufferMatrix;
|
||||
bindGroupEntry_uMatrix.offset = 0;
|
||||
bindGroupEntry_uMatrix.size = sizeof(WgPipelineMatrix);
|
||||
bindGroupEntry_uMatrix.sampler = nullptr;
|
||||
bindGroupEntry_uMatrix.textureView = nullptr;
|
||||
// bind group entries
|
||||
WGPUBindGroupEntry bindGroupEntries[] {
|
||||
bindGroupEntry_uMatrix // @binding(0) uMatrix
|
||||
};
|
||||
// bind group descriptor
|
||||
WGPUBindGroupDescriptor bindGroupDescPipeline{};
|
||||
bindGroupDescPipeline.nextInChain = nullptr;
|
||||
bindGroupDescPipeline.label = "The binding group pipeline stroke";
|
||||
bindGroupDescPipeline.layout = pipelinePipelineStroke.mBindGroupLayout;
|
||||
bindGroupDescPipeline.entryCount = 1;
|
||||
bindGroupDescPipeline.entries = bindGroupEntries;
|
||||
mBindGroup = wgpuDeviceCreateBindGroup(device, &bindGroupDescPipeline);
|
||||
assert(mBindGroup);
|
||||
}
|
||||
|
||||
void WgPipelineBindGroupStroke::release() {
|
||||
if (uBufferMatrix) {
|
||||
wgpuBufferDestroy(uBufferMatrix);
|
||||
wgpuBufferRelease(uBufferMatrix);
|
||||
uBufferMatrix = nullptr;
|
||||
}
|
||||
if (mBindGroup) {
|
||||
wgpuBindGroupRelease(mBindGroup);
|
||||
mBindGroup = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void WgPipelineBindGroupStroke::update(WGPUQueue queue, WgPipelineDataStroke& pipelineDataStroke) {
|
||||
wgpuQueueWriteBuffer(queue, uBufferMatrix, 0, &pipelineDataStroke.uMatrix, sizeof(pipelineDataStroke.uMatrix));
|
||||
}
|
||||
|
||||
//************************************************************************
|
||||
// WgPipelineStroke
|
||||
//************************************************************************
|
||||
|
||||
void WgPipelineStroke::initialize(WGPUDevice device) {
|
||||
// bind group layout group 0
|
||||
// bind group layout descriptor @group(0) @binding(0) uMatrix
|
||||
WGPUBindGroupLayoutEntry bindGroupLayoutEntry_uMatrix{};
|
||||
bindGroupLayoutEntry_uMatrix.nextInChain = nullptr;
|
||||
bindGroupLayoutEntry_uMatrix.binding = 0;
|
||||
bindGroupLayoutEntry_uMatrix.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
bindGroupLayoutEntry_uMatrix.buffer.nextInChain = nullptr;
|
||||
bindGroupLayoutEntry_uMatrix.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
bindGroupLayoutEntry_uMatrix.buffer.hasDynamicOffset = false;
|
||||
bindGroupLayoutEntry_uMatrix.buffer.minBindingSize = 0;
|
||||
// bind group layout entries @group(0)
|
||||
WGPUBindGroupLayoutEntry bindGroupLayoutEntries[] {
|
||||
bindGroupLayoutEntry_uMatrix
|
||||
};
|
||||
// bind group layout descriptor scene @group(0)
|
||||
WGPUBindGroupLayoutDescriptor bindGroupLayoutDesc{};
|
||||
bindGroupLayoutDesc.nextInChain = nullptr;
|
||||
bindGroupLayoutDesc.label = "Bind group layout pipeline stroke";
|
||||
bindGroupLayoutDesc.entryCount = 1;
|
||||
bindGroupLayoutDesc.entries = bindGroupLayoutEntries; // @binding
|
||||
mBindGroupLayout = wgpuDeviceCreateBindGroupLayout(device, &bindGroupLayoutDesc);
|
||||
assert(mBindGroupLayout);
|
||||
|
||||
// pipeline layout
|
||||
|
||||
// bind group layout descriptors
|
||||
WGPUBindGroupLayout mBindGroupLayouts[] {
|
||||
mBindGroupLayout
|
||||
};
|
||||
// pipeline layout descriptor
|
||||
WGPUPipelineLayoutDescriptor pipelineLayoutDesc{};
|
||||
pipelineLayoutDesc.nextInChain = nullptr;
|
||||
pipelineLayoutDesc.label = "Pipeline pipeline layout stroke";
|
||||
pipelineLayoutDesc.bindGroupLayoutCount = 1;
|
||||
pipelineLayoutDesc.bindGroupLayouts = mBindGroupLayouts;
|
||||
mPipelineLayout = wgpuDeviceCreatePipelineLayout(device, &pipelineLayoutDesc);
|
||||
assert(mPipelineLayout);
|
||||
|
||||
// depth stencil state
|
||||
WGPUDepthStencilState depthStencilState{};
|
||||
depthStencilState.nextInChain = nullptr;
|
||||
depthStencilState.format = WGPUTextureFormat_Stencil8;
|
||||
depthStencilState.depthWriteEnabled = false;
|
||||
depthStencilState.depthCompare = WGPUCompareFunction_Always;
|
||||
// depthStencilState.stencilFront
|
||||
depthStencilState.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depthStencilState.stencilFront.failOp = WGPUStencilOperation_Replace;
|
||||
depthStencilState.stencilFront.depthFailOp = WGPUStencilOperation_Replace;
|
||||
depthStencilState.stencilFront.passOp = WGPUStencilOperation_Replace;
|
||||
// depthStencilState.stencilBack
|
||||
depthStencilState.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
depthStencilState.stencilBack.failOp = WGPUStencilOperation_Replace;
|
||||
depthStencilState.stencilBack.depthFailOp = WGPUStencilOperation_Replace;
|
||||
depthStencilState.stencilBack.passOp = WGPUStencilOperation_Replace;
|
||||
// stencil mask
|
||||
depthStencilState.stencilReadMask = 0xFFFFFFFF;
|
||||
depthStencilState.stencilWriteMask = 0xFFFFFFFF;
|
||||
// depth bias
|
||||
depthStencilState.depthBias = 0;
|
||||
depthStencilState.depthBiasSlopeScale = 0.0f;
|
||||
depthStencilState.depthBiasClamp = 0.0f;
|
||||
|
||||
// shader module wgsl descriptor
|
||||
WGPUShaderModuleWGSLDescriptor shaderModuleWGSLDesc{};
|
||||
shaderModuleWGSLDesc.chain.next = nullptr;
|
||||
shaderModuleWGSLDesc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
|
||||
shaderModuleWGSLDesc.code = cShaderSource_PipelineEmpty;
|
||||
// shader module descriptor
|
||||
WGPUShaderModuleDescriptor shaderModuleDesc{};
|
||||
shaderModuleDesc.nextInChain = &shaderModuleWGSLDesc.chain;
|
||||
shaderModuleDesc.label = "The shader module pipeline Stroke";
|
||||
shaderModuleDesc.hintCount = 0;
|
||||
shaderModuleDesc.hints = nullptr;
|
||||
mShaderModule = wgpuDeviceCreateShaderModule(device, &shaderModuleDesc);
|
||||
assert(mShaderModule);
|
||||
|
||||
// vertex attributes
|
||||
WGPUVertexAttribute vertexAttributes[] = {
|
||||
{ WGPUVertexFormat_Float32x2, sizeof(float) * 0, 0 }, // position
|
||||
};
|
||||
// vertex buffer layout
|
||||
WGPUVertexBufferLayout vertexBufferLayout{};
|
||||
vertexBufferLayout.arrayStride = sizeof(float) * 2; // position
|
||||
vertexBufferLayout.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vertexBufferLayout.attributeCount = 1; // position
|
||||
vertexBufferLayout.attributes = vertexAttributes;
|
||||
|
||||
// blend state
|
||||
WGPUBlendState blendState{};
|
||||
// blendState.color
|
||||
blendState.color.operation = WGPUBlendOperation_Add;
|
||||
blendState.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blendState.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
// blendState.alpha
|
||||
blendState.alpha.operation = WGPUBlendOperation_Add;
|
||||
blendState.alpha.srcFactor = WGPUBlendFactor_Zero;
|
||||
blendState.alpha.dstFactor = WGPUBlendFactor_One;
|
||||
|
||||
// color target state (WGPUTextureFormat_BGRA8UnormSrgb)
|
||||
WGPUColorTargetState colorTargetState0{};
|
||||
colorTargetState0.nextInChain = nullptr;
|
||||
colorTargetState0.format = WGPUTextureFormat_BGRA8Unorm;
|
||||
//colorTargetState0.format = WGPUTextureFormat_BGRA8UnormSrgb;
|
||||
colorTargetState0.blend = &blendState;
|
||||
colorTargetState0.writeMask = WGPUColorWriteMask_All;
|
||||
// color target states
|
||||
WGPUColorTargetState colorTargetStates[] = {
|
||||
colorTargetState0
|
||||
};
|
||||
// fragmanet state
|
||||
WGPUFragmentState fragmentState{};
|
||||
fragmentState.nextInChain = nullptr;
|
||||
fragmentState.module = mShaderModule;
|
||||
fragmentState.entryPoint = "fs_main";
|
||||
fragmentState.constantCount = 0;
|
||||
fragmentState.constants = nullptr;
|
||||
fragmentState.targetCount = 1;
|
||||
fragmentState.targets = colorTargetStates; // render target index
|
||||
|
||||
// render pipeline descriptor
|
||||
WGPURenderPipelineDescriptor renderPipelineDesc{};
|
||||
renderPipelineDesc.nextInChain = nullptr;
|
||||
renderPipelineDesc.label = "Render pipeline pipeline stroke";
|
||||
// renderPipelineDesc.layout
|
||||
renderPipelineDesc.layout = mPipelineLayout;
|
||||
// renderPipelineDesc.vertex
|
||||
renderPipelineDesc.vertex.nextInChain = nullptr;
|
||||
renderPipelineDesc.vertex.module = mShaderModule;
|
||||
renderPipelineDesc.vertex.entryPoint = "vs_main";
|
||||
renderPipelineDesc.vertex.constantCount = 0;
|
||||
renderPipelineDesc.vertex.constants = nullptr;
|
||||
renderPipelineDesc.vertex.bufferCount = 1;
|
||||
renderPipelineDesc.vertex.buffers = &vertexBufferLayout; // buffer index
|
||||
// renderPipelineDesc.primitive
|
||||
renderPipelineDesc.primitive.nextInChain = nullptr;
|
||||
renderPipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
renderPipelineDesc.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
|
||||
renderPipelineDesc.primitive.frontFace = WGPUFrontFace_CCW;
|
||||
renderPipelineDesc.primitive.cullMode = WGPUCullMode_None;
|
||||
// renderPipelineDesc.depthStencil
|
||||
renderPipelineDesc.depthStencil = &depthStencilState;
|
||||
// renderPipelineDesc.multisample
|
||||
renderPipelineDesc.multisample.nextInChain = nullptr;
|
||||
renderPipelineDesc.multisample.count = 1;
|
||||
renderPipelineDesc.multisample.mask = 0xFFFFFFFF;
|
||||
renderPipelineDesc.multisample.alphaToCoverageEnabled = false;
|
||||
// renderPipelineDesc.fragment
|
||||
renderPipelineDesc.fragment = &fragmentState;
|
||||
mRenderPipeline = wgpuDeviceCreateRenderPipeline(device, &renderPipelineDesc);
|
||||
assert(mRenderPipeline);
|
||||
}
|
||||
|
||||
void WgPipelineStroke::release() {
|
||||
wgpuRenderPipelineRelease(mRenderPipeline);
|
||||
wgpuShaderModuleRelease(mShaderModule);
|
||||
wgpuPipelineLayoutRelease(mPipelineLayout);
|
||||
wgpuBindGroupLayoutRelease(mBindGroupLayout);
|
||||
}
|
46
src/renderer/wg_engine/tvgWgPipelineStroke.h
Normal file
46
src/renderer/wg_engine/tvgWgPipelineStroke.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2023 the ThorVG project. All rights reserved.
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _TVG_WG_PIPELINE_STROKE_H_
|
||||
#define _TVG_WG_PIPELINE_STROKE_H_
|
||||
|
||||
#include "tvgWgPipelineBase.h"
|
||||
|
||||
class WgPipelineStroke;
|
||||
|
||||
struct WgPipelineDataStroke: WgPipelineData {};
|
||||
|
||||
class WgPipelineBindGroupStroke: public WgPipelineBindGroup {
|
||||
public:
|
||||
void initialize(WGPUDevice device, WgPipelineStroke& pipelinePipelineStroke);
|
||||
void release();
|
||||
|
||||
void update(WGPUQueue mQueue, WgPipelineDataStroke& pipelineDataSolid);
|
||||
};
|
||||
|
||||
class WgPipelineStroke: public WgPipelineBase {
|
||||
public:
|
||||
void initialize(WGPUDevice device) override;
|
||||
void release() override;
|
||||
};
|
||||
|
||||
#endif // _TVG_WG_PIPELINE_STROKE_H_
|
|
@ -27,11 +27,19 @@
|
|||
//***********************************************************************
|
||||
|
||||
void WgGeometryData::draw(WGPURenderPassEncoder renderPassEncoder) {
|
||||
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 0, mBufferVertex, 0, mVertexCount * sizeof(float) * 3);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 0, mBufferVertex, 0, mVertexCount * sizeof(float) * 2);
|
||||
wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder, mBufferIndex, WGPUIndexFormat_Uint32, 0, mIndexCount * sizeof(uint32_t));
|
||||
wgpuRenderPassEncoderDrawIndexed(renderPassEncoder, mIndexCount, 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
void WgGeometryData::update(WGPUDevice device, WGPUQueue queue, WgVertexList* vertexList) {
|
||||
update(device, queue,
|
||||
(float *)vertexList->mVertexList.data,
|
||||
vertexList->mVertexList.count,
|
||||
vertexList->mIndexList.data,
|
||||
vertexList->mIndexList.count);
|
||||
}
|
||||
|
||||
void WgGeometryData::update(WGPUDevice device, WGPUQueue queue, float* vertexData, size_t vertexCount, uint32_t* indexData, size_t indexCount) {
|
||||
release();
|
||||
|
||||
|
@ -40,11 +48,11 @@ void WgGeometryData::update(WGPUDevice device, WGPUQueue queue, float* vertexDat
|
|||
bufferVertexDesc.nextInChain = nullptr;
|
||||
bufferVertexDesc.label = "Buffer vertex geometry data";
|
||||
bufferVertexDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
|
||||
bufferVertexDesc.size = sizeof(float) * vertexCount * 3; // x, y, z
|
||||
bufferVertexDesc.size = sizeof(float) * vertexCount * 2; // x, y
|
||||
bufferVertexDesc.mappedAtCreation = false;
|
||||
mBufferVertex = wgpuDeviceCreateBuffer(device, &bufferVertexDesc);
|
||||
assert(mBufferVertex);
|
||||
wgpuQueueWriteBuffer(queue, mBufferVertex, 0, vertexData, sizeof(float) * vertexCount * 3);
|
||||
wgpuQueueWriteBuffer(queue, mBufferVertex, 0, vertexData, vertexCount * sizeof(float) * 2);
|
||||
mVertexCount = vertexCount;
|
||||
// buffer index data create and write
|
||||
WGPUBufferDescriptor bufferIndexDesc{};
|
||||
|
@ -55,7 +63,7 @@ void WgGeometryData::update(WGPUDevice device, WGPUQueue queue, float* vertexDat
|
|||
bufferIndexDesc.mappedAtCreation = false;
|
||||
mBufferIndex = wgpuDeviceCreateBuffer(device, &bufferIndexDesc);
|
||||
assert(mBufferIndex);
|
||||
wgpuQueueWriteBuffer(queue, mBufferIndex, 0, indexData, sizeof(uint32_t) * indexCount);
|
||||
wgpuQueueWriteBuffer(queue, mBufferIndex, 0, indexData, indexCount * sizeof(uint32_t));
|
||||
mIndexCount = indexCount;
|
||||
}
|
||||
|
||||
|
@ -75,104 +83,280 @@ void WgGeometryData::release() {
|
|||
}
|
||||
|
||||
//***********************************************************************
|
||||
// WgRenderDataShape
|
||||
// WgRenderDataShapeSettings
|
||||
//***********************************************************************
|
||||
|
||||
void WgRenderDataShape::release() {
|
||||
releaseRenderData();
|
||||
void WgRenderDataShapeSettings::update(WGPUQueue queue, const Fill* fill, const RenderUpdateFlag flags,
|
||||
const RenderTransform* transform, const float* viewMatrix, const uint8_t* color,
|
||||
WgPipelineLinear& linear, WgPipelineRadial& radial, WgPipelineSolid& solid)
|
||||
|
||||
{
|
||||
// setup fill properties
|
||||
if ((flags & (RenderUpdateFlag::Gradient | RenderUpdateFlag::Transform)) && fill) {
|
||||
// setup linear fill properties
|
||||
if (fill->identifier() == TVG_CLASS_ID_LINEAR) {
|
||||
WgPipelineDataLinear brushDataLinear{};
|
||||
brushDataLinear.updateMatrix(viewMatrix, transform);
|
||||
brushDataLinear.updateGradient((LinearGradient*)fill);
|
||||
mPipelineBindGroupLinear.update(queue, brushDataLinear);
|
||||
|
||||
mPipelineBindGroup = &mPipelineBindGroupLinear;
|
||||
mPipelineBase = &linear;
|
||||
} // setup radial fill properties
|
||||
else if (fill->identifier() == TVG_CLASS_ID_RADIAL) {
|
||||
WgPipelineDataRadial brushDataRadial{};
|
||||
brushDataRadial.updateMatrix(viewMatrix, transform);
|
||||
brushDataRadial.updateGradient((RadialGradient*)fill);
|
||||
mPipelineBindGroupRadial.update(queue, brushDataRadial);
|
||||
|
||||
mPipelineBindGroup = &mPipelineBindGroupRadial;
|
||||
mPipelineBase = &radial;
|
||||
}
|
||||
} // setup solid fill properties
|
||||
else if ((flags & (RenderUpdateFlag::Color | RenderUpdateFlag::Transform)) && !fill) {
|
||||
WgPipelineDataSolid pipelineDataSolid{};
|
||||
pipelineDataSolid.updateMatrix(viewMatrix, transform);
|
||||
pipelineDataSolid.updateColor(color);
|
||||
mPipelineBindGroupSolid.update(queue, pipelineDataSolid);
|
||||
|
||||
mPipelineBindGroup = &mPipelineBindGroupSolid;
|
||||
mPipelineBase = &solid;
|
||||
}
|
||||
}
|
||||
|
||||
void WgRenderDataShapeSettings::release() {
|
||||
mPipelineBindGroupSolid.release();
|
||||
mPipelineBindGroupLinear.release();
|
||||
mPipelineBindGroupRadial.release();
|
||||
}
|
||||
|
||||
//***********************************************************************
|
||||
// WgRenderDataShape
|
||||
//***********************************************************************
|
||||
|
||||
void WgRenderDataShape::release() {
|
||||
releaseRenderData();
|
||||
mRenderSettingsShape.release();
|
||||
mRenderSettingsStroke.release();
|
||||
}
|
||||
|
||||
void WgRenderDataShape::releaseRenderData() {
|
||||
for (uint32_t i = 0; i < mGeometryDataFill.count; i++) {
|
||||
mGeometryDataFill[i]->release();
|
||||
delete mGeometryDataFill[i];
|
||||
for (uint32_t i = 0; i < mGeometryDataStroke.count; i++) {
|
||||
mGeometryDataStroke[i]->release();
|
||||
delete mGeometryDataStroke[i];
|
||||
}
|
||||
mGeometryDataFill.clear();
|
||||
mGeometryDataStroke.clear();
|
||||
for (uint32_t i = 0; i < mGeometryDataShape.count; i++) {
|
||||
mGeometryDataShape[i]->release();
|
||||
delete mGeometryDataShape[i];
|
||||
}
|
||||
mGeometryDataShape.clear();
|
||||
}
|
||||
|
||||
void WgRenderDataShape::tesselate(WGPUDevice device, WGPUQueue queue, const RenderShape& rshape) {
|
||||
releaseRenderData();
|
||||
Array<WgVertexList*> outlines{};
|
||||
decodePath(rshape, outlines);
|
||||
|
||||
Array<Array<float>*> outlines;
|
||||
// create geometry data for fill for each outline
|
||||
for (uint32_t i = 0; i < outlines.count; i++) {
|
||||
auto outline = outlines[i];
|
||||
// append shape if it can create at least one triangle
|
||||
if (outline->mVertexList.count > 2) {
|
||||
outline->computeTriFansIndexes();
|
||||
|
||||
// create geometry data for fill using triangle fan indexes
|
||||
WgGeometryData* geometryData = new WgGeometryData();
|
||||
geometryData->update(device, queue, outline);
|
||||
mGeometryDataShape.push(geometryData);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < outlines.count; i++)
|
||||
delete outlines[i];
|
||||
}
|
||||
|
||||
// TODO: separate to entity
|
||||
void WgRenderDataShape::stroke(WGPUDevice device, WGPUQueue queue, const RenderShape& rshape) {
|
||||
if (!rshape.stroke) return;
|
||||
|
||||
// TODO: chnage to shared_ptrs
|
||||
Array<WgVertexList*> outlines{};
|
||||
decodePath(rshape, outlines);
|
||||
|
||||
WgVertexList strokes;
|
||||
strokeSublines(rshape, outlines, strokes);
|
||||
|
||||
// append shape if it can create at least one triangle
|
||||
// TODO: create single geometry data for strokes pere shape
|
||||
if (strokes.mIndexList.count > 2) {
|
||||
WgGeometryData* geometryData = new WgGeometryData();
|
||||
geometryData->initialize(device);
|
||||
geometryData->update(device, queue, &strokes);
|
||||
mGeometryDataStroke.push(geometryData);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < outlines.count; i++)
|
||||
delete outlines[i];
|
||||
}
|
||||
|
||||
void WgRenderDataShape::decodePath(const RenderShape& rshape, Array<WgVertexList*>& outlines) {
|
||||
size_t pntIndex = 0;
|
||||
for (uint32_t cmdIndex = 0; cmdIndex < rshape.path.cmds.count; cmdIndex++) {
|
||||
PathCommand cmd = rshape.path.cmds[cmdIndex];
|
||||
if (cmd == PathCommand::MoveTo) {
|
||||
outlines.push(new Array<float>);
|
||||
|
||||
outlines.push(new WgVertexList);
|
||||
auto outline = outlines.last();
|
||||
outline->push(rshape.path.pts[pntIndex].x);
|
||||
outline->push(rshape.path.pts[pntIndex].y);
|
||||
outline->push(0.0f);
|
||||
|
||||
outline->mVertexList.push(rshape.path.pts[pntIndex]);
|
||||
pntIndex++;
|
||||
} else if (cmd == PathCommand::LineTo) {
|
||||
auto outline = outlines.last();
|
||||
outline->push(rshape.path.pts[pntIndex].x);
|
||||
outline->push(rshape.path.pts[pntIndex].y);
|
||||
outline->push(0.0f);
|
||||
|
||||
if (outline)
|
||||
outline->mVertexList.push(rshape.path.pts[pntIndex]);
|
||||
pntIndex++;
|
||||
} else if (cmd == PathCommand::Close) {
|
||||
auto outline = outlines.last();
|
||||
if ((outline) && (outline->count > 1)) {
|
||||
outline->push(outline->data[0]);
|
||||
outline->push(outline->data[1]);
|
||||
outline->push(0.0f);
|
||||
}
|
||||
if ((outline) && (outline->mVertexList.count > 0))
|
||||
outline->mVertexList.push(outline->mVertexList[0]);
|
||||
} else if (cmd == PathCommand::CubicTo) {
|
||||
auto outline = outlines.last();
|
||||
|
||||
Point p0 = { outline->data[outline->count - 3], outline->data[outline->count - 2] };
|
||||
Point p1 = { rshape.path.pts[pntIndex + 0].x, rshape.path.pts[pntIndex + 0].y };
|
||||
Point p2 = { rshape.path.pts[pntIndex + 1].x, rshape.path.pts[pntIndex + 1].y };
|
||||
Point p3 = { rshape.path.pts[pntIndex + 2].x, rshape.path.pts[pntIndex + 2].y };
|
||||
|
||||
const size_t segs = 16;
|
||||
for (size_t i = 1; i <= segs; i++) {
|
||||
float t = i / (float)segs;
|
||||
// get cubic spline interpolation coefficients
|
||||
float t0 = 1 * (1.0f - t) * (1.0f - t) * (1.0f - t);
|
||||
float t1 = 3 * (1.0f - t) * (1.0f - t) * t;
|
||||
float t2 = 3 * (1.0f - t) * t * t;
|
||||
float t3 = 1 * t * t * t;
|
||||
|
||||
outline->push(p0.x * t0 + p1.x * t1 + p2.x * t2 + p3.x * t3);
|
||||
outline->push(p0.y * t0 + p1.y * t1 + p2.y * t2 + p3.y * t3);
|
||||
outline->push(0.0f);
|
||||
}
|
||||
|
||||
if ((outline) && (outline->mVertexList.count > 0))
|
||||
outline->appendCubic(
|
||||
rshape.path.pts[pntIndex + 0],
|
||||
rshape.path.pts[pntIndex + 1],
|
||||
rshape.path.pts[pntIndex + 2]
|
||||
);
|
||||
pntIndex += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create render index buffers to emulate triangle fan polygon
|
||||
Array<uint32_t> indexBuffer;
|
||||
for (size_t i = 0; i < outlines.count; i++) {
|
||||
void WgRenderDataShape::strokeSublines(const RenderShape& rshape, Array<WgVertexList*>& outlines, WgVertexList& strokes) {
|
||||
float wdt = rshape.stroke->width / 2;
|
||||
for (uint32_t i = 0; i < outlines.count; i++) {
|
||||
auto outline = outlines[i];
|
||||
|
||||
size_t vertexCount = outline->count / 3;
|
||||
assert(vertexCount > 2);
|
||||
|
||||
indexBuffer.clear();
|
||||
indexBuffer.reserve((vertexCount - 2) * 3);
|
||||
for (size_t j = 0; j < vertexCount - 2; j++) {
|
||||
indexBuffer.push(0);
|
||||
indexBuffer.push(j + 1);
|
||||
indexBuffer.push(j + 2);
|
||||
// single point sub-path
|
||||
if (outline->mVertexList.count == 1) {
|
||||
if (rshape.stroke->cap == StrokeCap::Round) {
|
||||
strokes.appendCircle(outline->mVertexList[0], wdt);
|
||||
} else if (rshape.stroke->cap == StrokeCap::Butt) {
|
||||
// for zero length sub-paths no stroke is rendered
|
||||
} else if (rshape.stroke->cap == StrokeCap::Square) {
|
||||
strokes.appendRect(
|
||||
outline->mVertexList[0] + WgPoint(+wdt, +wdt),
|
||||
outline->mVertexList[0] + WgPoint(+wdt, -wdt),
|
||||
outline->mVertexList[0] + WgPoint(-wdt, +wdt),
|
||||
outline->mVertexList[0] + WgPoint(-wdt, -wdt)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WgGeometryData* geometryData = new WgGeometryData();
|
||||
geometryData->initialize(device);
|
||||
geometryData->update(device, queue, outline->data, vertexCount, indexBuffer.data, indexBuffer.count);
|
||||
|
||||
mGeometryDataFill.push(geometryData);
|
||||
|
||||
// single line sub-path
|
||||
if (outline->mVertexList.count == 2) {
|
||||
WgPoint v0 = outline->mVertexList[0];
|
||||
WgPoint v1 = outline->mVertexList[1];
|
||||
WgPoint dir0 = (v1 - v0).normal();
|
||||
WgPoint nrm0 = WgPoint{ -dir0.y, +dir0.x };
|
||||
if (rshape.stroke->cap == StrokeCap::Round) {
|
||||
strokes.appendRect(
|
||||
v0 - nrm0 * wdt, v0 + nrm0 * wdt,
|
||||
v1 - nrm0 * wdt, v1 + nrm0 * wdt);
|
||||
strokes.appendCircle(outline->mVertexList[0], wdt);
|
||||
strokes.appendCircle(outline->mVertexList[1], wdt);
|
||||
} else if (rshape.stroke->cap == StrokeCap::Butt) {
|
||||
strokes.appendRect(
|
||||
v0 - nrm0 * wdt, v0 + nrm0 * wdt,
|
||||
v1 - nrm0 * wdt, v1 + nrm0 * wdt
|
||||
);
|
||||
} else if (rshape.stroke->cap == StrokeCap::Square) {
|
||||
strokes.appendRect(
|
||||
v0 - nrm0 * wdt - dir0 * wdt, v0 + nrm0 * wdt - dir0 * wdt,
|
||||
v1 - nrm0 * wdt + dir0 * wdt, v1 + nrm0 * wdt + dir0 * wdt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// multi-lined sub-path
|
||||
if (outline->mVertexList.count > 2) {
|
||||
// append first cap
|
||||
WgPoint v0 = outline->mVertexList[0];
|
||||
WgPoint v1 = outline->mVertexList[1];
|
||||
WgPoint dir0 = (v1 - v0).normal();
|
||||
WgPoint nrm0 = WgPoint{ -dir0.y, +dir0.x };
|
||||
if (rshape.stroke->cap == StrokeCap::Round) {
|
||||
strokes.appendCircle(v0, wdt);
|
||||
} else if (rshape.stroke->cap == StrokeCap::Butt) {
|
||||
// no cap needed
|
||||
} else if (rshape.stroke->cap == StrokeCap::Square) {
|
||||
strokes.appendRect(
|
||||
v0 - nrm0 * wdt - dir0 * wdt,
|
||||
v0 + nrm0 * wdt - dir0 * wdt,
|
||||
v0 - nrm0 * wdt,
|
||||
v0 + nrm0 * wdt
|
||||
);
|
||||
}
|
||||
|
||||
// append last cap
|
||||
v0 = outline->mVertexList[outline->mVertexList.count - 2];
|
||||
v1 = outline->mVertexList[outline->mVertexList.count - 1];
|
||||
dir0 = (v1 - v0).normal();
|
||||
nrm0 = WgPoint{ -dir0.y, +dir0.x };
|
||||
if (rshape.stroke->cap == StrokeCap::Round) {
|
||||
strokes.appendCircle(v1, wdt);
|
||||
} else if (rshape.stroke->cap == StrokeCap::Butt) {
|
||||
// no cap needed
|
||||
} else if (rshape.stroke->cap == StrokeCap::Square) {
|
||||
strokes.appendRect(
|
||||
v1 - nrm0 * wdt,
|
||||
v1 + nrm0 * wdt,
|
||||
v1 - nrm0 * wdt + dir0 * wdt,
|
||||
v1 + nrm0 * wdt + dir0 * wdt
|
||||
);
|
||||
}
|
||||
|
||||
// append sub-lines
|
||||
for (uint32_t j = 0; j < outline->mVertexList.count - 1; j++) {
|
||||
WgPoint v0 = outline->mVertexList[j + 0];
|
||||
WgPoint v1 = outline->mVertexList[j + 1];
|
||||
WgPoint dir = (v1 - v0).normal();
|
||||
WgPoint nrm { -dir.y, +dir.x };
|
||||
strokes.appendRect(
|
||||
v0 - nrm * wdt,
|
||||
v0 + nrm * wdt,
|
||||
v1 - nrm * wdt,
|
||||
v1 + nrm * wdt
|
||||
);
|
||||
}
|
||||
|
||||
// append joints (TODO: separate by joint types)
|
||||
for (uint32_t j = 1; j < outline->mVertexList.count - 1; j++) {
|
||||
WgPoint v0 = outline->mVertexList[j - 1];
|
||||
WgPoint v1 = outline->mVertexList[j + 0];
|
||||
WgPoint v2 = outline->mVertexList[j + 1];
|
||||
WgPoint dir0 = (v1 - v0).normal();
|
||||
WgPoint dir1 = (v1 - v2).normal();
|
||||
WgPoint nrm0 { -dir0.y, +dir0.x };
|
||||
WgPoint nrm1 { +dir1.y, -dir1.x };
|
||||
if (rshape.stroke->join == StrokeJoin::Round) {
|
||||
strokes.appendCircle(v1, wdt);
|
||||
} else if (rshape.stroke->join == StrokeJoin::Bevel) {
|
||||
strokes.appendRect(
|
||||
v1 - nrm0 * wdt, v1 - nrm1 * wdt,
|
||||
v1 + nrm1 * wdt, v1 + nrm0 * wdt
|
||||
);
|
||||
} else if (rshape.stroke->join == StrokeJoin::Miter) {
|
||||
WgPoint nrm = (dir0 + dir1).normal();
|
||||
float cosine = nrm.dot(nrm0);
|
||||
if ((cosine != 0.0f) && (abs(wdt / cosine) <= rshape.stroke->miterlimit * 2)) {
|
||||
strokes.appendRect(v1 + nrm * (wdt / cosine), v1 + nrm0 * wdt, v1 + nrm1 * wdt, v1);
|
||||
strokes.appendRect(v1 - nrm * (wdt / cosine), v1 - nrm0 * wdt, v1 - nrm1 * wdt, v1);
|
||||
} else {
|
||||
strokes.appendRect(
|
||||
v1 - nrm0 * wdt, v1 - nrm1 * wdt,
|
||||
v1 + nrm1 * wdt, v1 + nrm0 * wdt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < outlines.count; i++)
|
||||
delete outlines[i];
|
||||
}
|
|
@ -20,12 +20,13 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _TVG_WG_RENDER_DATA_H_
|
||||
#define _TVG_WG_RENDER_DATA_H_
|
||||
|
||||
#include "tvgWgPipelineSolid.h"
|
||||
#include "tvgWgPipelineLinear.h"
|
||||
#include "tvgWgPipelineRadial.h"
|
||||
|
||||
#ifndef _TVG_WG_RENDER_DATA_H_
|
||||
#define _TVG_WG_RENDER_DATA_H_
|
||||
#include "tvgWgGeometry.h"
|
||||
|
||||
class WgGeometryData {
|
||||
public:
|
||||
|
@ -39,6 +40,7 @@ public:
|
|||
|
||||
void initialize(WGPUDevice device) {};
|
||||
void draw(WGPURenderPassEncoder renderPassEncoder);
|
||||
void update(WGPUDevice device, WGPUQueue queue, WgVertexList* vertexList);
|
||||
void update(WGPUDevice device, WGPUQueue queue, float* vertexData, size_t vertexCount, uint32_t* indexData, size_t indexCount);
|
||||
void release();
|
||||
};
|
||||
|
@ -49,17 +51,28 @@ public:
|
|||
virtual void release() = 0;
|
||||
};
|
||||
|
||||
class WgRenderDataShape: public WgRenderData {
|
||||
public:
|
||||
Array<WgGeometryData*> mGeometryDataFill;
|
||||
Array<WgGeometryData*> mGeometryDataStroke;
|
||||
|
||||
struct WgRenderDataShapeSettings {
|
||||
WgPipelineBindGroupSolid mPipelineBindGroupSolid{};
|
||||
WgPipelineBindGroupLinear mPipelineBindGroupLinear{};
|
||||
WgPipelineBindGroupRadial mPipelineBindGroupRadial{};
|
||||
|
||||
WgPipelineBase* mPipelineBase{}; // external
|
||||
WgPipelineBindGroup* mPipelineBindGroup{}; // external
|
||||
|
||||
// update render shape settings defined by flags and fill settings
|
||||
void update(WGPUQueue queue, const Fill* fill, const RenderUpdateFlag flags,
|
||||
const RenderTransform* transform, const float* viewMatrix, const uint8_t* color,
|
||||
WgPipelineLinear& linear, WgPipelineRadial& radial, WgPipelineSolid& solid);
|
||||
void release();
|
||||
};
|
||||
|
||||
class WgRenderDataShape: public WgRenderData {
|
||||
public:
|
||||
Array<WgGeometryData*> mGeometryDataShape;
|
||||
Array<WgGeometryData*> mGeometryDataStroke;
|
||||
|
||||
WgRenderDataShapeSettings mRenderSettingsShape;
|
||||
WgRenderDataShapeSettings mRenderSettingsStroke;
|
||||
public:
|
||||
WgRenderDataShape() {}
|
||||
|
||||
|
@ -67,6 +80,10 @@ public:
|
|||
void releaseRenderData();
|
||||
|
||||
void tesselate(WGPUDevice device, WGPUQueue queue, const RenderShape& rshape);
|
||||
void stroke(WGPUDevice device, WGPUQueue queue, const RenderShape& rshape);
|
||||
private:
|
||||
void decodePath(const RenderShape& rshape, Array<WgVertexList*>& outlines);
|
||||
void strokeSublines(const RenderShape& rshape, Array<WgVertexList*>& outlines, WgVertexList& strokes);
|
||||
};
|
||||
|
||||
#endif //_TVG_WG_RENDER_DATA_H_
|
||||
|
|
|
@ -102,11 +102,13 @@ void WgRenderer::initialize() {
|
|||
|
||||
// create pipelines
|
||||
mPipelineEmpty.initialize(mDevice);
|
||||
mPipelineStroke.initialize(mDevice);
|
||||
mPipelineSolid.initialize(mDevice);
|
||||
mPipelineLinear.initialize(mDevice);
|
||||
mPipelineRadial.initialize(mDevice);
|
||||
mPipelineBindGroupEmpty.initialize(mDevice, mPipelineEmpty);
|
||||
mGeometryDataPipeline.initialize(mDevice);
|
||||
mPipelineBindGroupStroke.initialize(mDevice, mPipelineStroke);
|
||||
mGeometryDataWindow.initialize(mDevice);
|
||||
}
|
||||
|
||||
void WgRenderer::release() {
|
||||
|
@ -117,11 +119,13 @@ void WgRenderer::release() {
|
|||
if (mStencilTexView) wgpuTextureViewRelease(mStencilTexView);
|
||||
if (mSwapChain) wgpuSwapChainRelease(mSwapChain);
|
||||
if (mSurface) wgpuSurfaceRelease(mSurface);
|
||||
mGeometryDataPipeline.release();
|
||||
mGeometryDataWindow.release();
|
||||
mPipelineBindGroupStroke.release();
|
||||
mPipelineBindGroupEmpty.release();
|
||||
mPipelineRadial.release();
|
||||
mPipelineLinear.release();
|
||||
mPipelineSolid.release();
|
||||
mPipelineStroke.release();
|
||||
mPipelineEmpty.release();
|
||||
if (mDevice) {
|
||||
wgpuDeviceDestroy(mDevice);
|
||||
|
@ -137,44 +141,31 @@ RenderData WgRenderer::prepare(const RenderShape& rshape, RenderData data, const
|
|||
if (!renderDataShape) {
|
||||
renderDataShape = new WgRenderDataShape();
|
||||
renderDataShape->initialize(mDevice);
|
||||
renderDataShape->mPipelineBindGroupSolid.initialize(mDevice, mPipelineSolid);
|
||||
renderDataShape->mPipelineBindGroupLinear.initialize(mDevice, mPipelineLinear);
|
||||
renderDataShape->mPipelineBindGroupRadial.initialize(mDevice, mPipelineRadial);
|
||||
renderDataShape->mRenderSettingsShape.mPipelineBindGroupSolid.initialize(mDevice, mPipelineSolid);
|
||||
renderDataShape->mRenderSettingsShape.mPipelineBindGroupLinear.initialize(mDevice, mPipelineLinear);
|
||||
renderDataShape->mRenderSettingsShape.mPipelineBindGroupRadial.initialize(mDevice, mPipelineRadial);
|
||||
renderDataShape->mRenderSettingsStroke.mPipelineBindGroupSolid.initialize(mDevice, mPipelineSolid);
|
||||
renderDataShape->mRenderSettingsStroke.mPipelineBindGroupLinear.initialize(mDevice, mPipelineLinear);
|
||||
renderDataShape->mRenderSettingsStroke.mPipelineBindGroupRadial.initialize(mDevice, mPipelineRadial);
|
||||
}
|
||||
|
||||
if (flags & (RenderUpdateFlag::Path | RenderUpdateFlag::Stroke))
|
||||
renderDataShape->releaseRenderData();
|
||||
|
||||
if (flags & RenderUpdateFlag::Path)
|
||||
renderDataShape->tesselate(mDevice, mQueue, rshape);
|
||||
|
||||
// setup fill properties
|
||||
if ((flags & (RenderUpdateFlag::Gradient | RenderUpdateFlag::Transform)) && (rshape.fill)) {
|
||||
// setup linear fill properties
|
||||
if (rshape.fill->identifier() == TVG_CLASS_ID_LINEAR) {
|
||||
WgPipelineDataLinear brushDataLinear{};
|
||||
brushDataLinear.updateMatrix(mViewMatrix, transform);
|
||||
brushDataLinear.updateGradient((LinearGradient*)rshape.fill);
|
||||
renderDataShape->mPipelineBindGroupLinear.update(mQueue, brushDataLinear);
|
||||
renderDataShape->mPipelineBindGroup = &renderDataShape->mPipelineBindGroupLinear;
|
||||
renderDataShape->mPipelineBase = &mPipelineLinear;
|
||||
} // setup radial fill properties
|
||||
else if (rshape.fill->identifier() == TVG_CLASS_ID_RADIAL) {
|
||||
WgPipelineDataRadial brushDataRadial{};
|
||||
brushDataRadial.updateMatrix(mViewMatrix, transform);
|
||||
brushDataRadial.updateGradient((RadialGradient*)rshape.fill);
|
||||
renderDataShape->mPipelineBindGroupRadial.update(mQueue, brushDataRadial);
|
||||
renderDataShape->mPipelineBindGroup = &renderDataShape->mPipelineBindGroupRadial;
|
||||
renderDataShape->mPipelineBase = &mPipelineRadial;
|
||||
}
|
||||
}
|
||||
if (flags & RenderUpdateFlag::Stroke)
|
||||
renderDataShape->stroke(mDevice, mQueue, rshape);
|
||||
|
||||
// setup solid fill properties
|
||||
if ((flags & (RenderUpdateFlag::Color | RenderUpdateFlag::Transform)) && (!rshape.fill)) {
|
||||
WgPipelineDataSolid pipelineDataSolid{};
|
||||
pipelineDataSolid.updateMatrix(mViewMatrix, transform);
|
||||
pipelineDataSolid.updateColor(rshape);
|
||||
renderDataShape->mPipelineBindGroupSolid.update(mQueue, pipelineDataSolid);
|
||||
renderDataShape->mPipelineBindGroup = &renderDataShape->mPipelineBindGroupSolid;
|
||||
renderDataShape->mPipelineBase = &mPipelineSolid;
|
||||
}
|
||||
// setup shape fill properties
|
||||
renderDataShape->mRenderSettingsShape.update(mQueue, rshape.fill, flags, transform, mViewMatrix, rshape.color,
|
||||
mPipelineLinear, mPipelineRadial, mPipelineSolid);
|
||||
|
||||
// setup stroke fill properties
|
||||
if (rshape.stroke)
|
||||
renderDataShape->mRenderSettingsStroke.update(mQueue, rshape.stroke->fill, flags, transform, mViewMatrix, rshape.stroke->color,
|
||||
mPipelineLinear, mPipelineRadial, mPipelineSolid);
|
||||
|
||||
return renderDataShape;
|
||||
}
|
||||
|
@ -283,16 +274,33 @@ bool WgRenderer::sync() {
|
|||
for (size_t i = 0; i < mRenderDatas.count; i++) {
|
||||
WgRenderDataShape* renderData = (WgRenderDataShape*)(mRenderDatas[i]);
|
||||
|
||||
for (uint32_t j = 0; j < renderData->mGeometryDataFill.count; j++) {
|
||||
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, 0);
|
||||
for (uint32_t j = 0; j < renderData->mGeometryDataShape.count; j++) {
|
||||
// draw to stencil (first pass)
|
||||
mPipelineEmpty.set(renderPassEncoder);
|
||||
mPipelineBindGroupEmpty.bind(renderPassEncoder, 0);
|
||||
renderData->mGeometryDataFill[j]->draw(renderPassEncoder);
|
||||
renderData->mGeometryDataShape[j]->draw(renderPassEncoder);
|
||||
|
||||
// fill shape (secind pass)
|
||||
renderData->mPipelineBase->set(renderPassEncoder);
|
||||
renderData->mPipelineBindGroup->bind(renderPassEncoder, 0);
|
||||
mGeometryDataPipeline.draw(renderPassEncoder);
|
||||
// fill shape (second pass)
|
||||
renderData->mRenderSettingsShape.mPipelineBase->set(renderPassEncoder);
|
||||
renderData->mRenderSettingsShape.mPipelineBindGroup->bind(renderPassEncoder, 0);
|
||||
mGeometryDataWindow.draw(renderPassEncoder);
|
||||
}
|
||||
|
||||
if (renderData->mRenderSettingsStroke.mPipelineBase) {
|
||||
// draw strokes to stencil (first pass)
|
||||
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, 255);
|
||||
for (uint32_t j = 0; j < renderData->mGeometryDataStroke.count; j++) {
|
||||
mPipelineStroke.set(renderPassEncoder);
|
||||
mPipelineBindGroupStroke.bind(renderPassEncoder, 0);
|
||||
renderData->mGeometryDataStroke[j]->draw(renderPassEncoder);
|
||||
}
|
||||
|
||||
// fill strokes (second pass)
|
||||
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, 0);
|
||||
renderData->mRenderSettingsStroke.mPipelineBase->set(renderPassEncoder);
|
||||
renderData->mRenderSettingsStroke.mPipelineBindGroup->bind(renderPassEncoder, 0);
|
||||
mGeometryDataWindow.draw(renderPassEncoder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -402,19 +410,21 @@ bool WgRenderer::target(void* window, uint32_t w, uint32_t h) {
|
|||
assert(mStencilTexView);
|
||||
|
||||
// update pipeline geometry data
|
||||
static float vertexData[] = {
|
||||
0.0f, 0.0f, 0.0f,
|
||||
(float)w, 0.0f, 0.0f,
|
||||
(float)w, (float)h, 0.0f,
|
||||
1.0f, (float)h, 0.0f
|
||||
};
|
||||
static uint32_t indexData[] = { 0, 1, 2, 0, 2, 3 };
|
||||
// update render data pipeline empty
|
||||
mGeometryDataPipeline.update(mDevice, mQueue, vertexData, 4, indexData, 6);
|
||||
WgVertexList wnd;
|
||||
wnd.appendRect(
|
||||
WgPoint(0.0f, 0.0f), WgPoint(w, 0.0f),
|
||||
WgPoint(0.0f, h), WgPoint(w, h)
|
||||
);
|
||||
// update render data pipeline empty and stroke
|
||||
mGeometryDataWindow.update(mDevice, mQueue, &wnd);
|
||||
// update bind group pipeline empty
|
||||
WgPipelineDataEmpty pipelineDataEmpty{};
|
||||
pipelineDataEmpty.updateMatrix(mViewMatrix, nullptr);
|
||||
mPipelineBindGroupEmpty.update(mQueue, pipelineDataEmpty);
|
||||
// update bind group pipeline stroke
|
||||
WgPipelineDataStroke pipelineDataStroke{};
|
||||
pipelineDataStroke.updateMatrix(mViewMatrix, nullptr);
|
||||
mPipelineBindGroupStroke.update(mQueue, pipelineDataStroke);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#define _TVG_WG_RENDERER_H_
|
||||
|
||||
#include "tvgWgPipelineEmpty.h"
|
||||
#include "tvgWgPipelineStroke.h"
|
||||
#include "tvgWgRenderData.h"
|
||||
|
||||
class WgRenderer : public RenderMethod
|
||||
|
@ -78,11 +79,13 @@ private:
|
|||
WGPUTextureView mStencilTexView{};
|
||||
private:
|
||||
WgPipelineEmpty mPipelineEmpty;
|
||||
WgPipelineStroke mPipelineStroke;
|
||||
WgPipelineSolid mPipelineSolid;
|
||||
WgPipelineLinear mPipelineLinear;
|
||||
WgPipelineRadial mPipelineRadial;
|
||||
WgGeometryData mGeometryDataPipeline;
|
||||
WgGeometryData mGeometryDataWindow;
|
||||
WgPipelineBindGroupEmpty mPipelineBindGroupEmpty;
|
||||
WgPipelineBindGroupStroke mPipelineBindGroupStroke;
|
||||
};
|
||||
|
||||
#endif /* _TVG_WG_RENDERER_H_ */
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
const char* cShaderSource_PipelineEmpty = R"(
|
||||
// vertex input
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f
|
||||
@location(0) position: vec2f
|
||||
};
|
||||
|
||||
// Matrix
|
||||
|
@ -66,7 +66,7 @@ fn fs_main(in: VertexOutput) -> void {
|
|||
const char* cShaderSource_PipelineSolid = R"(
|
||||
// vertex input
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f
|
||||
@location(0) position: vec2f
|
||||
};
|
||||
|
||||
// Matrix
|
||||
|
@ -110,7 +110,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
|||
const char* cShaderSource_PipelineLinear = R"(
|
||||
// vertex input
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f
|
||||
@location(0) position: vec2f
|
||||
};
|
||||
|
||||
// Matrix
|
||||
|
@ -195,7 +195,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
|||
const char* cShaderSource_PipelineRadial = R"(
|
||||
// vertex input
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f
|
||||
@location(0) position: vec2f
|
||||
};
|
||||
|
||||
// Matrix
|
||||
|
|
Loading…
Add table
Reference in a new issue