diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f41c5bf..4a10169 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,13 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 + - uses: actions/checkout@v5 + with: + repository: treeform/shady + ref: shady-backends + path: shady - uses: treeform/setup-nim-action@v6 + - run: nimby install shady/shady.nimble - run: nimby install gltf/gltf.nimble working-directory: ${{ github.workspace }}/.. - name: Install OpenGL test dependencies (Linux) @@ -57,12 +63,57 @@ jobs: with: path: ${{ github.workspace }} - run: nim check src/gltf.nim + - run: nim r -d:release tests/backend_shaders.nim - name: Run tests (Linux) if: runner.os == 'Linux' - run: xvfb-run -s "-screen 0 1024x768x24" env LIBGL_ALWAYS_SOFTWARE=1 nim r tests/tests.nim + run: xvfb-run -s "-screen 0 1024x768x24" env LIBGL_ALWAYS_SOFTWARE=1 nim r -d:release tests/tests.nim - name: Run tests (Windows) if: runner.os == 'Windows' shell: pwsh run: | - nim c -o:tests.exe tests/tests.nim + nim c -d:release -o:tests.exe tests/tests.nim .\tests.exe + + backend-compile: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + backend: opengl + flags: -d:useOpenGL + - os: windows-latest + backend: opengl + flags: -d:useOpenGL + - os: windows-latest + backend: directx + flags: -d:useDirectX + - os: windows-latest + backend: vulkan + flags: -d:useVulkan + - os: macos-latest + backend: opengl + flags: -d:useOpenGL + - os: macos-latest + backend: metal + flags: -d:useMetal4 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/checkout@v5 + with: + repository: treeform/shady + ref: shady-backends + path: shady + - uses: treeform/setup-nim-action@v6 + - run: nimby install shady/shady.nimble + - run: nimby install gltf/gltf.nimble + working-directory: ${{ github.workspace }}/.. + - name: Generate viewer atlas + run: nim r tools/gen_atlas.nim + - name: Validate backend shaders (${{ matrix.backend }}) + run: nim r -d:release ${{ matrix.flags }} tests/backend_shaders.nim + - name: Compile sample assets (${{ matrix.backend }}) + run: nim c --compileOnly -d:release -d:shadyBinaryShaders ${{ matrix.flags }} tests/sample_assets.nim + - name: Compile viewer (${{ matrix.backend }}) + run: nim c --compileOnly -d:release -d:shadyBinaryShaders ${{ matrix.flags }} tools/gltf_viewer.nim diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0fdba8e..3573f9e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + - uses: actions/checkout@v5 + with: + repository: treeform/shady + ref: shady-backends + path: shady - uses: treeform/setup-nim-action@v6 + - run: nimby install shady/shady.nimble - run: nimby install gltf/gltf.nimble working-directory: ${{ github.workspace }}/.. - run: nim doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} diff --git a/gltf.nimble b/gltf.nimble index 0d64801..72ed043 100644 --- a/gltf.nimble +++ b/gltf.nimble @@ -17,3 +17,5 @@ requires "opengl >= 1.2.3" requires "webby >= 0.2.1" requires "windy >= 0.4.4" requires "silky >= 0.0.2" +requires "shady >= 0.0.2" +requires "metal4 >= 0.1.0" diff --git a/src/gltf/animations.nim b/src/gltf/animations.nim index 91d16f3..a8004e9 100644 --- a/src/gltf/animations.nim +++ b/src/gltf/animations.nim @@ -1,7 +1,8 @@ import std/math, vmath, - common, models + common, + models proc resetToBase*(node: Node) = ## Reset the node and its children to the original transform. @@ -258,6 +259,7 @@ proc applyMorphs(node: Node) = tangent.x = dir.x tangent.y = dir.y tangent.z = dir.z + primitive.markGeometryDirty() for child in node.nodes: child.applyMorphs() @@ -341,4 +343,3 @@ proc updateAnimation*(node: Node, dt: float32) = if clipIdx >= 0 and clipIdx < node.animations.len: applyClipAt(node.animations[clipIdx], node.animTime) node.applyMorphs() - node.updateOnGpu() diff --git a/src/gltf/backends/directx/common.nim b/src/gltf/backends/directx/common.nim new file mode 100644 index 0000000..0364b30 --- /dev/null +++ b/src/gltf/backends/directx/common.nim @@ -0,0 +1,46 @@ +when not defined(windows): + {.error: "The glTF DirectX backend requires Windows.".} + +import + pkg/dx12 + +type + DxTopology* = enum + dtPoint, dtLine, dtTriangle + + PipelineKey* = object + topology*: DxTopology + doubleSided*: bool + blended*: bool + + DxTexture* = ref object + resource*: ID3D12Resource + format*: uint32 + isCube*: bool + mipLevels*: int + + PrimitiveData* = ref object + geometryVersion*: uint64 + vertexBuffer*: ID3D12Resource + vertexBufferPtr*: pointer + vertexBufferView*: D3D12_VERTEX_BUFFER_VIEW + vertexCapacity*: int + indexBuffer*: ID3D12Resource + indexBufferPtr*: pointer + indexBufferView*: D3D12_INDEX_BUFFER_VIEW + indexCapacity*: int + indexCount*: int + topology*: uint32 + topologyKind*: DxTopology + + MaterialData* = ref object + materialVersion*: uint64 + heap*: ID3D12DescriptorHeap + handleGpu*: D3D12_GPU_DESCRIPTOR_HANDLE + textures*: seq[DxTexture] + + GltfFileData* = ref object + sceneVersion*: uint64 + + DxPrimitive* = PrimitiveData + DxMaterial* = MaterialData diff --git a/src/gltf/backends/directx/renderer.nim b/src/gltf/backends/directx/renderer.nim new file mode 100644 index 0000000..6995b82 --- /dev/null +++ b/src/gltf/backends/directx/renderer.nim @@ -0,0 +1,1846 @@ +## DirectX backend shader sources and renderer. + +when not defined(windows): + {.error: "The glTF DirectX backend requires Windows.".} + +import + std/[algorithm, math, tables], + chroma, pixie, vmath, windy, + pkg/dx12, pkg/dx12/context, + ../../common, ../../models, + ./common, + ../shaders as shaderSources + +const + VertexEntryPoint* = "VSMain" + FragmentEntryPoint* = "PSMain" + + PbrVertexShader* = shaderSources.PbrVertHlsl + PbrFragmentShader* = shaderSources.PbrFragHlsl + SkyboxVertexShader* = shaderSources.SkyboxVertHlsl + SkyboxFragmentShader* = shaderSources.SkyboxFragHlsl + ShadowDepthVertexShader* = shaderSources.ShadowDepthVertHlsl + ShadowDepthFragmentShader* = shaderSources.ShadowDepthFragHlsl + + TextureDescriptorCount = 7 + RootTextureDescriptorCount = 7 + VertexConstantRegisters = 529 + PixelConstantRegisters = 21 + StudioEnvSize = 8 + PreferredMsaaSamples = 8'u32 + +type + DxVertex {.packed.} = object + position: array[3, float32] + color: array[4, float32] + normal: array[3, float32] + uv: array[2, float32] + tangent: array[4, float32] + joints: array[4, uint16] + weights: array[4, float32] + uv1: array[2, float32] + + RgbaSubresource = object + width, height: int + pixels: seq[ColorRGBX] + + BlendEntry = object + node: Node + primitive: Primitive + transform: Mat4 + + Renderer* = ref object + window: Window + ctx: D3D12Context + rootSignature: ID3D12RootSignature + pipelineStates: Table[PipelineKey, ID3D12PipelineState] + sampleCount: uint32 + msaaColorBuffer: ID3D12Resource + msaaRtvHeap: ID3D12DescriptorHeap + msaaRtvHandle: D3D12_CPU_DESCRIPTOR_HANDLE + depthBuffer: ID3D12Resource + dsvHeap: ID3D12DescriptorHeap + dsvHandle: D3D12_CPU_DESCRIPTOR_HANDLE + readbackBuffer: ID3D12Resource + readbackFootprint: D3D12_PLACED_SUBRESOURCE_FOOTPRINT + readbackSize: IVec2 + srvDescriptorSize: UINT + frameResources: seq[ID3D12Resource] + +proc f32bits(value: float32): uint32 = + cast[uint32](value) + +proc putFloat(data: var openArray[uint32], index: int, value: float32) = + data[index] = value.f32bits + +proc putVec2(data: var openArray[uint32], index: int, value: Vec2) = + data.putFloat(index + 0, value.x) + data.putFloat(index + 1, value.y) + +proc putVec3(data: var openArray[uint32], index: int, value: Vec3) = + data.putFloat(index + 0, value.x) + data.putFloat(index + 1, value.y) + data.putFloat(index + 2, value.z) + +proc putColor(data: var openArray[uint32], index: int, value: Color) = + data.putFloat(index + 0, value.r) + data.putFloat(index + 1, value.g) + data.putFloat(index + 2, value.b) + data.putFloat(index + 3, value.a) + +proc putMat4(data: var openArray[uint32], registerIndex: int, value: Mat4) = + var outIndex = registerIndex * 4 + for i in 0 ..< 4: + for j in 0 ..< 4: + data.putFloat(outIndex, value[i, j]) + inc outIndex + +proc perspectiveDxRh*(fovY, aspect, nearPlane, farPlane: float32): Mat4 = + ## DirectX right-handed projection matrix for vmath camera transforms. + let + h = 1.0'f32 / tan(degToRad(fovY) * 0.5'f32) + w = h / aspect + depth = nearPlane - farPlane + result[0, 0] = w + result[1, 1] = h + result[2, 2] = farPlane / depth + result[2, 3] = -1.0'f32 + result[3, 2] = (nearPlane * farPlane) / depth + +proc bufferDesc(size: uint64): D3D12_RESOURCE_DESC = + result.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER + result.Alignment = 0 + result.Width = size + result.Height = 1 + result.DepthOrArraySize = 1 + result.MipLevels = 1 + result.Format = DXGI_FORMAT_UNKNOWN + result.SampleDesc = DXGI_SAMPLE_DESC(Count: 1, Quality: 0) + result.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR + result.Flags = D3D12_RESOURCE_FLAG_NONE + +proc defaultHeap(): D3D12_HEAP_PROPERTIES = + result.typ = D3D12_HEAP_TYPE_DEFAULT + result.CPUPageProperty = 0 + result.MemoryPoolPreference = 0 + result.CreationNodeMask = 1 + result.VisibleNodeMask = 1 + +proc uploadHeap(): D3D12_HEAP_PROPERTIES = + result.typ = D3D12_HEAP_TYPE_UPLOAD + result.CPUPageProperty = 0 + result.MemoryPoolPreference = 0 + result.CreationNodeMask = 1 + result.VisibleNodeMask = 1 + +proc supportsMsaa(device: ID3D12Device, format, sampleCount: uint32): bool = + var levels = D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS( + Format: format, + SampleCount: sampleCount, + Flags: D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE, + NumQualityLevels: 0 + ) + try: + device.checkFeatureSupport( + D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, + addr levels, + UINT(sizeof(levels)) + ) + levels.NumQualityLevels > 0 + except: + false + +proc chooseMsaaSampleCount(device: ID3D12Device): uint32 = + for sampleCount in [PreferredMsaaSamples, 4'u32, 2'u32]: + if device.supportsMsaa(DXGI_FORMAT_R8G8B8A8_UNORM, sampleCount) and + device.supportsMsaa(DXGI_FORMAT_D32_FLOAT, sampleCount): + return sampleCount + 1'u32 + +proc msaaEnabled(renderer: Renderer): bool = + renderer.sampleCount > 1 + +proc readbackHeap(): D3D12_HEAP_PROPERTIES = + result.typ = D3D12_HEAP_TYPE_READBACK + result.CPUPageProperty = 0 + result.MemoryPoolPreference = 0 + result.CreationNodeMask = 1 + result.VisibleNodeMask = 1 + +proc createUploadResource( + renderer: Renderer, + byteSize: uint64, + mapped: var pointer +): ID3D12Resource = + var + heap = uploadHeap() + desc = bufferDesc(max(1'u64, byteSize)) + result = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nil + ) + result.map(0, nil, addr mapped) + +proc alignConstantBufferSize(size: int): int = + ((max(1, size) + 255) div 256) * 256 + +proc createFrameConstantBuffer( + renderer: Renderer, + data: openArray[uint32] +): uint64 = + let + dataBytes = data.len * sizeof(uint32) + byteSize = alignConstantBufferSize(dataBytes) + var mapped: pointer + let resource = renderer.createUploadResource(uint64(byteSize), mapped) + if dataBytes > 0: + copyMem(mapped, unsafeAddr data[0], dataBytes) + if byteSize > dataBytes: + zeroMem(cast[pointer](cast[uint](mapped) + uint(dataBytes)), byteSize - dataBytes) + resource.unmap(0, nil) + renderer.frameResources.add(resource) + resource.getGPUVirtualAddress() + +proc offsetCpuHandle( + base: D3D12_CPU_DESCRIPTOR_HANDLE, + descriptorSize: UINT, + index: int +): D3D12_CPU_DESCRIPTOR_HANDLE = + result = base + result.ptrValue = base.ptrValue + uint64(descriptorSize) * uint64(index) + +proc textureDesc2D( + width, + height: int, + format: uint32, + flags = D3D12_RESOURCE_FLAG_NONE, + mipLevels = 1, + sampleCount = 1'u32 +): D3D12_RESOURCE_DESC = + result.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D + result.Alignment = 0 + result.Width = uint64(max(1, width)) + result.Height = UINT(max(1, height)) + result.DepthOrArraySize = 1 + result.MipLevels = uint16(max(1, mipLevels)) + result.Format = format + result.SampleDesc = DXGI_SAMPLE_DESC(Count: sampleCount, Quality: 0) + result.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN + result.Flags = flags + +proc textureDescCube(size: int, format: uint32, mipLevels = 1): D3D12_RESOURCE_DESC = + result = textureDesc2D(size, size, format, mipLevels = mipLevels) + result.DepthOrArraySize = 6 + +proc downsample(src: RgbaSubresource): RgbaSubresource = + result.width = max(1, src.width div 2) + result.height = max(1, src.height div 2) + result.pixels = newSeq[ColorRGBX](result.width * result.height) + for y in 0 ..< result.height: + let + sy0 = y * src.height div result.height + sy1 = min(src.height, max(sy0 + 1, (y + 1) * src.height div result.height)) + for x in 0 ..< result.width: + let + sx0 = x * src.width div result.width + sx1 = min(src.width, max(sx0 + 1, (x + 1) * src.width div result.width)) + var + r, g, b, a, count: uint32 + for sy in sy0 ..< sy1: + for sx in sx0 ..< sx1: + let pixel = src.pixels[sy * src.width + sx] + r += pixel.r.uint32 + g += pixel.g.uint32 + b += pixel.b.uint32 + a += pixel.a.uint32 + inc count + result.pixels[y * result.width + x] = rgbx( + uint8(r div count), + uint8(g div count), + uint8(b div count), + uint8(a div count) + ) + +proc buildMipChain(base: RgbaSubresource): seq[RgbaSubresource] = + result.add(base) + while result[^1].width > 1 or result[^1].height > 1: + result.add(result[^1].downsample()) + +proc buildImageMips(image: Image): seq[RgbaSubresource] = + result = buildMipChain(RgbaSubresource( + width: image.width, + height: image.height, + pixels: image.data + )) + +proc studioFaceDirection(face, x, y, size: int): Vec3 = + let + u = ((x.float32 + 0.5'f32) / size.float32) * 2.0'f32 - 1.0'f32 + v = ((y.float32 + 0.5'f32) / size.float32) * 2.0'f32 - 1.0'f32 + case face + of 0: + normalize(vec3(1.0'f32, -v, -u)) + of 1: + normalize(vec3(-1.0'f32, -v, u)) + of 2: + normalize(vec3(u, 1.0'f32, v)) + of 3: + normalize(vec3(u, -1.0'f32, -v)) + of 4: + normalize(vec3(u, -v, 1.0'f32)) + of 5: + normalize(vec3(-u, -v, -1.0'f32)) + else: + vec3(0, 1, 0) + +proc studioColor(dir: Vec3): ColorRGBX = + let + hemi = clamp(dir.y * 0.5'f32 + 0.5'f32, 0.0'f32, 1.0'f32) + keyDir = normalize(vec3(0.35'f32, 0.85'f32, 0.25'f32)) + fillDir = normalize(vec3(-0.45'f32, 0.65'f32, -0.35'f32)) + key = pow(max(dot(dir, keyDir), 0.0'f32), 24.0'f32) + fill = pow(max(dot(dir, fillDir), 0.0'f32), 8.0'f32) + cool = vec3(0.18'f32, 0.19'f32, 0.21'f32) + neutral = vec3(0.58'f32, 0.6'f32, 0.63'f32) + sky = vec3(0.92'f32, 0.94'f32, 0.97'f32) + var color = mix(cool, neutral, hemi) + color = mix(color, sky, hemi * hemi) + color += vec3(0.28'f32, 0.27'f32, 0.25'f32) * key + color += vec3(0.10'f32, 0.11'f32, 0.12'f32) * fill + rgbx( + uint8(clamp((color.x * 255.0'f32).int, 0, 255)), + uint8(clamp((color.y * 255.0'f32).int, 0, 255)), + uint8(clamp((color.z * 255.0'f32).int, 0, 255)), + 255 + ) + +proc buildStudioCubeMips(): seq[RgbaSubresource] = + for face in 0 ..< 6: + var base = RgbaSubresource( + width: StudioEnvSize, + height: StudioEnvSize, + pixels: newSeq[ColorRGBX](StudioEnvSize * StudioEnvSize) + ) + for y in 0 ..< StudioEnvSize: + for x in 0 ..< StudioEnvSize: + base.pixels[y * StudioEnvSize + x] = + studioColor(studioFaceDirection(face, x, y, StudioEnvSize)) + for mip in base.buildMipChain(): + result.add(mip) + +proc executeUpload(renderer: Renderer) = + renderer.ctx.commandList.close() + var cmdList = cast[ID3D12CommandList](renderer.ctx.commandList) + renderer.ctx.commandQueue.executeCommandLists(1, addr cmdList) + renderer.ctx.waitForGpu() + +proc uploadTextureBytes( + renderer: Renderer, + desc: var D3D12_RESOURCE_DESC, + pixels: pointer, + srcRowSize: int, + srcRowCount: int, + format: uint32, + isCube = false +): DxTexture = + var heap = defaultHeap() + let resource = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr desc, + D3D12_RESOURCE_STATE_COPY_DEST, + nil + ) + + var + footprints = newSeq[D3D12_PLACED_SUBRESOURCE_FOOTPRINT](int(desc.DepthOrArraySize)) + numRows = newSeq[UINT](int(desc.DepthOrArraySize)) + rowSizes = newSeq[UINT64](int(desc.DepthOrArraySize)) + totalBytes: UINT64 + renderer.ctx.device.getCopyableFootprints( + addr desc, + 0, + UINT(desc.DepthOrArraySize), + 0'u64, + addr footprints[0], + addr numRows[0], + addr rowSizes[0], + addr totalBytes + ) + + var + uploadDesc = bufferDesc(totalBytes) + uploadHeapProps = uploadHeap() + let uploadBuffer = renderer.ctx.device.createCommittedResource( + addr uploadHeapProps, + D3D12_HEAP_FLAG_NONE, + addr uploadDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nil + ) + + var uploadPtr: pointer + uploadBuffer.map(0, nil, addr uploadPtr) + let uploadBase = cast[uint](uploadPtr) + let sourceBase = cast[uint](pixels) + for face in 0 ..< int(desc.DepthOrArraySize): + let rowPitch = int(footprints[face].Footprint.RowPitch) + var dst = cast[ptr uint8](uploadBase + uint(footprints[face].Offset)) + let faceBase = sourceBase + uint(face * srcRowSize * srcRowCount) + for y in 0 ..< srcRowCount: + let src = cast[pointer](faceBase + uint(y * srcRowSize)) + copyMem(dst, src, srcRowSize) + if rowPitch > srcRowSize: + zeroMem( + cast[pointer](cast[uint](dst) + uint(srcRowSize)), + rowPitch - srcRowSize + ) + dst = cast[ptr uint8](cast[uint](dst) + uint(rowPitch)) + uploadBuffer.unmap(0, nil) + + renderer.ctx.commandAllocator.reset() + renderer.ctx.commandList.reset(renderer.ctx.commandAllocator, nil) + for subresource in 0 ..< int(desc.DepthOrArraySize): + var dstLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: resource, + typ: D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + data: D3D12_TEXTURE_COPY_LOCATION_UNION(SubresourceIndex: uint32(subresource)) + ) + var srcLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: uploadBuffer, + typ: D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + data: D3D12_TEXTURE_COPY_LOCATION_UNION(PlacedFootprint: footprints[subresource]) + ) + renderer.ctx.commandList.copyTextureRegion( + addr dstLocation, + 0, + 0, + 0, + addr srcLocation, + nil + ) + + var barrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union(Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: resource, + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_COPY_DEST, + StateAfter: D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + )) + ) + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + renderer.executeUpload() + uploadBuffer.release() + + DxTexture(resource: resource, format: format, isCube: isCube, mipLevels: 1) + +proc uploadRgbaSubresources( + renderer: Renderer, + desc: var D3D12_RESOURCE_DESC, + subresources: openArray[RgbaSubresource], + isCube = false +): DxTexture = + var heap = defaultHeap() + let resource = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr desc, + D3D12_RESOURCE_STATE_COPY_DEST, + nil + ) + + let subresourceCount = int(desc.DepthOrArraySize) * int(desc.MipLevels) + var + footprints = newSeq[D3D12_PLACED_SUBRESOURCE_FOOTPRINT](subresourceCount) + numRows = newSeq[UINT](subresourceCount) + rowSizes = newSeq[UINT64](subresourceCount) + totalBytes: UINT64 + renderer.ctx.device.getCopyableFootprints( + addr desc, + 0, + UINT(subresourceCount), + 0'u64, + addr footprints[0], + addr numRows[0], + addr rowSizes[0], + addr totalBytes + ) + + var + uploadDesc = bufferDesc(totalBytes) + uploadHeapProps = uploadHeap() + let uploadBuffer = renderer.ctx.device.createCommittedResource( + addr uploadHeapProps, + D3D12_HEAP_FLAG_NONE, + addr uploadDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nil + ) + + var uploadPtr: pointer + uploadBuffer.map(0, nil, addr uploadPtr) + let uploadBase = cast[uint](uploadPtr) + for subresource in 0 ..< subresourceCount: + let + source = subresources[subresource] + srcRowSize = source.width * 4 + rowPitch = int(footprints[subresource].Footprint.RowPitch) + var dst = cast[ptr uint8](uploadBase + uint(footprints[subresource].Offset)) + for y in 0 ..< source.height: + let src = cast[pointer]( + cast[uint](unsafeAddr source.pixels[0]) + uint(y * srcRowSize) + ) + copyMem(dst, src, srcRowSize) + if rowPitch > srcRowSize: + zeroMem( + cast[pointer](cast[uint](dst) + uint(srcRowSize)), + rowPitch - srcRowSize + ) + dst = cast[ptr uint8](cast[uint](dst) + uint(rowPitch)) + uploadBuffer.unmap(0, nil) + + renderer.ctx.commandAllocator.reset() + renderer.ctx.commandList.reset(renderer.ctx.commandAllocator, nil) + for subresource in 0 ..< subresourceCount: + var dstLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: resource, + typ: D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + data: D3D12_TEXTURE_COPY_LOCATION_UNION(SubresourceIndex: uint32(subresource)) + ) + var srcLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: uploadBuffer, + typ: D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + data: D3D12_TEXTURE_COPY_LOCATION_UNION(PlacedFootprint: footprints[subresource]) + ) + renderer.ctx.commandList.copyTextureRegion( + addr dstLocation, + 0, + 0, + 0, + addr srcLocation, + nil + ) + + var barrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union(Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: resource, + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_COPY_DEST, + StateAfter: D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + )) + ) + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + renderer.executeUpload() + uploadBuffer.release() + + DxTexture( + resource: resource, + format: desc.Format, + isCube: isCube, + mipLevels: int(desc.MipLevels) + ) + +proc uploadImage(renderer: Renderer, image: Image): DxTexture = + let mips = image.buildImageMips() + var desc = textureDesc2D( + image.width, + image.height, + DXGI_FORMAT_R8G8B8A8_UNORM, + mipLevels = mips.len + ) + renderer.uploadRgbaSubresources(desc, mips) + +proc uploadSolidImage(renderer: Renderer, color: ColorRGBX): DxTexture = + var image = newImage(1, 1) + image.fill(color) + renderer.uploadImage(image) + +proc uploadStudioCube(renderer: Renderer): DxTexture = + let mips = buildStudioCubeMips() + var desc = textureDescCube( + StudioEnvSize, + DXGI_FORMAT_R8G8B8A8_UNORM, + mips.len div 6 + ) + renderer.uploadRgbaSubresources(desc, mips, isCube = true) + +proc uploadShadowPlaceholder(renderer: Renderer): DxTexture = + var pixel = 1.0'f32 + var desc = textureDesc2D(1, 1, DXGI_FORMAT_R32_FLOAT) + renderer.uploadTextureBytes( + desc, + addr pixel, + sizeof(float32), + 1, + DXGI_FORMAT_R32_FLOAT + ) + +proc createSrv( + renderer: Renderer, + texture: DxTexture, + handle: D3D12_CPU_DESCRIPTOR_HANDLE +) = + var srvDesc: D3D12_SHADER_RESOURCE_VIEW_DESC + srvDesc.Format = texture.format + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING + if texture.isCube: + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE + srvDesc.data = D3D12_SHADER_RESOURCE_VIEW_DESC_UNION( + TextureCube: D3D12_TEXCUBE_SRV( + MostDetailedMip: 0, + MipLevels: UINT(texture.mipLevels), + ResourceMinLODClamp: 0.0 + ) + ) + else: + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D + srvDesc.data = D3D12_SHADER_RESOURCE_VIEW_DESC_UNION( + Texture2D: D3D12_TEX2D_SRV( + MostDetailedMip: 0, + MipLevels: UINT(texture.mipLevels), + PlaneSlice: 0, + ResourceMinLODClamp: 0.0 + ) + ) + renderer.ctx.device.createShaderResourceView( + texture.resource, + addr srvDesc, + handle + ) + +proc createColorBuffer(renderer: Renderer, size: IVec2) = + if renderer.msaaColorBuffer != nil: + renderer.msaaColorBuffer.release() + renderer.msaaColorBuffer = nil + if renderer.msaaRtvHeap != nil: + renderer.msaaRtvHeap.release() + renderer.msaaRtvHeap = nil + if not renderer.msaaEnabled: + return + + var rtvHeapDesc = D3D12_DESCRIPTOR_HEAP_DESC( + typ: D3D12_DESCRIPTOR_HEAP_TYPE_RTV, + NumDescriptors: 1, + Flags: D3D12_DESCRIPTOR_HEAP_FLAG_NONE, + NodeMask: 0 + ) + renderer.msaaRtvHeap = + renderer.ctx.device.createDescriptorHeap(addr rtvHeapDesc) + renderer.msaaRtvHandle = + renderer.msaaRtvHeap.getCPUDescriptorHandleForHeapStart() + + var colorDesc = textureDesc2D( + size.x.int, + size.y.int, + DXGI_FORMAT_R8G8B8A8_UNORM, + D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET, + sampleCount = renderer.sampleCount + ) + var + heap = defaultHeap() + clearColor = [0.0.FLOAT, 0.0.FLOAT, 0.0.FLOAT, 1.0.FLOAT] + clearValue = D3D12_CLEAR_VALUE( + Format: DXGI_FORMAT_R8G8B8A8_UNORM, + data: D3D12_CLEAR_VALUE_UNION(Color: clearColor) + ) + renderer.msaaColorBuffer = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr colorDesc, + D3D12_RESOURCE_STATE_RENDER_TARGET, + addr clearValue + ) + renderer.ctx.device.createRenderTargetView( + renderer.msaaColorBuffer, + nil, + renderer.msaaRtvHandle + ) + +proc createDepthBuffer(renderer: Renderer, size: IVec2) = + if renderer.depthBuffer != nil: + renderer.depthBuffer.release() + renderer.depthBuffer = nil + if renderer.dsvHeap != nil: + renderer.dsvHeap.release() + renderer.dsvHeap = nil + + var dsvHeapDesc = D3D12_DESCRIPTOR_HEAP_DESC( + typ: D3D12_DESCRIPTOR_HEAP_TYPE_DSV, + NumDescriptors: 1, + Flags: D3D12_DESCRIPTOR_HEAP_FLAG_NONE, + NodeMask: 0 + ) + renderer.dsvHeap = renderer.ctx.device.createDescriptorHeap(addr dsvHeapDesc) + renderer.dsvHandle = renderer.dsvHeap.getCPUDescriptorHandleForHeapStart() + + var depthDesc = textureDesc2D( + size.x.int, + size.y.int, + DXGI_FORMAT_D32_FLOAT, + D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL, + sampleCount = renderer.sampleCount + ) + var heap = defaultHeap() + var clearValue = D3D12_CLEAR_VALUE( + Format: DXGI_FORMAT_D32_FLOAT, + data: D3D12_CLEAR_VALUE_UNION( + DepthStencil: D3D12_DEPTH_STENCIL_VALUE(Depth: 1.0'f32, Stencil: 0) + ) + ) + renderer.depthBuffer = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr depthDesc, + D3D12_RESOURCE_STATE_DEPTH_WRITE, + addr clearValue + ) + renderer.ctx.device.createDepthStencilView( + renderer.depthBuffer, + nil, + renderer.dsvHandle + ) + +proc createReadbackBuffer(renderer: Renderer, size: IVec2) = + if renderer.readbackBuffer != nil: + renderer.readbackBuffer.release() + renderer.readbackBuffer = nil + + var readbackDesc = textureDesc2D( + size.x.int, + size.y.int, + DXGI_FORMAT_R8G8B8A8_UNORM + ) + var + numRows: UINT + rowSize: UINT64 + totalBytes: UINT64 + renderer.ctx.device.getCopyableFootprints( + addr readbackDesc, + 0, + 1, + 0'u64, + addr renderer.readbackFootprint, + addr numRows, + addr rowSize, + addr totalBytes + ) + + var + heap = readbackHeap() + desc = bufferDesc(totalBytes) + renderer.readbackBuffer = renderer.ctx.device.createCommittedResource( + addr heap, + D3D12_HEAP_FLAG_NONE, + addr desc, + D3D12_RESOURCE_STATE_COPY_DEST, + nil + ) + renderer.readbackSize = size + +proc createPipeline( + renderer: Renderer, + key: PipelineKey, + vsCode, + psCode: string +): ID3D12PipelineState = + let + vsBlob = compileShader(vsCode, VertexEntryPoint, "vs_5_0") + psBlob = compileShader(psCode, FragmentEntryPoint, "ps_5_0") + + var inputElements = [ + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "POSITION", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32B32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 0, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "COLOR", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32B32A32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 12, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "NORMAL", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32B32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 28, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 40, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 1, + Format: DXGI_FORMAT_R32G32B32A32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 48, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 2, + Format: DXGI_FORMAT_R16G16B16A16_UINT, + InputSlot: 0, + AlignedByteOffset: 64, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 3, + Format: DXGI_FORMAT_R32G32B32A32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 72, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 4, + Format: DXGI_FORMAT_R32G32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 88, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ) + ] + + var blendDesc: D3D12_BLEND_DESC + blendDesc.AlphaToCoverageEnable = 0 + blendDesc.IndependentBlendEnable = 0 + blendDesc.RenderTarget[0] = D3D12_RENDER_TARGET_BLEND_DESC( + BlendEnable: if key.blended: 1 else: 0, + LogicOpEnable: 0, + SrcBlend: if key.blended: D3D12_BLEND_SRC_ALPHA else: D3D12_BLEND_ONE, + DestBlend: if key.blended: D3D12_BLEND_INV_SRC_ALPHA else: D3D12_BLEND_ZERO, + BlendOp: D3D12_BLEND_OP_ADD, + SrcBlendAlpha: D3D12_BLEND_ONE, + DestBlendAlpha: if key.blended: D3D12_BLEND_INV_SRC_ALPHA else: D3D12_BLEND_ZERO, + BlendOpAlpha: D3D12_BLEND_OP_ADD, + LogicOp: 0, + RenderTargetWriteMask: uint8(D3D12_COLOR_WRITE_ENABLE_ALL) + ) + + let depthOp = D3D12_DEPTH_STENCILOP_DESC( + StencilFailOp: D3D12_STENCIL_OP_KEEP, + StencilDepthFailOp: D3D12_STENCIL_OP_KEEP, + StencilPassOp: D3D12_STENCIL_OP_KEEP, + StencilFunc: D3D12_COMPARISON_FUNC_ALWAYS + ) + + var psoDesc = D3D12_GRAPHICS_PIPELINE_STATE_DESC( + pRootSignature: renderer.rootSignature, + VS: shaderBytecode(vsBlob), + PS: shaderBytecode(psBlob), + StreamOutput: D3D12_STREAM_OUTPUT_DESC(), + BlendState: blendDesc, + SampleMask: D3D12_DEFAULT_SAMPLE_MASK, + RasterizerState: D3D12_RASTERIZER_DESC( + FillMode: D3D12_FILL_MODE_SOLID, + CullMode: if key.doubleSided: D3D12_CULL_MODE_NONE else: D3D12_CULL_MODE_BACK, + FrontCounterClockwise: 1, + DepthBias: 0, + DepthBiasClamp: 0.0, + SlopeScaledDepthBias: 0.0, + DepthClipEnable: 1, + MultisampleEnable: if renderer.msaaEnabled: 1 else: 0, + AntialiasedLineEnable: 0, + ForcedSampleCount: 0, + ConservativeRaster: D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF + ), + DepthStencilState: D3D12_DEPTH_STENCIL_DESC( + DepthEnable: 1, + DepthWriteMask: + if key.blended: D3D12_DEPTH_WRITE_MASK_ZERO + else: D3D12_DEPTH_WRITE_MASK_ALL, + DepthFunc: D3D12_COMPARISON_FUNC_LESS, + StencilEnable: 0, + StencilReadMask: 0xff'u8, + StencilWriteMask: 0xff'u8, + FrontFace: depthOp, + BackFace: depthOp + ), + InputLayout: D3D12_INPUT_LAYOUT_DESC( + pInputElementDescs: addr inputElements[0], + NumElements: uint32(inputElements.len) + ), + IBStripCutValue: 0, + PrimitiveTopologyType: + case key.topology + of dtPoint: D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT + of dtLine: D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE + of dtTriangle: D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, + NumRenderTargets: 1, + DSVFormat: DXGI_FORMAT_D32_FLOAT, + SampleDesc: DXGI_SAMPLE_DESC(Count: renderer.sampleCount, Quality: 0), + NodeMask: 0, + CachedPSO: D3D12_CACHED_PIPELINE_STATE(), + Flags: 0 + ) + psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM + result = renderer.ctx.device.createGraphicsPipelineState(addr psoDesc) + release(vsBlob) + release(psBlob) + +proc getPipeline(renderer: Renderer, key: PipelineKey): ID3D12PipelineState = + if key notin renderer.pipelineStates: + renderer.pipelineStates[key] = renderer.createPipeline( + key, + PbrVertexShader, + PbrFragmentShader + ) + renderer.pipelineStates[key] + +proc createRootSignature(renderer: Renderer) = + var srvRange = D3D12_DESCRIPTOR_RANGE( + RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV, + NumDescriptors: RootTextureDescriptorCount, + BaseShaderRegister: 0, + RegisterSpace: 0, + OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND + ) + var rootParams = [ + D3D12_ROOT_PARAMETER( + ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV, + data: D3D12_ROOT_PARAMETER_UNION( + Descriptor: D3D12_ROOT_DESCRIPTOR( + ShaderRegister: 0, + RegisterSpace: 0 + ) + ), + ShaderVisibility: D3D12_SHADER_VISIBILITY_VERTEX + ), + D3D12_ROOT_PARAMETER( + ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV, + data: D3D12_ROOT_PARAMETER_UNION( + Descriptor: D3D12_ROOT_DESCRIPTOR( + ShaderRegister: 1, + RegisterSpace: 0 + ) + ), + ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL + ), + D3D12_ROOT_PARAMETER( + ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, + data: D3D12_ROOT_PARAMETER_UNION( + DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE( + NumDescriptorRanges: 1, + pDescriptorRanges: addr srvRange + ) + ), + ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL + ) + ] + + var samplers: array[RootTextureDescriptorCount, D3D12_STATIC_SAMPLER_DESC] + for i in 0 ..< samplers.len: + samplers[i] = D3D12_STATIC_SAMPLER_DESC( + Filter: + if i == 6: D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT + else: D3D12_FILTER_MIN_MAG_MIP_LINEAR, + AddressU: + if i == 5 or i == 6: D3D12_TEXTURE_ADDRESS_MODE_CLAMP + else: D3D12_TEXTURE_ADDRESS_MODE_WRAP, + AddressV: + if i == 5 or i == 6: D3D12_TEXTURE_ADDRESS_MODE_CLAMP + else: D3D12_TEXTURE_ADDRESS_MODE_WRAP, + AddressW: + if i == 5 or i == 6: D3D12_TEXTURE_ADDRESS_MODE_CLAMP + else: D3D12_TEXTURE_ADDRESS_MODE_WRAP, + MipLODBias: 0.0, + MaxAnisotropy: 1, + ComparisonFunc: + if i == 6: D3D12_COMPARISON_FUNC_LESS_EQUAL + else: D3D12_COMPARISON_FUNC_ALWAYS, + BorderColor: D3D12_STATIC_BORDER_COLOR_OPAQUE_WHITE, + MinLOD: 0.0, + MaxLOD: 1000.0, + ShaderRegister: uint32(i), + RegisterSpace: 0, + ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL + ) + var rootDesc = D3D12_ROOT_SIGNATURE_DESC( + NumParameters: uint32(rootParams.len), + pParameters: addr rootParams[0], + NumStaticSamplers: uint32(samplers.len), + pStaticSamplers: addr samplers[0], + Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT + ) + let rootBlob = serializeRootSignature(addr rootDesc) + renderer.rootSignature = renderer.ctx.device.createRootSignature( + 0, + getBufferPointer(rootBlob), + getBufferSize(rootBlob) + ) + release(rootBlob) + +proc newRenderer*(window: Window): Renderer = + ## Creates a DirectX 12 renderer bound to a Windy window. + let safeSize = ivec2(max(1'i32, window.size.x), max(1'i32, window.size.y)) + result = Renderer(window: window) + let hwnd = window.getHWND() + if hwnd == 0: + raise newException(GltfError, "Failed to acquire HWND for DirectX renderer.") + result.ctx.initDevice(hwnd, safeSize.x.int, safeSize.y.int) + result.sampleCount = chooseMsaaSampleCount(result.ctx.device) + result.srvDescriptorSize = + result.ctx.device.getDescriptorHandleIncrementSize( + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV + ) + result.createRootSignature() + result.createColorBuffer(safeSize) + result.createDepthBuffer(safeSize) + result.createReadbackBuffer(safeSize) + +proc releaseTexture(texture: DxTexture) = + if texture != nil and texture.resource != nil: + texture.resource.release() + texture.resource = nil + +proc releaseMaterial(material: DxMaterial) = + if material == nil: + return + for texture in material.textures: + texture.releaseTexture() + material.textures.setLen(0) + if material.heap != nil: + material.heap.release() + material.heap = nil + +proc releasePrimitive(primitive: DxPrimitive) = + if primitive == nil: + return + if primitive.vertexBuffer != nil: + if primitive.vertexBufferPtr != nil: + primitive.vertexBuffer.unmap(0, nil) + primitive.vertexBufferPtr = nil + primitive.vertexBuffer.release() + primitive.vertexBuffer = nil + if primitive.indexBuffer != nil: + if primitive.indexBufferPtr != nil: + primitive.indexBuffer.unmap(0, nil) + primitive.indexBufferPtr = nil + primitive.indexBuffer.release() + primitive.indexBuffer = nil + +proc resize(renderer: Renderer, size: IVec2) = + let safeSize = ivec2(max(1'i32, size.x), max(1'i32, size.y)) + if renderer.readbackSize == safeSize: + return + renderer.ctx.resize(safeSize.x.int, safeSize.y.int) + renderer.createColorBuffer(safeSize) + renderer.createDepthBuffer(safeSize) + renderer.createReadbackBuffer(safeSize) + +proc vertexAt(primitive: Primitive, index: int): DxVertex = + let colorValue = + if index < primitive.colors.len: + primitive.colors[index].color + else: + color(1, 1, 1, 1) + result.position = [ + primitive.points[index].x, + primitive.points[index].y, + primitive.points[index].z + ] + result.color = [ + colorValue.r, + colorValue.g, + colorValue.b, + colorValue.a + ] + let normal = + if index < primitive.normals.len: + primitive.normals[index] + else: + vec3(0, 0, 0) + result.normal = [normal.x, normal.y, normal.z] + let uv = + if index < primitive.uvs.len: + primitive.uvs[index] + else: + vec2(0, 0) + result.uv = [uv.x, uv.y] + let tangent = + if index < primitive.tangents.len: + primitive.tangents[index] + else: + vec4(1, 0, 0, 1) + result.tangent = [tangent.x, tangent.y, tangent.z, tangent.w] + let joints = + if index < primitive.jointIds.len: + primitive.jointIds[index] + else: + [0'u16, 0'u16, 0'u16, 0'u16] + result.joints = joints + let weights = + if index < primitive.jointWeights.len: + primitive.jointWeights[index] + else: + vec4(0, 0, 0, 0) + result.weights = [weights.x, weights.y, weights.z, weights.w] + let uv1 = + if index < primitive.uvs1.len: + primitive.uvs1[index] + else: + vec2(0, 0) + result.uv1 = [uv1.x, uv1.y] + +proc primitiveSourceIndices(primitive: Primitive): seq[uint32] = + if primitive.indices32.len > 0: + result = primitive.indices32 + elif primitive.indices16.len > 0: + result.setLen(primitive.indices16.len) + for i, value in primitive.indices16: + result[i] = value.uint32 + else: + result.setLen(primitive.points.len) + for i in 0 ..< primitive.points.len: + result[i] = i.uint32 + +proc buildIndexData( + primitive: Primitive, + topology: var uint32, + topologyKind: var DxTopology +): seq[uint32] = + let src = primitive.primitiveSourceIndices() + case primitive.mode.int + of 0: # GL_POINTS + topology = D3D_PRIMITIVE_TOPOLOGY_POINTLIST + topologyKind = dtPoint + result = src + of 1: # GL_LINES + topology = D3D_PRIMITIVE_TOPOLOGY_LINELIST + topologyKind = dtLine + result = src + of 3: # GL_LINE_STRIP + topology = D3D_PRIMITIVE_TOPOLOGY_LINESTRIP + topologyKind = dtLine + result = src + of 2: # GL_LINE_LOOP + topology = D3D_PRIMITIVE_TOPOLOGY_LINELIST + topologyKind = dtLine + if src.len >= 2: + for i in 0 ..< src.len: + result.add(src[i]) + result.add(src[(i + 1) mod src.len]) + of 5: # GL_TRIANGLE_STRIP + topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP + topologyKind = dtTriangle + result = src + of 6: # GL_TRIANGLE_FAN + topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST + topologyKind = dtTriangle + if src.len >= 3: + for i in 1 ..< src.len - 1: + result.add(src[0]) + result.add(src[i]) + result.add(src[i + 1]) + else: + topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST + topologyKind = dtTriangle + result = src + +proc ensurePrimitive(renderer: Renderer, primitive: Primitive): DxPrimitive = + if primitive.normals.len == 0 and primitive.mode.int == 4: + primitive.computeSmoothNormals() + + if primitive.data == nil: + primitive.data = DxPrimitive() + result = primitive.data + + if primitive.points.len > result.vertexCapacity: + if result.vertexBuffer != nil: + if result.vertexBufferPtr != nil: + result.vertexBuffer.unmap(0, nil) + result.vertexBuffer.release() + result.vertexCapacity = max(primitive.points.len, 1) + result.vertexBuffer = renderer.createUploadResource( + uint64(result.vertexCapacity * sizeof(DxVertex)), + result.vertexBufferPtr + ) + result.vertexBufferView = D3D12_VERTEX_BUFFER_VIEW( + BufferLocation: result.vertexBuffer.getGPUVirtualAddress(), + SizeInBytes: UINT(result.vertexCapacity * sizeof(DxVertex)), + StrideInBytes: UINT(sizeof(DxVertex)) + ) + + var vertices = newSeq[DxVertex](primitive.points.len) + for i in 0 ..< primitive.points.len: + vertices[i] = primitive.vertexAt(i) + if vertices.len > 0: + copyMem( + result.vertexBufferPtr, + unsafeAddr vertices[0], + vertices.len * sizeof(DxVertex) + ) + + var topology: uint32 + var topologyKind: DxTopology + let indices = primitive.buildIndexData(topology, topologyKind) + result.topology = topology + result.topologyKind = topologyKind + result.indexCount = indices.len + if indices.len > result.indexCapacity: + if result.indexBuffer != nil: + if result.indexBufferPtr != nil: + result.indexBuffer.unmap(0, nil) + result.indexBuffer.release() + result.indexCapacity = max(indices.len, 1) + result.indexBuffer = renderer.createUploadResource( + uint64(result.indexCapacity * sizeof(uint32)), + result.indexBufferPtr + ) + result.indexBufferView = D3D12_INDEX_BUFFER_VIEW( + BufferLocation: result.indexBuffer.getGPUVirtualAddress(), + SizeInBytes: UINT(result.indexCapacity * sizeof(uint32)), + Format: DXGI_FORMAT_R32_UINT + ) + if indices.len > 0: + copyMem( + result.indexBufferPtr, + unsafeAddr indices[0], + indices.len * sizeof(uint32) + ) + result.geometryVersion = primitive.geometryVersion + +proc ensureMaterial(renderer: Renderer, material: Material): DxMaterial = + if material == nil: + return nil + if material.data != nil and material.data.materialVersion == material.materialVersion: + return material.data + if material.data != nil: + material.data.releaseMaterial() + + result = DxMaterial() + material.data = result + + var heapDesc = D3D12_DESCRIPTOR_HEAP_DESC( + typ: D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, + NumDescriptors: TextureDescriptorCount, + Flags: D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, + NodeMask: 0 + ) + result.heap = renderer.ctx.device.createDescriptorHeap(addr heapDesc) + let baseHandle = result.heap.getCPUDescriptorHandleForHeapStart() + result.handleGpu = result.heap.getGPUDescriptorHandleForHeapStart() + + let + baseColor = + if material != nil and material.baseColor != nil: + renderer.uploadImage(material.baseColor) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + metallicRoughness = + if material != nil and material.metallicRoughness != nil: + renderer.uploadImage(material.metallicRoughness) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + occlusion = + if material != nil and material.occlusion != nil: + renderer.uploadImage(material.occlusion) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + emissive = + if material != nil and material.emissive != nil: + renderer.uploadImage(material.emissive) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + normal = + if material != nil and material.normal != nil: + renderer.uploadImage(material.normal) + else: + renderer.uploadSolidImage(rgbx(128, 128, 255, 255)) + shadow = renderer.uploadShadowPlaceholder() + environment = renderer.uploadStudioCube() + + result.textures = @[ + baseColor, + metallicRoughness, + occlusion, + emissive, + normal, + environment, + shadow + ] + for i, texture in result.textures: + renderer.createSrv( + texture, + offsetCpuHandle(baseHandle, renderer.srvDescriptorSize, i) + ) + result.materialVersion = material.materialVersion + +proc prepareNodeResources(renderer: Renderer, node: Node) = + if node == nil: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.hasGeometry(): + discard renderer.ensurePrimitive(primitive) + discard renderer.ensureMaterial(primitive.material) + for child in node.nodes: + renderer.prepareNodeResources(child) + +proc putTextureTransform( + data: var openArray[uint32], + texCoordIndex, + offsetIndex, + scaleIndex, + rotationIndex: int, + transform: TextureTransform +) = + data[texCoordIndex] = transform.texCoord.uint32 + data.putVec2(offsetIndex, transform.offset) + data.putVec2(scaleIndex, transform.scale) + data.putFloat(rotationIndex, transform.rotation) + +proc shadyVertexConstants( + owner, + root: Node, + transform, + view, + proj: Mat4 +): array[VertexConstantRegisters * 4, uint32] = + let jointMatrices = root.skinMatrices(owner) + result[0] = (jointMatrices.len > 0).ord.uint32 + for i in 0 ..< min(jointMatrices.len, 128): + result.putMat4(1 + i * 4, jointMatrices[i]) + result.putMat4(513, transform) + result.putMat4(517, mat4()) + result.putMat4(521, proj) + result.putMat4(525, view) + +proc shadyPixelConstants( + primitive: Primitive, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3 +): array[PixelConstantRegisters * 4, uint32] = + let material = primitive.material + if material != nil: + result.putTextureTransform(0, 1, 4, 6, material.baseColorTransform) + result.putTextureTransform(7, 8, 10, 12, material.metallicRoughnessTransform) + result.putTextureTransform(13, 14, 16, 18, material.normalTransform) + result.putTextureTransform(19, 20, 22, 24, material.occlusionTransform) + result.putTextureTransform(25, 26, 28, 30, material.emissiveTransform) + result.putColor(32, material.baseColorFactor) + result.putFloat( + 36, + if material.alphaMode == MaskAlphaMode: material.alphaCutoff else: -1.0'f32 + ) + result.putFloat(37, material.roughnessFactor) + result.putFloat(38, material.metallicFactor) + result.putFloat(39, material.transmissionFactor) + result.putFloat(40, material.occlusionStrength) + result.putVec3(41, vec3( + material.emissiveFactor.r, + material.emissiveFactor.g, + material.emissiveFactor.b + )) + result.putFloat(44, material.normalScale) + result[45] = ( + material.hasNormalTexture and + primitive.normals.len > 0 and + primitive.tangents.len > 0 + ).ord.uint32 + else: + let identityTransform = TextureTransform( + texCoord: 0, + offset: vec2(0, 0), + scale: vec2(1, 1), + rotation: 0.0'f32 + ) + result.putTextureTransform(0, 1, 4, 6, identityTransform) + result.putTextureTransform(7, 8, 10, 12, identityTransform) + result.putTextureTransform(13, 14, 16, 18, identityTransform) + result.putTextureTransform(19, 20, 22, 24, identityTransform) + result.putTextureTransform(25, 26, 28, 30, identityTransform) + result.putColor(32, color(1, 1, 1, 1)) + result.putFloat(36, -1.0'f32) + result.putFloat(37, 1.0'f32) + result.putFloat(38, 1.0'f32) + result.putFloat(39, 0.0'f32) + result.putFloat(40, 1.0'f32) + result.putVec3(41, vec3(0, 0, 0)) + result.putFloat(44, 1.0'f32) + result[45] = 0 + result.putVec3(48, sunLightDirection) + result.putVec3(52, rimLightDirection) + result.putVec3(56, cameraPosition) + result.putColor(60, sunLightColor) + result.putColor(64, rimLightColor) + result.putFloat(68, 3.0'f32) + result[69] = 0 + result.putFloat(70, 0.0005'f32) + result.putVec2(72, vec2(1.0'f32 / 2048.0'f32, 1.0'f32 / 2048.0'f32)) + result[74] = 0 + result.putColor(76, tint) + result.putColor(80, ambientLightColor) + +proc drawPrimitive( + renderer: Renderer, + primitive: Primitive, + owner, + root: Node, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + blendedPass: bool +) = + if primitive == nil or not primitive.hasGeometry(): + return + + let isBlend = + primitive.material != nil and + primitive.material.alphaMode == BlendAlphaMode + if isBlend != blendedPass: + return + + let dxPrimitive = renderer.ensurePrimitive(primitive) + if dxPrimitive.indexCount == 0: + return + let dxMaterial = renderer.ensureMaterial(primitive.material) + let key = PipelineKey( + topology: dxPrimitive.topologyKind, + doubleSided: primitive.material != nil and primitive.material.doubleSided, + blended: isBlend + ) + let pipeline = renderer.getPipeline(key) + renderer.ctx.commandList.setPipelineState(pipeline) + renderer.ctx.commandList.setGraphicsRootSignature(renderer.rootSignature) + + let + vertexConstants = shadyVertexConstants(owner, root, transform, view, proj) + pixelConstants = shadyPixelConstants( + primitive, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition + ) + vertexConstantsGpu = renderer.createFrameConstantBuffer(vertexConstants) + pixelConstantsGpu = renderer.createFrameConstantBuffer(pixelConstants) + renderer.ctx.commandList.setGraphicsRootConstantBufferView( + 0, + vertexConstantsGpu + ) + renderer.ctx.commandList.setGraphicsRootConstantBufferView( + 1, + pixelConstantsGpu + ) + var heaps = [dxMaterial.heap] + renderer.ctx.commandList.setDescriptorHeaps(1, addr heaps[0]) + renderer.ctx.commandList.setGraphicsRootDescriptorTable( + 2, + dxMaterial.handleGpu + ) + renderer.ctx.commandList.iaSetPrimitiveTopology(dxPrimitive.topology) + renderer.ctx.commandList.iaSetVertexBuffers( + 0, + 1, + unsafeAddr dxPrimitive.vertexBufferView + ) + renderer.ctx.commandList.iaSetIndexBuffer(unsafeAddr dxPrimitive.indexBufferView) + renderer.ctx.commandList.drawIndexedInstanced( + UINT(dxPrimitive.indexCount), + 1, + 0, + 0, + 0 + ) + +proc collectOrDrawNode( + renderer: Renderer, + node, + root: Node, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + blended: var seq[BlendEntry] +) = + if node == nil or not node.visible: + return + node.mat = transform * node.trs + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.material != nil and primitive.material.alphaMode == BlendAlphaMode: + blended.add(BlendEntry(node: node, primitive: primitive, transform: node.mat)) + else: + renderer.drawPrimitive( + primitive, + node, + root, + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blendedPass = false + ) + for child in node.nodes: + renderer.collectOrDrawNode( + child, + root, + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blended + ) + +proc drawPbrFrame*( + renderer: Renderer, + node: Node, + size: IVec2, + clearColor: Color, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + vsync = true +) = + ## Draws a full glTF PBR frame through DirectX 12. + renderer.resize(size) + for resource in renderer.frameResources: + if resource != nil: + resource.release() + renderer.frameResources.setLen(0) + + if node != nil: + renderer.prepareNodeResources(node) + + renderer.ctx.commandAllocator.reset() + renderer.ctx.commandList.reset(renderer.ctx.commandAllocator, nil) + + var barrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union(Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: renderer.ctx.renderTargets[renderer.ctx.currentFrame], + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_PRESENT, + StateAfter: + if renderer.msaaEnabled: + D3D12_RESOURCE_STATE_RESOLVE_DEST + else: + D3D12_RESOURCE_STATE_RENDER_TARGET + )) + ) + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + var rtvHandle = + if renderer.msaaEnabled: + renderer.msaaRtvHandle + else: + renderer.ctx.rtvHandles[renderer.ctx.currentFrame] + renderer.ctx.commandList.rsSetViewports(1, addr renderer.ctx.viewport) + renderer.ctx.commandList.rsSetScissorRects(1, addr renderer.ctx.scissor) + renderer.ctx.commandList.omSetRenderTargets( + 1, + addr rtvHandle, + 1, + unsafeAddr renderer.dsvHandle + ) + var clear = [ + clearColor.r.FLOAT, + clearColor.g.FLOAT, + clearColor.b.FLOAT, + clearColor.a.FLOAT + ] + renderer.ctx.commandList.clearRenderTargetView( + rtvHandle, + unsafeAddr clear[0], + 0, + nil + ) + renderer.ctx.commandList.clearDepthStencilView( + renderer.dsvHandle, + D3D12_CLEAR_FLAG_DEPTH, + 1.0'f32, + 0, + 0, + nil + ) + + if node != nil and node.visible: + node.updateTransforms(transform, true) + var blended: seq[BlendEntry] + renderer.collectOrDrawNode( + node, + node, + transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blended + ) + if blended.len > 0: + blended.sort(proc(a, b: BlendEntry): int = + let + pa = (a.transform * vec4(0, 0, 0, 1)).xyz + pb = (b.transform * vec4(0, 0, 0, 1)).xyz + da = (cameraPosition - pa).lengthSq + db = (cameraPosition - pb).lengthSq + if da > db: -1 elif da < db: 1 else: 0 + ) + for entry in blended: + renderer.drawPrimitive( + entry.primitive, + entry.node, + node, + entry.transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blendedPass = true + ) + + if renderer.msaaEnabled: + var resolveBarrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union( + Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: renderer.msaaColorBuffer, + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_RENDER_TARGET, + StateAfter: D3D12_RESOURCE_STATE_RESOLVE_SOURCE + ) + ) + ) + renderer.ctx.commandList.resourceBarrier(1, addr resolveBarrier) + renderer.ctx.commandList.resolveSubresource( + renderer.ctx.renderTargets[renderer.ctx.currentFrame], + 0, + renderer.msaaColorBuffer, + 0, + DXGI_FORMAT_R8G8B8A8_UNORM + ) + resolveBarrier.data.Transition.StateBefore = + D3D12_RESOURCE_STATE_RESOLVE_SOURCE + resolveBarrier.data.Transition.StateAfter = + D3D12_RESOURCE_STATE_RENDER_TARGET + renderer.ctx.commandList.resourceBarrier(1, addr resolveBarrier) + + barrier.data.Transition.StateBefore = D3D12_RESOURCE_STATE_RESOLVE_DEST + barrier.data.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + else: + barrier.data.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET + barrier.data.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + + var dstLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: renderer.readbackBuffer, + typ: D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + data: D3D12_TEXTURE_COPY_LOCATION_UNION( + PlacedFootprint: renderer.readbackFootprint + ) + ) + var srcLocation = D3D12_TEXTURE_COPY_LOCATION( + pResource: renderer.ctx.renderTargets[renderer.ctx.currentFrame], + typ: D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + data: D3D12_TEXTURE_COPY_LOCATION_UNION(SubresourceIndex: 0) + ) + renderer.ctx.commandList.copyTextureRegion( + addr dstLocation, + 0, + 0, + 0, + addr srcLocation, + nil + ) + + barrier.data.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE + barrier.data.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT + renderer.ctx.commandList.resourceBarrier(1, addr barrier) + renderer.ctx.commandList.close() + + renderer.ctx.executeFrame(vsync) + renderer.ctx.waitForGpu() + +proc captureScreenshot*(renderer: Renderer): Image = + ## Reads the most recently rendered DirectX frame. + let + width = renderer.readbackSize.x.int + height = renderer.readbackSize.y.int + rowPitch = int(renderer.readbackFootprint.Footprint.RowPitch) + result = newImage(width, height) + var mapped: pointer + renderer.readbackBuffer.map(0, nil, addr mapped) + let base = cast[uint](mapped) + uint(renderer.readbackFootprint.Offset) + for y in 0 ..< height: + let srcRow = cast[ptr UncheckedArray[uint8]](base + uint(y * rowPitch)) + for x in 0 ..< width: + let src = x * 4 + let alpha = srcRow[src + 3] + result.data[result.dataIndex(x, y)] = rgbx( + min(srcRow[src + 0], alpha), + min(srcRow[src + 1], alpha), + min(srcRow[src + 2], alpha), + alpha + ) + renderer.readbackBuffer.unmap(0, nil) + +proc clearNode*(renderer: Renderer, node: Node) = + ## Releases DirectX resources associated with a loaded node tree. + if node == nil: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.data != nil: + primitive.data.releasePrimitive() + primitive.data = nil + if primitive.material != nil: + if primitive.material.data != nil: + primitive.material.data.releaseMaterial() + primitive.material.data = nil + for child in node.nodes: + renderer.clearNode(child) + +proc shutdown*(renderer: Renderer) = + ## Releases all DirectX resources held by the renderer. + if renderer == nil: + return + renderer.ctx.waitForGpu() + for resource in renderer.frameResources: + if resource != nil: + resource.release() + renderer.frameResources.setLen(0) + if renderer.readbackBuffer != nil: + renderer.readbackBuffer.release() + renderer.readbackBuffer = nil + if renderer.msaaColorBuffer != nil: + renderer.msaaColorBuffer.release() + renderer.msaaColorBuffer = nil + if renderer.msaaRtvHeap != nil: + renderer.msaaRtvHeap.release() + renderer.msaaRtvHeap = nil + if renderer.depthBuffer != nil: + renderer.depthBuffer.release() + renderer.depthBuffer = nil + if renderer.dsvHeap != nil: + renderer.dsvHeap.release() + renderer.dsvHeap = nil + for pso in renderer.pipelineStates.values: + if pso != nil: + pso.release() + renderer.pipelineStates.clear() + if renderer.rootSignature != nil: + renderer.rootSignature.release() + renderer.rootSignature = nil + renderer.ctx.cleanup() + +proc beginFrame*(renderer: Renderer; window: Window; size: IVec2) = + discard renderer + discard window + discard size + +proc clearScreen*(renderer: Renderer; color: ColorRGBX) = + discard renderer + discard color + +proc clearScreen*(renderer: Renderer; color: Color) = + discard renderer + discard color + +proc render*(renderer: Renderer; node: Node; params: RenderParams) = + renderer.drawPbrFrame( + node, + params.size, + params.clearColor, + params.transform, + params.view, + params.proj, + tint = params.tint, + ambientLightColor = params.ambientLightColor, + sunLightDirection = params.sunLightDirection, + sunLightColor = params.sunLightColor, + rimLightDirection = params.rimLightDirection, + rimLightColor = params.rimLightColor, + cameraPosition = params.cameraPosition, + vsync = params.vsync + ) + +proc render*(renderer: Renderer; file: GltfFile; params: RenderParams) = + if file != nil: + renderer.render(file.root, params) + +proc endFrame*(renderer: Renderer) = + discard renderer + +proc release*(renderer: Renderer; node: Node) = + renderer.clearNode(node) diff --git a/src/gltf/backends/metal/common.nim b/src/gltf/backends/metal/common.nim new file mode 100644 index 0000000..84afb20 --- /dev/null +++ b/src/gltf/backends/metal/common.nim @@ -0,0 +1,28 @@ +when defined(macosx): + import pkg/metal4 + +type + MetalTexture* = ref object + width*, height*: int + when defined(macosx): + texture*: MTLTexture + + PrimitiveData* = ref object + geometryVersion*: uint64 + vertexCount*: int + indexCount*: int + uses32BitIndices*: bool + when defined(macosx): + vertexBuffer*: MTLBuffer + indexBuffer*: MTLBuffer + + MaterialData* = ref object + materialVersion*: uint64 + baseColor*: MetalTexture + metallicRoughness*: MetalTexture + normal*: MetalTexture + occlusion*: MetalTexture + emissive*: MetalTexture + + GltfFileData* = ref object + sceneVersion*: uint64 diff --git a/src/gltf/backends/metal/renderer.nim b/src/gltf/backends/metal/renderer.nim new file mode 100644 index 0000000..124ae00 --- /dev/null +++ b/src/gltf/backends/metal/renderer.nim @@ -0,0 +1,1185 @@ +import + std/algorithm, + chroma, pixie, vmath, windy, + ../../common, ../../models, + ./common, + ../shaders as shaderSources + +when defined(macosx): + import pkg/metal4 + +const + VertexEntryPoint* = "vertexMain" + FragmentEntryPoint* = "fragmentMain" + StudioEnvSize = 8 + + PbrVertexShader* = shaderSources.PbrVertMsl + PbrFragmentShader* = shaderSources.PbrFragMsl + SkyboxVertexShader* = shaderSources.SkyboxVertMsl + SkyboxFragmentShader* = shaderSources.SkyboxFragMsl + ShadowDepthVertexShader* = shaderSources.ShadowDepthVertMsl + ShadowDepthFragmentShader* = shaderSources.ShadowDepthFragMsl + +type + MetalVertex {.packed.} = object + position: array[3, float32] + color: array[4, float32] + normal: array[3, float32] + uv: array[2, float32] + tangent: array[4, float32] + joints: array[4, uint32] + weights: array[4, float32] + uv1: array[2, float32] + + RgbaSubresource = object + width, height: int + pixels: seq[ColorRGBX] + + Std140Writer = object + data: seq[uint32] + offset: int + + Renderer* = ref object + window*: Window + size*: IVec2 + clearColor*: Color + scene*: Node + params*: RenderParams + when defined(macosx): + ctx*: MetalContext + pipelineState*: MTLRenderPipelineState + blendPipelineState*: MTLRenderPipelineState + depthState*: MTLDepthStencilState + depthReadState*: MTLDepthStencilState + sampler*: MTLSamplerState + whiteTexture*: MTLTexture + environmentTexture*: MTLTexture + depthTexture*: MTLTexture + offscreenTexture*: MTLTexture + targetWidth*: int + targetHeight*: int + + BlendEntry = object + node: Node + primitive: Primitive + transform: Mat4 + +when defined(macosx): + type + MTLVertexFormat = uint + MTLVertexStepFunction = uint + MTLVertexDescriptor = distinct NSObject + MTLVertexBufferLayoutDescriptor = distinct NSObject + MTLVertexBufferLayoutDescriptorArray = distinct NSObject + MTLVertexAttributeDescriptor = distinct NSObject + MTLVertexAttributeDescriptorArray = distinct NSObject + +const + MetalVertexBufferIndex = 2'u + +when defined(macosx): + const + MTLVertexFormatFloat2: MTLVertexFormat = 29 + MTLVertexFormatFloat3: MTLVertexFormat = 30 + MTLVertexFormatFloat4: MTLVertexFormat = 31 + MTLVertexFormatUInt4: MTLVertexFormat = 39 + MTLVertexStepFunctionPerVertex: MTLVertexStepFunction = 1 + MTLTextureTypeCube = 5'u + +when defined(macosx): + objc: + proc vertexDescriptor*( + class: typedesc[MTLVertexDescriptor] + ): MTLVertexDescriptor + proc setVertexDescriptor*( + self: MTLRenderPipelineDescriptor, + x: MTLVertexDescriptor + ) + proc layouts*( + self: MTLVertexDescriptor + ): MTLVertexBufferLayoutDescriptorArray + proc attributes*( + self: MTLVertexDescriptor + ): MTLVertexAttributeDescriptorArray + proc objectAtIndexedSubscript*( + self: MTLVertexBufferLayoutDescriptorArray, + x: uint + ): MTLVertexBufferLayoutDescriptor + proc objectAtIndexedSubscript*( + self: MTLVertexAttributeDescriptorArray, + x: uint + ): MTLVertexAttributeDescriptor + proc setStride*( + self: MTLVertexBufferLayoutDescriptor, + x: uint + ) + proc setStepFunction*( + self: MTLVertexBufferLayoutDescriptor, + x: MTLVertexStepFunction + ) + proc setFormat*( + self: MTLVertexAttributeDescriptor, + x: MTLVertexFormat + ) + proc setOffset*( + self: MTLVertexAttributeDescriptor, + x: uint + ) + proc setBufferIndex*( + self: MTLVertexAttributeDescriptor, + x: uint + ) + proc setTextureType*( + self: MTLTextureDescriptor, + x: uint + ) + proc replaceRegion*( + self: MTLTexture, + x: MTLRegion, + mipmapLevel: uint, + slice: uint, + withBytes: pointer, + bytesPerRow: uint, + bytesPerImage: uint + ) + proc setFragmentBytes*( + self: MTLRenderCommandEncoder, + x: pointer, + length: uint, + atIndex: uint + ) + proc getBytes*( + self: MTLTexture, + x: pointer, + bytesPerRow: uint, + fromRegion: MTLRegion, + mipmapLevel: uint + ) + proc waitUntilCompleted*(self: MTLCommandBuffer) + +proc clampSize(size: IVec2): IVec2 = + ivec2(max(1'i32, size.x), max(1'i32, size.y)) + +when not defined(macosx): + proc toRgbx(value: Color): ColorRGBX = + proc channel(v: float32): uint8 = + var c = v + if c < 0: + c = 0 + elif c > 1: + c = 1 + uint8(c * 255 + 0.5) + rgbx(channel(value.r), channel(value.g), channel(value.b), channel(value.a)) + +when defined(macosx): + proc toMetalColor(value: Color): MTLClearColor = + MTLClearColor( + red: value.r.float64, + green: value.g.float64, + blue: value.b.float64, + alpha: value.a.float64 + ) + +proc copyVec2(dst: var array[2, float32], src: Vec2) = + dst[0] = src.x + dst[1] = src.y + +proc copyVec3(dst: var array[3, float32], src: Vec3) = + dst[0] = src.x + dst[1] = src.y + dst[2] = src.z + +proc copyVec4(dst: var array[4, float32], src: Vec4) = + dst[0] = src.x + dst[1] = src.y + dst[2] = src.z + dst[3] = src.w + +proc copyColor(dst: var array[4, float32], src: ColorRGBX) = + dst[0] = src.r.float32 / 255.0 + dst[1] = src.g.float32 / 255.0 + dst[2] = src.b.float32 / 255.0 + dst[3] = src.a.float32 / 255.0 + +proc copyJoints(dst: var array[4, uint32], src: array[4, uint16]) = + for i in 0 ..< 4: + dst[i] = src[i].uint32 + +proc f32bits(value: float32): uint32 = + cast[uint32](value) + +proc align(value, alignment: int): int = + ((value + alignment - 1) div alignment) * alignment + +proc alignOffset(writer: var Std140Writer, alignment: int) = + writer.offset = align(writer.offset, alignment) + +proc ensureBytes(writer: var Std140Writer, byteCount: int) = + let words = (max(0, byteCount) + 3) div 4 + if writer.data.len < words: + writer.data.setLen(words) + +proc putU32(writer: var Std140Writer, value: uint32) = + writer.ensureBytes(writer.offset + 4) + writer.data[writer.offset div 4] = value + writer.offset += 4 + +proc putFloat(writer: var Std140Writer, value: float32) = + writer.putU32(value.f32bits) + +proc putInt(writer: var Std140Writer, value: int) = + writer.putU32(value.uint32) + +proc putBool(writer: var Std140Writer, value: bool) = + writer.putU32(value.ord.uint32) + +proc putVec2(writer: var Std140Writer, value: Vec2) = + writer.alignOffset(8) + writer.putFloat(value.x) + writer.putFloat(value.y) + +proc putVec3(writer: var Std140Writer, value: Vec3) = + writer.alignOffset(16) + writer.putFloat(value.x) + writer.putFloat(value.y) + writer.putFloat(value.z) + writer.ensureBytes(writer.offset + 4) + writer.offset += 4 + +proc putColor(writer: var Std140Writer, value: Color) = + writer.alignOffset(16) + writer.putFloat(value.r) + writer.putFloat(value.g) + writer.putFloat(value.b) + writer.putFloat(value.a) + +proc putMat4(writer: var Std140Writer, value: Mat4) = + writer.alignOffset(16) + for i in 0 ..< 4: + for j in 0 ..< 4: + writer.putFloat(value[i, j]) + +proc putMat4Array(writer: var Std140Writer, values: openArray[Mat4], count: int) = + writer.alignOffset(16) + for i in 0 ..< count: + if i < values.len: + writer.putMat4(values[i]) + else: + writer.ensureBytes(writer.offset + 64) + writer.offset += 64 + +proc finish(writer: var Std140Writer): seq[uint32] = + writer.ensureBytes(max(4, align(writer.offset, 4))) + writer.data + +proc downsample(src: RgbaSubresource): RgbaSubresource = + result.width = max(1, src.width div 2) + result.height = max(1, src.height div 2) + result.pixels = newSeq[ColorRGBX](result.width * result.height) + for y in 0 ..< result.height: + let + sy0 = y * src.height div result.height + sy1 = min(src.height, max(sy0 + 1, (y + 1) * src.height div result.height)) + for x in 0 ..< result.width: + let + sx0 = x * src.width div result.width + sx1 = min(src.width, max(sx0 + 1, (x + 1) * src.width div result.width)) + var r, g, b, a, count: uint32 + for sy in sy0 ..< sy1: + for sx in sx0 ..< sx1: + let pixel = src.pixels[sy * src.width + sx] + r += pixel.r.uint32 + g += pixel.g.uint32 + b += pixel.b.uint32 + a += pixel.a.uint32 + inc count + result.pixels[y * result.width + x] = rgbx( + uint8(r div count), + uint8(g div count), + uint8(b div count), + uint8(a div count) + ) + +proc buildMipChain(base: RgbaSubresource): seq[RgbaSubresource] = + result.add(base) + while result[^1].width > 1 or result[^1].height > 1: + result.add(result[^1].downsample()) + +proc studioFaceDirection(face, x, y, size: int): Vec3 = + let + u = ((x.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f + v = ((y.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f + case face + of 0: normalize(vec3(1.0'f, -v, -u)) + of 1: normalize(vec3(-1.0'f, -v, u)) + of 2: normalize(vec3(u, 1.0'f, v)) + of 3: normalize(vec3(u, -1.0'f, -v)) + of 4: normalize(vec3(u, -v, 1.0'f)) + of 5: normalize(vec3(-u, -v, -1.0'f)) + else: vec3(0, 1, 0) + +proc studioColor(dir: Vec3): ColorRGBX = + let + hemi = clamp(dir.y * 0.5'f + 0.5'f, 0.0'f, 1.0'f) + keyDir = normalize(vec3(0.35'f, 0.85'f, 0.25'f)) + fillDir = normalize(vec3(-0.45'f, 0.65'f, -0.35'f)) + key = pow(max(dot(dir, keyDir), 0.0'f), 24.0'f) + fill = pow(max(dot(dir, fillDir), 0.0'f), 8.0'f) + cool = vec3(0.18'f, 0.19'f, 0.21'f) + neutral = vec3(0.58'f, 0.6'f, 0.63'f) + sky = vec3(0.92'f, 0.94'f, 0.97'f) + var color = mix(cool, neutral, hemi) + color = mix(color, sky, hemi * hemi) + color += vec3(0.28'f, 0.27'f, 0.25'f) * key + color += vec3(0.10'f, 0.11'f, 0.12'f) * fill + rgbx( + uint8(clamp((color.x * 255.0'f).int, 0, 255)), + uint8(clamp((color.y * 255.0'f).int, 0, 255)), + uint8(clamp((color.z * 255.0'f).int, 0, 255)), + 255 + ) + +proc buildStudioCubeMips(): seq[RgbaSubresource] = + for face in 0 ..< 6: + var base = RgbaSubresource( + width: StudioEnvSize, + height: StudioEnvSize, + pixels: newSeq[ColorRGBX](StudioEnvSize * StudioEnvSize) + ) + for y in 0 ..< StudioEnvSize: + for x in 0 ..< StudioEnvSize: + base.pixels[y * StudioEnvSize + x] = + studioColor(studioFaceDirection(face, x, y, StudioEnvSize)) + for mip in base.buildMipChain(): + result.add(mip) + +proc fillVertex( + vertex: var MetalVertex, + primitive: Primitive, + index: int +) = + vertex.position.copyVec3(primitive.points[index]) + if index < primitive.colors.len: + vertex.color.copyColor(primitive.colors[index]) + else: + vertex.color = [1.0'f, 1.0, 1.0, 1.0] + if index < primitive.normals.len: + vertex.normal.copyVec3(primitive.normals[index]) + if index < primitive.uvs.len: + vertex.uv.copyVec2(primitive.uvs[index]) + if index < primitive.tangents.len: + vertex.tangent.copyVec4(primitive.tangents[index]) + if index < primitive.jointIds.len: + vertex.joints.copyJoints(primitive.jointIds[index]) + if index < primitive.jointWeights.len: + vertex.weights.copyVec4(primitive.jointWeights[index]) + if index < primitive.uvs1.len: + vertex.uv1.copyVec2(primitive.uvs1[index]) + +proc buildVertices(primitive: Primitive): seq[MetalVertex] = + if primitive.indices32.len > 0: + result.setLen(primitive.indices32.len) + for i, index in primitive.indices32: + result[i].fillVertex(primitive, index.int) + elif primitive.indices16.len > 0: + result.setLen(primitive.indices16.len) + for i, index in primitive.indices16: + result[i].fillVertex(primitive, index.int) + else: + result.setLen(primitive.points.len) + for i in 0 ..< result.len: + result[i].fillVertex(primitive, i) + +when defined(macosx): + proc createTexture( + renderer: Renderer, + width, + height: int, + usage: MTLTextureUsage + ): MTLTexture = + let descriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( + MTLPixelFormatBGRA8Unorm, + width.uint, + height.uint, + false + ) + descriptor.setUsage(usage) + result = renderer.ctx.device.newTextureWithDescriptor(descriptor) + checkNil(result, "Could not create a Metal texture") + + proc ensureTargets(renderer: Renderer, width, height: int) = + let + targetWidth = max(1, width) + targetHeight = max(1, height) + if renderer.targetWidth == targetWidth and + renderer.targetHeight == targetHeight and + not renderer.depthTexture.isNil and + not renderer.offscreenTexture.isNil: + return + + let depthDescriptor = + MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( + MTLPixelFormatDepth32Float, + targetWidth.uint, + targetHeight.uint, + false + ) + depthDescriptor.setUsage( + MTLTextureUsageRenderTarget or MTLTextureUsageShaderRead + ) + renderer.depthTexture = + renderer.ctx.device.newTextureWithDescriptor(depthDescriptor) + checkNil(renderer.depthTexture, "Could not create a Metal depth texture") + renderer.offscreenTexture = renderer.createTexture( + targetWidth, + targetHeight, + MTLTextureUsageRenderTarget + ) + renderer.targetWidth = targetWidth + renderer.targetHeight = targetHeight + + proc createWhiteTexture(renderer: Renderer): MTLTexture = + var pixel = [255'u8, 255, 255, 255] + let descriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( + MTLPixelFormatRGBA8Unorm, + 1, + 1, + false + ) + descriptor.setUsage(MTLTextureUsageShaderRead) + result = renderer.ctx.device.newTextureWithDescriptor(descriptor) + checkNil(result, "Could not create a white Metal texture") + result.replaceRegion( + MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize(width: 1, height: 1, depth: 1) + ), + 0, + pixel[0].addr, + 4 + ) + + proc setAttribute( + descriptor: MTLVertexDescriptor, + index: uint, + format: MTLVertexFormat, + offset: uint + ) = + let attribute = descriptor.attributes().objectAtIndexedSubscript(index) + attribute.setFormat(format) + attribute.setOffset(offset) + attribute.setBufferIndex(MetalVertexBufferIndex) + + proc createVertexDescriptor(): MTLVertexDescriptor = + result = MTLVertexDescriptor.vertexDescriptor() + checkNil(result, "Could not create a Metal vertex descriptor") + result.setAttribute(0, MTLVertexFormatFloat3, 0) + result.setAttribute(1, MTLVertexFormatFloat4, 12) + result.setAttribute(2, MTLVertexFormatFloat3, 28) + result.setAttribute(3, MTLVertexFormatFloat2, 40) + result.setAttribute(4, MTLVertexFormatFloat4, 48) + result.setAttribute(5, MTLVertexFormatUInt4, 64) + result.setAttribute(6, MTLVertexFormatFloat4, 80) + result.setAttribute(7, MTLVertexFormatFloat2, 96) + let layout = + result.layouts().objectAtIndexedSubscript(MetalVertexBufferIndex) + layout.setStride(sizeof(MetalVertex).uint) + layout.setStepFunction(MTLVertexStepFunctionPerVertex) + + proc createStudioCubeTexture(renderer: Renderer): MTLTexture = + let descriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( + MTLPixelFormatRGBA8Unorm, + StudioEnvSize.uint, + StudioEnvSize.uint, + true + ) + descriptor.setTextureType(MTLTextureTypeCube) + descriptor.setUsage(MTLTextureUsageShaderRead) + result = renderer.ctx.device.newTextureWithDescriptor(descriptor) + checkNil(result, "Could not create a Metal environment texture") + + let + region = MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize(width: 0, height: 0, depth: 1) + ) + mipsPerFace = 4 + mips = buildStudioCubeMips() + for face in 0 ..< 6: + for mip in 0 ..< mipsPerFace: + let subresource = mips[face * mipsPerFace + mip] + var mipRegion = region + mipRegion.size.width = subresource.width.uint + mipRegion.size.height = subresource.height.uint + result.replaceRegion( + mipRegion, + mip.uint, + face.uint, + subresource.pixels[0].addr, + (subresource.width * sizeof(ColorRGBX)).uint, + (subresource.width * subresource.height * sizeof(ColorRGBX)).uint + ) + + proc initPipeline(renderer: Renderer) = + var error: NSError + let vertexLibrary = renderer.ctx.device.newLibraryWithSource( + @PbrVertexShader, + 0.ID, + error.addr + ) + checkNSError(error, "Could not compile Metal glTF vertex shader") + checkNil(vertexLibrary, "Metal glTF vertex shader library was nil") + error = 0.NSError + let fragmentLibrary = renderer.ctx.device.newLibraryWithSource( + @PbrFragmentShader, + 0.ID, + error.addr + ) + checkNSError(error, "Could not compile Metal glTF fragment shader") + checkNil(fragmentLibrary, "Metal glTF fragment shader library was nil") + + let vertexFunction = vertexLibrary.newFunctionWithName(@VertexEntryPoint) + checkNil(vertexFunction, "Could not load Metal vertex entry point") + let fragmentFunction = + fragmentLibrary.newFunctionWithName(@FragmentEntryPoint) + checkNil(fragmentFunction, "Could not load Metal fragment entry point") + + let pipelineDescriptor = MTLRenderPipelineDescriptor.alloc().init() + checkNil(pipelineDescriptor, "Could not create a Metal pipeline descriptor") + let colorAttachment = + pipelineDescriptor.colorAttachments().objectAtIndexedSubscript(0) + pipelineDescriptor.setVertexFunction(vertexFunction) + pipelineDescriptor.setFragmentFunction(fragmentFunction) + pipelineDescriptor.setVertexDescriptor(createVertexDescriptor()) + colorAttachment.setPixelFormat(MTLPixelFormatBGRA8Unorm) + pipelineDescriptor.setDepthAttachmentPixelFormat(MTLPixelFormatDepth32Float) + renderer.pipelineState = + renderer.ctx.device.newRenderPipelineStateWithDescriptor( + pipelineDescriptor, + error.addr + ) + checkNSError(error, "Could not create Metal glTF pipeline") + checkNil(renderer.pipelineState, "Metal glTF pipeline state was nil") + error = 0.NSError + colorAttachment.setBlendingEnabled(true) + colorAttachment.setSourceRGBBlendFactor(MTLBlendFactorSourceAlpha) + colorAttachment.setDestinationRGBBlendFactor( + MTLBlendFactorOneMinusSourceAlpha + ) + colorAttachment.setRgbBlendOperation(MTLBlendOperationAdd) + colorAttachment.setSourceAlphaBlendFactor(MTLBlendFactorOne) + colorAttachment.setDestinationAlphaBlendFactor( + MTLBlendFactorOneMinusSourceAlpha + ) + colorAttachment.setAlphaBlendOperation(MTLBlendOperationAdd) + renderer.blendPipelineState = + renderer.ctx.device.newRenderPipelineStateWithDescriptor( + pipelineDescriptor, + error.addr + ) + checkNSError(error, "Could not create Metal blended glTF pipeline") + checkNil( + renderer.blendPipelineState, + "Metal blended glTF pipeline state was nil" + ) + + let depthDescriptor = MTLDepthStencilDescriptor.alloc().init() + checkNil(depthDescriptor, "Could not create a Metal depth descriptor") + depthDescriptor.setDepthCompareFunction(MTLCompareFunctionLess) + depthDescriptor.setDepthWriteEnabled(true) + renderer.depthState = + renderer.ctx.device.newDepthStencilStateWithDescriptor(depthDescriptor) + checkNil(renderer.depthState, "Could not create a Metal depth state") + depthDescriptor.setDepthWriteEnabled(false) + renderer.depthReadState = + renderer.ctx.device.newDepthStencilStateWithDescriptor(depthDescriptor) + checkNil( + renderer.depthReadState, + "Could not create a Metal depth read state" + ) + + let samplerDescriptor = MTLSamplerDescriptor.alloc().init() + checkNil(samplerDescriptor, "Could not create a Metal sampler descriptor") + samplerDescriptor.setMinFilter(MTLSamplerMinMagFilterLinear) + samplerDescriptor.setMagFilter(MTLSamplerMinMagFilterLinear) + samplerDescriptor.setMipFilter(MTLSamplerMipFilterLinear) + samplerDescriptor.setSAddressMode(MTLSamplerAddressModeRepeat) + samplerDescriptor.setTAddressMode(MTLSamplerAddressModeRepeat) + renderer.sampler = + renderer.ctx.device.newSamplerStateWithDescriptor(samplerDescriptor) + checkNil(renderer.sampler, "Could not create a Metal sampler state") + renderer.whiteTexture = renderer.createWhiteTexture() + renderer.environmentTexture = renderer.createStudioCubeTexture() + + proc uploadBuffer[T]( + renderer: Renderer, + values: openArray[T] + ): MTLBuffer = + if values.len == 0: + return 0.MTLBuffer + result = renderer.ctx.device.newBufferWithBytes( + unsafeAddr values[0], + uint(values.len * sizeof(T)), + 0 + ) + checkNil(result, "Could not create a Metal buffer") + + proc uploadImage(renderer: Renderer, image: Image): MetalTexture = + if image == nil or image.width <= 0 or image.height <= 0: + return nil + + result = MetalTexture(width: image.width, height: image.height) + let descriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( + MTLPixelFormatRGBA8Unorm, + image.width.uint, + image.height.uint, + false + ) + descriptor.setUsage(MTLTextureUsageShaderRead) + result.texture = renderer.ctx.device.newTextureWithDescriptor(descriptor) + checkNil(result.texture, "Could not create a Metal texture") + result.texture.replaceRegion( + MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize( + width: image.width.uint, + height: image.height.uint, + depth: 1 + ) + ), + 0, + unsafeAddr image.data[0], + uint(image.width * 4) + ) + +proc ensurePrimitive(renderer: Renderer, primitive: Primitive) = + if primitive == nil: + return + if primitive.data == nil: + primitive.data = PrimitiveData() + when defined(macosx): + if primitive.data.geometryVersion == primitive.geometryVersion and + not primitive.data.vertexBuffer.isNil: + return + else: + if primitive.data.geometryVersion == primitive.geometryVersion: + return + + let vertices = primitive.buildVertices() + primitive.data.vertexCount = vertices.len + primitive.data.uses32BitIndices = primitive.indices32.len > 0 + primitive.data.indexCount = + if primitive.indices32.len > 0: + primitive.indices32.len + else: + primitive.indices16.len + primitive.data.geometryVersion = primitive.geometryVersion + + when defined(macosx): + primitive.data.vertexBuffer = renderer.uploadBuffer(vertices) + if primitive.indices32.len > 0: + primitive.data.indexBuffer = renderer.uploadBuffer(primitive.indices32) + else: + primitive.data.indexBuffer = renderer.uploadBuffer(primitive.indices16) + else: + discard renderer + +proc ensureMaterial(renderer: Renderer, material: Material) = + if material == nil: + return + if material.data == nil: + material.data = MaterialData() + when defined(macosx): + if material.data.materialVersion == material.materialVersion and + material.data.baseColor != nil: + return + else: + if material.data.materialVersion == material.materialVersion: + return + + material.data.materialVersion = material.materialVersion + when defined(macosx): + material.data.baseColor = renderer.uploadImage(material.baseColor) + material.data.metallicRoughness = + renderer.uploadImage(material.metallicRoughness) + material.data.normal = renderer.uploadImage(material.normal) + material.data.occlusion = renderer.uploadImage(material.occlusion) + material.data.emissive = renderer.uploadImage(material.emissive) + else: + discard renderer + +proc prepareNode(renderer: Renderer, node: Node) = + if node == nil: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + renderer.ensurePrimitive(primitive) + renderer.ensureMaterial(primitive.material) + for child in node.nodes: + renderer.prepareNode(child) + +proc putTextureTransform( + writer: var Std140Writer, + transform: TextureTransform +) = + writer.putInt(transform.texCoord) + writer.putVec2(transform.offset) + writer.putVec2(transform.scale) + writer.putFloat(transform.rotation) + +proc shadyVertexConstants( + owner, + root: Node, + transform, + view, + proj: Mat4 +): seq[uint32] = + var writer: Std140Writer + let jointMatrices = root.skinMatrices(owner) + writer.putBool(jointMatrices.len > 0) + writer.putMat4Array(jointMatrices, 128) + writer.putMat4(transform) + writer.putMat4(mat4()) + writer.putMat4(proj) + writer.putMat4(view) + writer.finish() + +proc shadyFragmentConstants( + primitive: Primitive, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + debugView: DebugView, + cameraPosition: Vec3 +): seq[uint32] = + var writer: Std140Writer + let material = primitive.material + if material != nil: + writer.putTextureTransform(material.baseColorTransform) + writer.putTextureTransform(material.metallicRoughnessTransform) + writer.putTextureTransform(material.normalTransform) + writer.putTextureTransform(material.occlusionTransform) + writer.putTextureTransform(material.emissiveTransform) + writer.putColor(material.baseColorFactor) + writer.putFloat( + if material.alphaMode == MaskAlphaMode: material.alphaCutoff else: -1.0'f + ) + writer.putFloat(material.roughnessFactor) + writer.putFloat(material.metallicFactor) + writer.putFloat(material.transmissionFactor) + writer.putFloat(material.occlusionStrength) + writer.putVec3(vec3( + material.emissiveFactor.r, + material.emissiveFactor.g, + material.emissiveFactor.b + )) + writer.putFloat(material.normalScale) + writer.putBool( + material.hasNormalTexture and + primitive.normals.len > 0 and + primitive.tangents.len > 0 + ) + else: + let identityTransform = TextureTransform( + texCoord: 0, + offset: vec2(0, 0), + scale: vec2(1, 1), + rotation: 0.0'f + ) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putColor(color(1, 1, 1, 1)) + writer.putFloat(-1.0'f) + writer.putFloat(1.0'f) + writer.putFloat(1.0'f) + writer.putFloat(0.0'f) + writer.putFloat(1.0'f) + writer.putVec3(vec3(0, 0, 0)) + writer.putFloat(1.0'f) + writer.putBool(false) + + writer.putVec3(sunLightDirection) + writer.putVec3(rimLightDirection) + writer.putVec3(cameraPosition) + writer.putColor(sunLightColor) + writer.putColor(rimLightColor) + writer.putFloat(3.0'f) + writer.putBool(false) + writer.putFloat(0.0005'f) + writer.putVec2(vec2(1.0'f / 2048.0'f, 1.0'f / 2048.0'f)) + writer.putInt(debugView.int) + writer.putColor(tint) + writer.putColor(ambientLightColor) + writer.finish() + +when defined(macosx): + proc metalPrimitive(mode: PrimitiveMode): MTLPrimitiveType = + case mode + of PointsMode: + MTLPrimitiveTypePoint + of LinesMode, LineLoopMode: + MTLPrimitiveTypeLine + of LineStripMode: + MTLPrimitiveTypeLineStrip + of TrianglesMode, TriangleFanMode: + MTLPrimitiveTypeTriangle + of TriangleStripMode: + MTLPrimitiveTypeTriangleStrip + + proc renderPrimitive( + renderer: Renderer, + encoder: MTLRenderCommandEncoder, + primitive: Primitive, + owner, + root: Node, + transform: Mat4, + params: RenderParams, + deferBlend: bool, + blended: var seq[BlendEntry] + ) = + if primitive == nil: + return + let isBlend = + primitive.material != nil and + primitive.material.alphaMode == BlendAlphaMode + if deferBlend and isBlend: + blended.add( + BlendEntry( + node: owner, + primitive: primitive, + transform: transform + ) + ) + return + renderer.ensurePrimitive(primitive) + renderer.ensureMaterial(primitive.material) + + let data = primitive.data + if data == nil or data.vertexCount == 0 or data.vertexBuffer.isNil: + return + if primitive.mode in {LineLoopMode, TriangleFanMode}: + return + + if isBlend: + encoder.setRenderPipelineState(renderer.blendPipelineState) + else: + encoder.setRenderPipelineState(renderer.pipelineState) + + var baseColor = renderer.whiteTexture + var metallicRoughness = renderer.whiteTexture + var normal = renderer.whiteTexture + var occlusion = renderer.whiteTexture + var emissive = renderer.whiteTexture + if primitive.material != nil and + primitive.material.data != nil and + primitive.material.data.baseColor != nil and + not primitive.material.data.baseColor.texture.isNil: + baseColor = primitive.material.data.baseColor.texture + if primitive.material != nil and + primitive.material.data != nil and + primitive.material.data.metallicRoughness != nil and + not primitive.material.data.metallicRoughness.texture.isNil: + metallicRoughness = primitive.material.data.metallicRoughness.texture + if primitive.material != nil and + primitive.material.data != nil and + primitive.material.data.normal != nil and + not primitive.material.data.normal.texture.isNil: + normal = primitive.material.data.normal.texture + if primitive.material != nil and + primitive.material.data != nil and + primitive.material.data.occlusion != nil and + not primitive.material.data.occlusion.texture.isNil: + occlusion = primitive.material.data.occlusion.texture + if primitive.material != nil and + primitive.material.data != nil and + primitive.material.data.emissive != nil and + not primitive.material.data.emissive.texture.isNil: + emissive = primitive.material.data.emissive.texture + + var vertexConstants = shadyVertexConstants( + owner, + root, + transform, + params.view, + params.proj + ) + var fragmentConstants = shadyFragmentConstants( + primitive, + params.tint, + params.ambientLightColor, + params.sunLightDirection, + params.sunLightColor, + params.rimLightDirection, + params.rimLightColor, + params.debugView, + params.cameraPosition + ) + encoder.setVertexBuffer(data.vertexBuffer, 0, MetalVertexBufferIndex) + encoder.setVertexBytes( + vertexConstants[0].addr, + (vertexConstants.len * sizeof(uint32)).uint, + 0 + ) + encoder.setFragmentBytes( + fragmentConstants[0].addr, + (fragmentConstants.len * sizeof(uint32)).uint, + 1 + ) + encoder.setFragmentTexture(baseColor, 0) + encoder.setFragmentTexture(metallicRoughness, 1) + encoder.setFragmentTexture(occlusion, 2) + encoder.setFragmentTexture(emissive, 3) + encoder.setFragmentTexture(normal, 4) + encoder.setFragmentTexture(renderer.environmentTexture, 5) + encoder.setFragmentTexture(renderer.depthTexture, 6) + for i in 0 .. 6: + encoder.setFragmentSamplerState(renderer.sampler, i.uint) + if primitive.material != nil and primitive.material.doubleSided: + encoder.setCullMode(MTLCullModeNone) + else: + encoder.setCullMode(MTLCullModeBack) + encoder.drawPrimitives( + primitive.mode.metalPrimitive(), + 0, + data.vertexCount.uint + ) + + proc renderNode( + renderer: Renderer, + encoder: MTLRenderCommandEncoder, + node: Node, + params: RenderParams, + deferBlend: bool, + blended: var seq[BlendEntry] + ) = + if node == nil or not node.visible: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + renderer.renderPrimitive( + encoder, + primitive, + node, + renderer.scene, + node.mat, + params, + deferBlend, + blended + ) + for child in node.nodes: + renderer.renderNode(encoder, child, params, deferBlend, blended) + + proc encodeScene( + renderer: Renderer, + commandBuffer: MTLCommandBuffer, + colorTexture: MTLTexture, + width, + height: int, + storeDepth: bool + ) = + let renderPass = MTLRenderPassDescriptor.renderPassDescriptor() + let colorAttachment = + renderPass.colorAttachments().objectAtIndexedSubscript(0) + colorAttachment.setTexture(colorTexture) + colorAttachment.setLoadAction(MTLLoadActionClear) + colorAttachment.setStoreAction(MTLStoreActionStore) + colorAttachment.setClearColor(renderer.clearColor.toMetalColor()) + + let depthAttachment = renderPass.depthAttachment() + depthAttachment.setTexture(renderer.depthTexture) + depthAttachment.setLoadAction(MTLLoadActionClear) + depthAttachment.setStoreAction( + if storeDepth: + MTLStoreActionStore + else: + MTLStoreActionDontCare + ) + depthAttachment.setClearDepth(1.0) + + let encoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPass) + checkNil(encoder, "Could not create a Metal render encoder") + encoder.setRenderPipelineState(renderer.pipelineState) + encoder.setViewport( + MTLViewport( + originX: 0, + originY: 0, + width: width.float64, + height: height.float64, + znear: 0, + zfar: 1 + ) + ) + encoder.setCullMode(MTLCullModeBack) + encoder.setFrontFacingWinding(MTLWindingCounterClockwise) + encoder.setDepthStencilState(renderer.depthState) + if renderer.scene != nil: + renderer.scene.updateTransforms( + renderer.params.transform, + renderer.params.useTrs + ) + var blended: seq[BlendEntry] + renderer.renderNode( + encoder, + renderer.scene, + renderer.params, + deferBlend = true, + blended = blended + ) + if blended.len > 0: + encoder.setDepthStencilState(renderer.depthReadState) + var sorted = blended + sorted.sort(proc(a, b: BlendEntry): int = + let + pa = (a.transform * vec4(0, 0, 0, 1)).xyz + pb = (b.transform * vec4(0, 0, 0, 1)).xyz + da = (renderer.params.cameraPosition - pa).lengthSq + db = (renderer.params.cameraPosition - pb).lengthSq + if da > db: + -1 + elif da < db: + 1 + else: + 0 + ) + for entry in sorted: + var dummy: seq[BlendEntry] + renderer.renderPrimitive( + encoder, + entry.primitive, + entry.node, + renderer.scene, + entry.transform, + renderer.params, + deferBlend = false, + blended = dummy + ) + encoder.setDepthStencilState(renderer.depthState) + encoder.endEncoding() + +proc newRenderer*(window: Window): Renderer = + result = Renderer( + window: window, + size: clampSize(window.size), + clearColor: color(0, 0, 0, 1) + ) + when defined(macosx): + result.ctx = newMetalContext(window) + result.initPipeline() + +proc beginFrame*(renderer: Renderer; window: Window; size: IVec2) = + renderer.window = window + renderer.size = clampSize(size) + when defined(macosx): + renderer.ctx.window = window + renderer.ctx.updateDrawableSize() + +proc clearScreen*(renderer: Renderer; color: ColorRGBX) = + renderer.clearColor = color.color + +proc clearScreen*(renderer: Renderer; color: Color) = + renderer.clearColor = color + +proc render*(renderer: Renderer; node: Node; params: RenderParams) = + if node != nil: + renderer.prepareNode(node) + renderer.scene = node + renderer.params = params + +proc render*(renderer: Renderer; file: GltfFile; params: RenderParams) = + if file != nil: + if file.data == nil: + file.data = GltfFileData() + file.data.sceneVersion = file.sceneVersion + renderer.render(file.root, params) + +proc endFrame*(renderer: Renderer) = + when defined(macosx): + renderer.ctx.window = renderer.window + let drawable = renderer.ctx.currentDrawable() + if drawable.isNil: + return + + let + drawableSize = renderer.ctx.layer.drawableSize() + width = max(1, drawableSize.width.int) + height = max(1, drawableSize.height.int) + renderer.ensureTargets(width, height) + let + commandBuffer = renderer.ctx.newCommandBuffer() + texture = drawable.texture() + renderer.encodeScene(commandBuffer, texture, width, height, false) + commandBuffer.presentDrawable(drawable) + commandBuffer.commit() + else: + discard renderer + +proc captureScreenshot*(renderer: Renderer): Image = + when defined(macosx): + let + width = max(1, renderer.size.x.int) + height = max(1, renderer.size.y.int) + sampleScale = 4 + renderWidth = width * sampleScale + renderHeight = height * sampleScale + renderer.ensureTargets(renderWidth, renderHeight) + let commandBuffer = renderer.ctx.newCommandBuffer() + renderer.encodeScene( + commandBuffer, + renderer.offscreenTexture, + renderWidth, + renderHeight, + true + ) + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + + var pixels = newSeq[uint8](renderWidth * renderHeight * 4) + renderer.offscreenTexture.getBytes( + pixels[0].addr, + (renderWidth * 4).uint, + MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize( + width: renderWidth.uint, + height: renderHeight.uint, + depth: 1 + ) + ), + 0 + ) + + let supersampled = newImage(renderWidth, renderHeight) + for y in 0 ..< renderHeight: + for x in 0 ..< renderWidth: + let i = (y * renderWidth + x) * 4 + supersampled[x, y] = rgbx( + pixels[i + 2], + pixels[i + 1], + pixels[i], + pixels[i + 3] + ) + result = supersampled.resize(width, height) + else: + result = newImage(renderer.size.x.int, renderer.size.y.int) + result.fill(renderer.clearColor.toRgbx()) + +proc release*(renderer: Renderer; node: Node) = + discard renderer + if node == nil: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + primitive.data = nil + if primitive.material != nil: + primitive.material.data = nil + for child in node.nodes: + renderer.release(child) + +proc shutdown*(renderer: Renderer) = + discard renderer diff --git a/src/gltf/backends/opengl/common.nim b/src/gltf/backends/opengl/common.nim new file mode 100644 index 0000000..0dc4120 --- /dev/null +++ b/src/gltf/backends/opengl/common.nim @@ -0,0 +1,27 @@ +import opengl + +type + PrimitiveData* = ref object + uploaded*: bool + geometryVersion*: uint64 + vertexArrayId*: GLuint + pointsId*: GLuint + uvsId*: GLuint + uvs1Id*: GLuint + normalsId*: GLuint + tangentsId*: GLuint + colorsId*: GLuint + jointIdsId*: GLuint + jointWeightsId*: GLuint + indicesId*: GLuint + + MaterialData* = ref object + materialVersion*: uint64 + baseColorId*: GLuint + metallicRoughnessId*: GLuint + normalId*: GLuint + occlusionId*: GLuint + emissiveId*: GLuint + + GltfFileData* = ref object + sceneVersion*: uint64 diff --git a/src/gltf/backends/opengl/renderer.nim b/src/gltf/backends/opengl/renderer.nim new file mode 100644 index 0000000..134d81c --- /dev/null +++ b/src/gltf/backends/opengl/renderer.nim @@ -0,0 +1,1531 @@ +## This file handles rendering for PBR. + +import + std/[strutils, algorithm, math], + opengl, windy, pixie, vmath, + ../../common, ../../models, ../../shaders, ../../ktx2, + ./common as openglCommon, + ../shaders as shaderSources + +const + VertexEntryPoint* = "main" + FragmentEntryPoint* = "main" + + envMapSize* = 512 # Size of the environment map. + StudioEnvSize = 8 + + PbrVertexShader* = shaderSources.PbrVertSrc + PbrFragmentShader* = shaderSources.PbrFragSrc + SkyboxVertexShader* = shaderSources.SkyboxVertSrc + SkyboxFragmentShader* = shaderSources.SkyboxFragSrc + ShadowDepthVertexShader* = shaderSources.ShadowDepthVertSrc + ShadowDepthFragmentShader* = shaderSources.ShadowDepthFragSrc + +var + envMapFBO*: GLuint # Framebuffer object for environment map. + environmentMapId*: GLuint # Texture ID for environment map. + rboDepth*: GLuint # Renderbuffer object for depth buffer. + + pbrShader*: GLuint # Shader program for PBR rendering. + skyboxShader*: GLuint # Shader program for skybox rendering. + skyboxVao*: GLuint # VAO for skybox rendering. + skyboxVbo*: GLuint # VBO for skybox rendering. + + shadowMapFbo*: GLuint + shadowMapTex*: GLuint + shadowDepthShader*: GLuint + environmentMipCount*: float32 + +const + ShadowMapSize = 2048 + +proc mipCountForSize(size: int): float32 = + if size <= 1: + 0.0'f32 + else: + floor(log2(size.float32)) + +proc updateEnvironmentMipUniform() = + if pbrShader == 0: + return + glUseProgram(pbrShader) + glUniform1f( + glGetUniformLocation(pbrShader, "environmentMipCount"), + environmentMipCount + ) + glUseProgram(0) + +proc setupPbr*() = + ## Sets up the PBR rendering system. + ## * Create Environment Map and Framebuffer. + + # Reduce visible seams when sampling blurred cubemap mip levels. + glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS) + + glGenTextures(1, addr environmentMapId) + glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) + + for i in 0 ..< 6: + glTexImage2D( + (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, + 0, + GL_RGB.GLint, + envMapSize, + envMapSize, + 0, + GL_RGB, + GL_UNSIGNED_BYTE, + nil + ) + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) + + # Set up the offscreen framebuffer. + glGenFramebuffers(1, addr envMapFBO) + glBindFramebuffer(GL_FRAMEBUFFER, envMapFBO) + + # Create a depth renderbuffer. + glGenRenderbuffers(1, addr rboDepth) + glBindRenderbuffer(GL_RENDERBUFFER, rboDepth) + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, envMapSize, envMapSize) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth) + + if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE: + echo "Framebuffer not complete!" + + glBindFramebuffer(GL_FRAMEBUFFER, 0) + + pbrShader = compileShaderFiles( + PbrVertexShader, + PbrFragmentShader + ) + + skyboxShader = compileShaderFiles( + SkyboxVertexShader, + SkyboxFragmentShader + ) + + shadowDepthShader = compileShaderFiles( + ShadowDepthVertexShader, + ShadowDepthFragmentShader + ) + + # Shadow map resources. + glGenFramebuffers(1, addr shadowMapFbo) + glGenTextures(1, addr shadowMapTex) + glBindTexture(GL_TEXTURE_2D, shadowMapTex) + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_DEPTH_COMPONENT.GLint, + ShadowMapSize, + ShadowMapSize, + 0, + GL_DEPTH_COMPONENT, + cGL_FLOAT, + nil + ) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.cint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.cint) + var borderColor = [1.0.GLfloat, 1.0, 1.0, 1.0] + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor[0].addr) + + glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFbo) + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + shadowMapTex, + 0 + ) + glDrawBuffer(GL_NONE) + glReadBuffer(GL_NONE) + glBindFramebuffer(GL_FRAMEBUFFER, 0) + + # Bind shadow map to texture unit 6 so the sampler2DShadow uniform always + # points to a valid depth texture, even when shadows are disabled. + glUseProgram(pbrShader) + glUniform1i(glGetUniformLocation(pbrShader, "shadowMap"), 6) + glUniform1f(glGetUniformLocation(pbrShader, "shadowBias"), 0.0015'f32) + glUniform2f( + glGetUniformLocation(pbrShader, "shadowMapTexelSize"), + 1.0'f32 / ShadowMapSize.float32, + 1.0'f32 / ShadowMapSize.float32 + ) + environmentMipCount = mipCountForSize(envMapSize) + glUniform1f( + glGetUniformLocation(pbrShader, "environmentMipCount"), + environmentMipCount + ) + glActiveTexture(GL_TEXTURE6) + glBindTexture(GL_TEXTURE_2D, shadowMapTex) + glUseProgram(0) + + # Full-screen triangle data. + var skyboxVertices = [ + -1.0f32, -1.0f32, + 3.0f32, -1.0f32, + -1.0f32, 3.0f32 + ] + + glGenVertexArrays(1, addr skyboxVao) + glGenBuffers(1, addr skyboxVbo) + + glBindVertexArray(skyboxVao) + glBindBuffer(GL_ARRAY_BUFFER, skyboxVbo) + glBufferData(GL_ARRAY_BUFFER, skyboxVertices.sizeof, addr skyboxVertices, GL_STATIC_DRAW) + + glEnableVertexAttribArray(0) + glVertexAttribPointer(0, 2, cGL_FLOAT, GL_FALSE, 0, nil) + + glBindVertexArray(0) + +proc loadCubeTexture(path: string): GLuint = + ## Creates a cube texture and returns its OpenGL ID. + + var textureId: GLuint + glGenTextures(1, addr(textureId)) + glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) + + let directions = [ + "px", "nx", "py", "ny", "pz", "nz" + ] + var faceSize = 1 + for i, direction in directions: + let image = readImage(path.replace("*", direction)) + if i == 0: + faceSize = max(image.width, image.height) + glTexImage2D( + (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, + 0, + GL_RGBA.GLint, + image.width.GLsizei, + image.height.GLsizei, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + image.data[0].addr + ) + + # Set texture parameters for the cube map. + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) + + # Generate mipmaps for the cube map. + glGenerateMipmap(GL_TEXTURE_CUBE_MAP) + environmentMipCount = mipCountForSize(faceSize) + updateEnvironmentMipUniform() + + return textureId + +proc createSolidCubeTexture(color: ColorRGBX): GLuint = + ## Creates a simple solid-color cube texture. + var + textureId: GLuint + pixel = [color.r, color.g, color.b, color.a] + + glGenTextures(1, textureId.addr) + glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) + + for i in 0 ..< 6: + glTexImage2D( + (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, + 0, + GL_RGBA.GLint, + 1, + 1, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + pixel[0].addr + ) + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) + environmentMipCount = 0.0'f32 + updateEnvironmentMipUniform() + textureId + +proc studioFaceDirection(face, x, y, size: int): Vec3 = + ## Returns a normalized direction for a cubemap texel. + let + u = ((x.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f + v = ((y.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f + case face + of 0: + normalize(vec3(1.0'f, -v, -u)) + of 1: + normalize(vec3(-1.0'f, -v, u)) + of 2: + normalize(vec3(u, 1.0'f, v)) + of 3: + normalize(vec3(u, -1.0'f, -v)) + of 4: + normalize(vec3(u, -v, 1.0'f)) + of 5: + normalize(vec3(-u, -v, -1.0'f)) + else: + vec3(0, 1, 0) + +proc studioColor(dir: Vec3): ColorRGBX = + ## Returns a tiny neutral studio-light sample color. + let + hemi = clamp(dir.y * 0.5'f + 0.5'f, 0.0'f, 1.0'f) + keyDir = normalize(vec3(0.35'f, 0.85'f, 0.25'f)) + fillDir = normalize(vec3(-0.45'f, 0.65'f, -0.35'f)) + key = pow(max(dot(dir, keyDir), 0.0'f), 24.0'f) + fill = pow(max(dot(dir, fillDir), 0.0'f), 8.0'f) + cool = vec3(0.18'f, 0.19'f, 0.21'f) + neutral = vec3(0.58'f, 0.6'f, 0.63'f) + sky = vec3(0.92'f, 0.94'f, 0.97'f) + var color = mix(cool, neutral, hemi) + color = mix(color, sky, hemi * hemi) + color += vec3(0.28'f, 0.27'f, 0.25'f) * key + color += vec3(0.10'f, 0.11'f, 0.12'f) * fill + rgbx( + clamp((color.x * 255.0'f).int, 0, 255).uint8, + clamp((color.y * 255.0'f).int, 0, 255).uint8, + clamp((color.z * 255.0'f).int, 0, 255).uint8, + 255 + ) + +proc createStudioCubeTexture(): GLuint = + ## Creates a tiny procedural cubemap for neutral studio lighting. + var textureId: GLuint + glGenTextures(1, textureId.addr) + glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) + + for face in 0 ..< 6: + let image = newImage(StudioEnvSize, StudioEnvSize) + for y in 0 ..< StudioEnvSize: + for x in 0 ..< StudioEnvSize: + image[x, y] = studioColor( + studioFaceDirection(face, x, y, StudioEnvSize) + ) + glTexImage2D( + (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + face).GLenum, + 0, + GL_RGBA.GLint, + image.width.GLsizei, + image.height.GLsizei, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + image.data[0].addr + ) + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri( + GL_TEXTURE_CUBE_MAP, + GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR + ) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) + glGenerateMipmap(GL_TEXTURE_CUBE_MAP) + environmentMipCount = mipCountForSize(StudioEnvSize) + updateEnvironmentMipUniform() + textureId + +proc loadEnvironmentMap*(cubeMapPath: string) = + ## Loads an environment map from a cube texture path. + if environmentMapId != 0: + glDeleteTextures(1, environmentMapId.addr) + environmentMapId = loadCubeTexture(cubeMapPath) + +proc loadSolidEnvironmentMap*(color = rgbx(180, 190, 220, 255)) = + ## Loads a solid-color cubemap. + if environmentMapId != 0: + glDeleteTextures(1, environmentMapId.addr) + environmentMapId = createSolidCubeTexture(color) + +proc loadDefaultEnvironmentMap*() = + ## Loads a small procedural studio cubemap. + if environmentMapId != 0: + glDeleteTextures(1, environmentMapId.addr) + environmentMapId = createStudioCubeTexture() + +proc drawSkybox*(view, proj: Mat4, lod: float32 = 0.0) = + ## Draws the skybox using a full-screen quad. + glUseProgram(skyboxShader) + + var + invProj = proj.inverse + invView = view.inverse + + glUniformMatrix4fv( + glGetUniformLocation(skyboxShader, "invProj"), + 1, + GL_FALSE, + cast[ptr float32](invProj.addr) + ) + glUniformMatrix4fv( + glGetUniformLocation(skyboxShader, "invView"), + 1, + GL_FALSE, + cast[ptr float32](invView.addr) + ) + + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) + glUniform1i(glGetUniformLocation(skyboxShader, "environmentMap"), 0) + glUniform1f(glGetUniformLocation(skyboxShader, "lod"), lod) + + # Draw a single triangle that covers the whole screen. + glDisable(GL_CULL_FACE) + glDisable(GL_DEPTH_TEST) + glDepthMask(GL_FALSE) + + glBindVertexArray(skyboxVao) + glDrawArrays(GL_TRIANGLES, 0, 3) + glBindVertexArray(0) + + glDepthMask(GL_TRUE) + glEnable(GL_DEPTH_TEST) + +type + Renderer* = ref object + window*: Window + + BlendEntry = object + node: Node + primitive: Primitive + transform: Mat4 + +proc glValue(filter: TextureMagFilter): GLint = + case filter + of NearestMagFilter: GL_NEAREST.GLint + of LinearMagFilter: GL_LINEAR.GLint + +proc glValue(filter: TextureMinFilter): GLint = + case filter + of NearestMinFilter: GL_NEAREST.GLint + of LinearMinFilter: GL_LINEAR.GLint + of NearestMipmapNearestMinFilter: GL_NEAREST_MIPMAP_NEAREST.GLint + of LinearMipmapNearestMinFilter: GL_LINEAR_MIPMAP_NEAREST.GLint + of NearestMipmapLinearMinFilter: GL_NEAREST_MIPMAP_LINEAR.GLint + of LinearMipmapLinearMinFilter: GL_LINEAR_MIPMAP_LINEAR.GLint + +proc glValue(wrap: TextureWrap): GLint = + case wrap + of RepeatWrap: GL_REPEAT.GLint + of ClampToEdgeWrap: GL_CLAMP_TO_EDGE.GLint + of MirroredRepeatWrap: GL_MIRRORED_REPEAT.GLint + +proc glValue(mode: PrimitiveMode): GLenum = + case mode + of PointsMode: GL_POINTS + of LinesMode: GL_LINES + of LineLoopMode: GL_LINE_LOOP + of LineStripMode: GL_LINE_STRIP + of TrianglesMode: GL_TRIANGLES + of TriangleStripMode: GL_TRIANGLE_STRIP + of TriangleFanMode: GL_TRIANGLE_FAN + +proc ensureData(primitive: Primitive): PrimitiveData = + if primitive.data == nil: + primitive.data = PrimitiveData() + primitive.data + +proc ensureData(material: Material): MaterialData = + if material.data == nil: + material.data = MaterialData() + material.data + +proc uploadTextureToGpu( + textureId: var GLuint, + image: Image, + ktx2Data: string, + sampler: TextureSampler +) = + ## Uploads a texture to OpenGL. + if ktx2Data.len > 0: + textureId = loadKtx2Texture(ktx2Data, sampler) + return + + if image == nil: + return + + glGenTextures(1, textureId.addr) + glBindTexture(GL_TEXTURE_2D, textureId) + # Opaque images upload as RGB, so NPOT widths can have non-4-byte rows. + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + if image.isOpaque(): + var data = newSeq[uint8](image.width * image.height * 3) + for i, rgbx in image.data: + data[i * 3 + 0] = rgbx.r + data[i * 3 + 1] = rgbx.g + data[i * 3 + 2] = rgbx.b + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGB.GLint, + image.width.GLint, + image.height.GLint, + 0, + GL_RGB, + GL_UNSIGNED_BYTE, + data[0].addr + ) + else: + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA.GLint, + image.width.GLint, + image.height.GLint, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + image.data[0].addr + ) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, sampler.magFilter.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, sampler.minFilter.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sampler.wrapS.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, sampler.wrapT.glValue) + glGenerateMipmap(GL_TEXTURE_2D) + glPixelStorei(GL_UNPACK_ALIGNMENT, 4) + +proc uploadMaterialToGpu(material: Material) = + if material == nil: + return + let data = material.ensureData() + if (material.baseColor != nil or material.baseColorKtx2.len > 0) and data.baseColorId == 0: + uploadTextureToGpu( + data.baseColorId, + material.baseColor, + material.baseColorKtx2, + material.baseColorSampler + ) + if (material.metallicRoughness != nil or material.metallicRoughnessKtx2.len > 0) and + data.metallicRoughnessId == 0: + uploadTextureToGpu( + data.metallicRoughnessId, + material.metallicRoughness, + material.metallicRoughnessKtx2, + material.metallicRoughnessSampler + ) + if (material.normal != nil or material.normalKtx2.len > 0) and data.normalId == 0: + uploadTextureToGpu( + data.normalId, + material.normal, + material.normalKtx2, + material.normalSampler + ) + if (material.occlusion != nil or material.occlusionKtx2.len > 0) and data.occlusionId == 0: + uploadTextureToGpu( + data.occlusionId, + material.occlusion, + material.occlusionKtx2, + material.occlusionSampler + ) + if (material.emissive != nil or material.emissiveKtx2.len > 0) and data.emissiveId == 0: + uploadTextureToGpu( + data.emissiveId, + material.emissive, + material.emissiveKtx2, + material.emissiveSampler + ) + data.materialVersion = material.materialVersion + +proc clearMaterialFromGpu(material: Material) = + if material == nil or material.data == nil: + return + let data = material.data + if data.baseColorId != 0.GLuint: + glDeleteTextures(1, data.baseColorId.addr) + data.baseColorId = 0 + if data.metallicRoughnessId != 0.GLuint: + glDeleteTextures(1, data.metallicRoughnessId.addr) + data.metallicRoughnessId = 0 + if data.normalId != 0.GLuint: + glDeleteTextures(1, data.normalId.addr) + data.normalId = 0 + if data.occlusionId != 0.GLuint: + glDeleteTextures(1, data.occlusionId.addr) + data.occlusionId = 0 + if data.emissiveId != 0.GLuint: + glDeleteTextures(1, data.emissiveId.addr) + data.emissiveId = 0 + material.data = nil + +proc clearFromGpu*(primitive: Primitive) + +proc uploadToGpu*(primitive: Primitive) = + ## Uploads primitive data to OpenGL. + if primitive == nil: + return + let data = primitive.ensureData() + if data.uploaded and data.geometryVersion == primitive.geometryVersion: + return + if data.uploaded: + primitive.clearFromGpu() + discard primitive.ensureData() + let gpu = primitive.data + + glGenVertexArrays(1, gpu.vertexArrayId.addr) + glBindVertexArray(gpu.vertexArrayId) + + if primitive.indices32.len > 0: + glGenBuffers(1, gpu.indicesId.addr) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpu.indicesId) + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + primitive.indices32.len * sizeof(uint32), + primitive.indices32[0].addr, + GL_STATIC_DRAW + ) + elif primitive.indices16.len > 0: + glGenBuffers(1, gpu.indicesId.addr) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpu.indicesId) + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + primitive.indices16.len * sizeof(uint16), + primitive.indices16[0].addr, + GL_STATIC_DRAW + ) + + if primitive.points.len > 0: + glGenBuffers(1, gpu.pointsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.pointsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.points.len * sizeof(Vec3), + primitive.points[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(0) + glVertexAttribPointer(0, 3, cGL_FLOAT, GL_FALSE, 0, nil) + + if primitive.uvs.len > 0: + glGenBuffers(1, gpu.uvsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.uvsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.uvs.len * sizeof(Vec2), + primitive.uvs[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(3) + glVertexAttribPointer(3, 2, cGL_FLOAT, GL_FALSE, 0, nil) + + if primitive.uvs1.len > 0: + glGenBuffers(1, gpu.uvs1Id.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.uvs1Id) + glBufferData( + GL_ARRAY_BUFFER, + primitive.uvs1.len * sizeof(Vec2), + primitive.uvs1[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(7) + glVertexAttribPointer(7, 2, cGL_FLOAT, GL_FALSE, 0, nil) + else: + glDisableVertexAttribArray(7) + glVertexAttrib2f(7, 0.0, 0.0) + + if primitive.normals.len > 0: + glGenBuffers(1, gpu.normalsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.normalsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.normals.len * sizeof(Vec3), + primitive.normals[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(2) + glVertexAttribPointer(2, 3, cGL_FLOAT, GL_FALSE, 0, nil) + + if primitive.tangents.len > 0: + glGenBuffers(1, gpu.tangentsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.tangentsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.tangents.len * sizeof(Vec4), + primitive.tangents[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(4) + glVertexAttribPointer(4, 4, cGL_FLOAT, GL_FALSE, 0, nil) + else: + glDisableVertexAttribArray(4) + glVertexAttrib4f(4, 1.0, 0.0, 0.0, 1.0) + + if primitive.colors.len > 0: + glGenBuffers(1, gpu.colorsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.colorsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.colors.len * sizeof(ColorRGBX), + primitive.colors[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(1) + glVertexAttribPointer(1, 4, cGL_UNSIGNED_BYTE, GL_TRUE, 0, nil) + else: + glDisableVertexAttribArray(1) + glVertexAttrib4f(1, 1.0, 1.0, 1.0, 1.0) + + if primitive.jointIds.len > 0: + glGenBuffers(1, gpu.jointIdsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.jointIdsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.jointIds.len * sizeof(JointIds), + primitive.jointIds[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(5) + glVertexAttribIPointer(5, 4, cGL_UNSIGNED_SHORT, 0, nil) + else: + glDisableVertexAttribArray(5) + glVertexAttribI4ui(5, 0, 0, 0, 0) + + if primitive.jointWeights.len > 0: + glGenBuffers(1, gpu.jointWeightsId.addr) + glBindBuffer(GL_ARRAY_BUFFER, gpu.jointWeightsId) + glBufferData( + GL_ARRAY_BUFFER, + primitive.jointWeights.len * sizeof(Vec4), + primitive.jointWeights[0].addr, + GL_STATIC_DRAW + ) + glEnableVertexAttribArray(6) + glVertexAttribPointer(6, 4, cGL_FLOAT, GL_FALSE, 0, nil) + else: + glDisableVertexAttribArray(6) + glVertexAttrib4f(6, 0.0, 0.0, 0.0, 0.0) + + uploadMaterialToGpu(primitive.material) + gpu.uploaded = true + gpu.geometryVersion = primitive.geometryVersion + +proc clearFromGpu*(primitive: Primitive) = + ## Clears primitive data from OpenGL. + if primitive == nil or primitive.data == nil: + return + let data = primitive.data + if data.uploaded: + glDeleteVertexArrays(1, data.vertexArrayId.addr) + if primitive.indices16.len > 0 or primitive.indices32.len > 0: + glDeleteBuffers(1, data.indicesId.addr) + if primitive.points.len > 0: + glDeleteBuffers(1, data.pointsId.addr) + if primitive.uvs.len > 0: + glDeleteBuffers(1, data.uvsId.addr) + if primitive.uvs1.len > 0: + glDeleteBuffers(1, data.uvs1Id.addr) + if primitive.normals.len > 0: + glDeleteBuffers(1, data.normalsId.addr) + if primitive.tangents.len > 0: + glDeleteBuffers(1, data.tangentsId.addr) + if primitive.colors.len > 0: + glDeleteBuffers(1, data.colorsId.addr) + if primitive.jointIds.len > 0: + glDeleteBuffers(1, data.jointIdsId.addr) + if primitive.jointWeights.len > 0: + glDeleteBuffers(1, data.jointWeightsId.addr) + primitive.data = nil + clearMaterialFromGpu(primitive.material) + +proc clearFromGpu*(mesh: Mesh) = + if mesh == nil: + return + for primitive in mesh.primitives: + primitive.clearFromGpu() + +proc clearFromGpu*(node: Node) = + if node == nil: + return + node.mesh.clearFromGpu() + for child in node.nodes: + child.clearFromGpu() + +proc setTextureTransformUniform(shader: GLuint, prefix: string, transform: TextureTransform) = + ## Sets UV transform uniforms for a texture input. + let + texCoordName = (prefix & "TexCoord").cstring + offsetName = (prefix & "UvOffset").cstring + scaleName = (prefix & "UvScale").cstring + rotationName = (prefix & "UvRotation").cstring + glUniform1i( + glGetUniformLocation(shader, texCoordName), + transform.texCoord.GLint + ) + glUniform2f( + glGetUniformLocation(shader, offsetName), + transform.offset.x, + transform.offset.y + ) + glUniform2f( + glGetUniformLocation(shader, scaleName), + transform.scale.x, + transform.scale.y + ) + glUniform1f( + glGetUniformLocation(shader, rotationName), + transform.rotation + ) + +proc renderPbrPrimitive( + primitive: Primitive, + transform, view, proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + debugView: DebugView, + cameraPosition: Vec3, + useShadow: bool, + lightSpace: Mat4, + shadowTex: GLuint, + deferBlend: bool, + blended: var seq[BlendEntry], + owner: Node, + root: Node +) = + if primitive == nil: + return + + let isBlend = + primitive.material != nil and + primitive.material.alphaMode == BlendAlphaMode + if deferBlend and isBlend: + blended.add(BlendEntry(node: owner, primitive: primitive, transform: transform)) + return + + glUseProgram(pbrShader) + + let + modelUniform = glGetUniformLocation(pbrShader, "model") + viewUniform = glGetUniformLocation(pbrShader, "view") + projUniform = glGetUniformLocation(pbrShader, "proj") + lightSpaceUniform = glGetUniformLocation(pbrShader, "lightSpace") + + var + modelArray = transform + viewArray = view + projArray = proj + lightSpaceArray = lightSpace + glUniformMatrix4fv(modelUniform, 1, GL_FALSE, cast[ptr float32](modelArray.addr)) + glUniformMatrix4fv(viewUniform, 1, GL_FALSE, cast[ptr float32](viewArray.addr)) + glUniformMatrix4fv(projUniform, 1, GL_FALSE, cast[ptr float32](projArray.addr)) + glUniformMatrix4fv(lightSpaceUniform, 1, GL_FALSE, cast[ptr float32](lightSpaceArray.addr)) + + let jointMatrices = root.skinMatrices(owner) + let useSkinning = jointMatrices.len > 0 + glUniform1i( + glGetUniformLocation(pbrShader, "useSkinning"), + useSkinning.ord.GLint + ) + if useSkinning: + glUniformMatrix4fv( + glGetUniformLocation(pbrShader, "jointMatrices"), + jointMatrices.len.GLsizei, + GL_FALSE, + cast[ptr float32](jointMatrices[0].addr) + ) + + primitive.uploadToGpu() + let primitiveData = primitive.data + glBindVertexArray(primitiveData.vertexArrayId) + + if primitive.material != nil: + let materialData = primitive.material.ensureData() + let useNormalTexture = + primitive.material.hasNormalTexture and + primitive.normals.len > 0 and + primitive.tangents.len > 0 + + glActiveTexture(GL_TEXTURE0) + glUniform1i(glGetUniformLocation(pbrShader, "baseColorTexture"), 0) + glBindTexture(GL_TEXTURE_2D, materialData.baseColorId) + + glUniform4f( + glGetUniformLocation(pbrShader, "baseColorFactor"), + primitive.material.baseColorFactor.r, + primitive.material.baseColorFactor.g, + primitive.material.baseColorFactor.b, + primitive.material.baseColorFactor.a + ) + setTextureTransformUniform(pbrShader, "baseColor", primitive.material.baseColorTransform) + + glActiveTexture(GL_TEXTURE1) + glUniform1i(glGetUniformLocation(pbrShader, "metallicRoughnessTexture"), 1) + glBindTexture(GL_TEXTURE_2D, materialData.metallicRoughnessId) + glUniform1f(glGetUniformLocation(pbrShader, "metallicFactor"), primitive.material.metallicFactor) + glUniform1f(glGetUniformLocation(pbrShader, "roughnessFactor"), primitive.material.roughnessFactor) + glUniform1f(glGetUniformLocation(pbrShader, "transmissionFactor"), primitive.material.transmissionFactor) + setTextureTransformUniform( + pbrShader, + "metallicRoughness", + primitive.material.metallicRoughnessTransform + ) + + glActiveTexture(GL_TEXTURE2) + glUniform1i(glGetUniformLocation(pbrShader, "normalTexture"), 2) + glBindTexture(GL_TEXTURE_2D, materialData.normalId) + glUniform1f(glGetUniformLocation(pbrShader, "normalScale"), primitive.material.normalScale) + setTextureTransformUniform(pbrShader, "normal", primitive.material.normalTransform) + glUniform1i(glGetUniformLocation(pbrShader, "useNormalTexture"), useNormalTexture.ord.GLint) + + glActiveTexture(GL_TEXTURE3) + glUniform1i(glGetUniformLocation(pbrShader, "occlusionTexture"), 3) + glBindTexture(GL_TEXTURE_2D, materialData.occlusionId) + glUniform1f(glGetUniformLocation(pbrShader, "occlusionStrength"), primitive.material.occlusionStrength) + setTextureTransformUniform(pbrShader, "occlusion", primitive.material.occlusionTransform) + + glActiveTexture(GL_TEXTURE4) + glUniform1i(glGetUniformLocation(pbrShader, "emissiveTexture"), 4) + glBindTexture(GL_TEXTURE_2D, materialData.emissiveId) + glUniform3f( + glGetUniformLocation(pbrShader, "emissiveFactor"), + primitive.material.emissiveFactor.r, + primitive.material.emissiveFactor.g, + primitive.material.emissiveFactor.b + ) + setTextureTransformUniform(pbrShader, "emissive", primitive.material.emissiveTransform) + + glActiveTexture(GL_TEXTURE5) + glUniform1i(glGetUniformLocation(pbrShader, "environmentMap"), 5) + glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) + + let activeShadowTex = + if shadowTex != 0.GLuint: + shadowTex + else: + shadowMapTex + glActiveTexture(GL_TEXTURE6) + glUniform1i(glGetUniformLocation(pbrShader, "shadowMap"), 6) + glBindTexture(GL_TEXTURE_2D, activeShadowTex) + + var cutoff = primitive.material.alphaCutoff + case primitive.material.alphaMode + of MaskAlphaMode: + glDisable(GL_BLEND) + glDepthMask(GL_TRUE) + cutoff = primitive.material.alphaCutoff + of BlendAlphaMode: + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glDepthMask(GL_FALSE) + cutoff = -1.0 + else: + glDisable(GL_BLEND) + glDepthMask(GL_TRUE) + cutoff = -1.0 + glUniform1f(glGetUniformLocation(pbrShader, "alphaCutoff"), cutoff) + + if primitive.material.doubleSided: + glDisable(GL_CULL_FACE) + else: + glEnable(GL_CULL_FACE) + else: + glBindTexture(GL_TEXTURE_2D, 0) + + glUniform4f( + glGetUniformLocation(pbrShader, "ambientLightColor"), + ambientLightColor.r, + ambientLightColor.g, + ambientLightColor.b, + ambientLightColor.a + ) + glUniform3f( + glGetUniformLocation(pbrShader, "sunLightDirection"), + sunLightDirection.x, + sunLightDirection.y, + sunLightDirection.z + ) + glUniform4f( + glGetUniformLocation(pbrShader, "sunLightColor"), + sunLightColor.r, + sunLightColor.g, + sunLightColor.b, + sunLightColor.a + ) + glUniform3f( + glGetUniformLocation(pbrShader, "rimLightDirection"), + rimLightDirection.x, + rimLightDirection.y, + rimLightDirection.z + ) + glUniform4f( + glGetUniformLocation(pbrShader, "rimLightColor"), + rimLightColor.r, + rimLightColor.g, + rimLightColor.b, + rimLightColor.a + ) + glUniform1i(glGetUniformLocation(pbrShader, "debugViewMode"), debugView.int.GLint) + glUniform3f( + glGetUniformLocation(pbrShader, "cameraPosition"), + cameraPosition.x, + cameraPosition.y, + cameraPosition.z + ) + glUniform4f(glGetUniformLocation(pbrShader, "tint"), tint.r, tint.g, tint.b, tint.a) + glUniform1i(glGetUniformLocation(pbrShader, "useShadow"), useShadow.Glint) + + let glMode = primitive.mode.glValue + if primitive.mode == PointsMode: + glPointSize(1.0) + + if primitive.indices16.len == 0 and primitive.indices32.len == 0: + glDrawArrays(glMode, 0, primitive.points.len.cint) + else: + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitiveData.indicesId) + if primitive.indices16.len > 0: + glDrawElements( + glMode, + primitive.indices16.len.GLint, + GL_UNSIGNED_SHORT, + nil + ) + elif primitive.indices32.len > 0: + glDrawElements( + glMode, + primitive.indices32.len.GLint, + GL_UNSIGNED_INT, + nil + ) + else: + raise newException(GltfError, "Invalid indices") + + glDisable(GL_BLEND) + glDepthMask(GL_TRUE) + glEnable(GL_CULL_FACE) + +proc renderPbrNode( + node: Node, + transform, view, proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + debugView: DebugView, + cameraPosition: Vec3, + useShadow: bool, + lightSpace: Mat4, + shadowTex: GLuint, + deferBlend: bool, + blended: var seq[BlendEntry], + drawChildren = true, + applyTrs = true, + root: Node = nil +) = + ## Renders a node with the PBR shader. + if not node.visible: + return + + let rootNode = + if root == nil: + node + else: + root + + node.mat = + if applyTrs: + transform * node.trs + else: + transform + + if node.mesh != nil: + for primitive in node.mesh.primitives: + renderPbrPrimitive( + primitive, + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow, + lightSpace, + shadowTex, + deferBlend, + blended, + node, + rootNode + ) + + if drawChildren: + for n in node.nodes: + n.renderPbrNode( + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow, + lightSpace, + shadowTex, + deferBlend, + blended, + drawChildren=true, + applyTrs=true, + root=rootNode + ) + +proc drawPbr*( + node: Node, + transform, view, proj: Mat4, + tint: Color, + useTrs = true, + ambientLightColor = color(0.1, 0.1, 0.1, 1), + sunLightDirection = vec3(1, 4, 2), + sunLightColor = color(1, 1, 1, 1), + rimLightDirection = vec3(-1, 1, -1), + rimLightColor = color(0, 0, 0, 0), + debugView = dvLit, + cameraPosition = vec3(0, 0, 10) +) = + ## Draws a node tree with PBR shading. + if not node.visible: + return + + node.updateTransforms(transform, useTrs) + + var blended: seq[BlendEntry] + + renderPbrNode( + node, + transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow=false, + lightSpace=mat4(), + shadowTex=0, + deferBlend=true, + blended=blended, + root=node + ) + + if blended.len > 0: + glDepthMask(GL_FALSE) + var sorted = blended + sorted.sort(proc(a, b: BlendEntry): int = + let + pa = (a.transform * vec4(0, 0, 0, 1)).xyz + pb = (b.transform * vec4(0, 0, 0, 1)).xyz + da = (cameraPosition - pa).lengthSq + db = (cameraPosition - pb).lengthSq + if da > db: -1 elif da < db: 1 else: 0 + ) + for entry in sorted: + var dummy: seq[BlendEntry] + renderPbrPrimitive( + entry.primitive, + entry.transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow=false, + lightSpace=mat4(), + shadowTex=0, + deferBlend=false, + blended=dummy, + owner=entry.node, + root=node + ) + glDepthMask(GL_TRUE) + +proc shadowLookAt(eye, center, up: Vec3): Mat4 = + ## Standard OpenGL lookAt (z-backward) for shadow mapping. + let + f = normalize(center - eye) + s = normalize(cross(f, up)) + u = cross(s, f) + result[0, 0] = s.x + result[1, 0] = s.y + result[2, 0] = s.z + result[3, 0] = -dot(s, eye) + result[0, 1] = u.x + result[1, 1] = u.y + result[2, 1] = u.z + result[3, 1] = -dot(u, eye) + result[0, 2] = -f.x + result[1, 2] = -f.y + result[2, 2] = -f.z + result[3, 2] = dot(f, eye) + result[3, 3] = 1.0'f32 + +proc getShadowMatrices(node: Node, transform: Mat4, lightDir: Vec3): (Mat4, Mat4, Mat4, Vec3) = + ## Compute light view/projection for the node tree. + let + bounds = getAABounds(node, transform) + center = bounds.center + radius = bounds.radius().float32 + dir = normalize(lightDir) + lightPos = center - dir * (radius * 2.0'f) + nearPlane = max(0.1'f, radius * 0.1'f) + farPlane = radius * 4.0'f + orthoSize = radius * 1.5'f + lightView = shadowLookAt(lightPos, center, vec3(0, 1, 0)) + lightProj = ortho( + -orthoSize, + orthoSize, + -orthoSize, + orthoSize, + nearPlane, + farPlane + ) + return (lightView, lightProj, lightProj * lightView, lightPos) + +proc renderShadowPrimitive( + primitive: Primitive, + shader: GLuint, + owner, + root: Node, + transform, view, proj: Mat4, + tint: Color +) = + if primitive == nil or not primitive.hasGeometry(): + return + if primitive.material != nil and primitive.material.alphaMode == BlendAlphaMode: + return + + primitive.uploadToGpu() + let primitiveData = primitive.data + glBindVertexArray(primitiveData.vertexArrayId) + + var + modelArray = transform + viewArray = view + projArray = proj + glUniformMatrix4fv( + glGetUniformLocation(shader, "model"), + 1, + GL_FALSE, + cast[ptr float32](modelArray.addr) + ) + glUniformMatrix4fv( + glGetUniformLocation(shader, "view"), + 1, + GL_FALSE, + cast[ptr float32](viewArray.addr) + ) + glUniformMatrix4fv( + glGetUniformLocation(shader, "proj"), + 1, + GL_FALSE, + cast[ptr float32](projArray.addr) + ) + + let jointMatrices = root.skinMatrices(owner) + let useSkinning = jointMatrices.len > 0 + let useSkinningUniform = glGetUniformLocation(shader, "useSkinning") + if useSkinningUniform >= 0: + glUniform1i(useSkinningUniform, useSkinning.ord.GLint) + if useSkinning: + let jointMatricesUniform = glGetUniformLocation(shader, "jointMatrices") + if jointMatricesUniform >= 0: + glUniformMatrix4fv( + jointMatricesUniform, + jointMatrices.len.GLsizei, + GL_FALSE, + cast[ptr float32](jointMatrices[0].addr) + ) + + let tintUniform = glGetUniformLocation(shader, "tint") + if tintUniform >= 0: + glUniform4f(tintUniform, tint.r, tint.g, tint.b, tint.a) + + let glMode = primitive.mode.glValue + if primitive.mode == PointsMode: + glPointSize(1.0) + if primitive.indices16.len == 0 and primitive.indices32.len == 0: + glDrawArrays(glMode, 0, primitive.points.len.cint) + else: + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitiveData.indicesId) + if primitive.indices16.len > 0: + glDrawElements(glMode, primitive.indices16.len.GLint, GL_UNSIGNED_SHORT, nil) + elif primitive.indices32.len > 0: + glDrawElements(glMode, primitive.indices32.len.GLint, GL_UNSIGNED_INT, nil) + +proc renderShadowNode( + node, + root: Node, + transform, view, proj: Mat4, + tint: Color, + applyTrs = true +) = + if node == nil or not node.visible: + return + node.mat = + if applyTrs: + transform * node.trs + else: + transform + if node.mesh != nil: + for primitive in node.mesh.primitives: + renderShadowPrimitive( + primitive, + shadowDepthShader, + node, + root, + node.mat, + view, + proj, + tint + ) + for child in node.nodes: + renderShadowNode(child, root, node.mat, view, proj, tint, applyTrs=true) + +proc drawPbrWithShadow*( + node: Node, + transform, view, proj: Mat4, + tint: Color, + sunLightDirection = vec3(1, 4, 2), + useTrs = true, + ambientLightColor = color(0.1, 0.1, 0.1, 1), + sunLightColor = color(1, 1, 1, 1), + rimLightDirection = vec3(-1, 1, -1), + rimLightColor = color(0, 0, 0, 0), + debugView = dvLit, + cameraPosition = vec3(0, 0, 10) +) = + ## Draws a node tree with PBR shading and shadows. + if not node.visible: + return + + node.updateTransforms(transform, useTrs) + + let (lightView, lightProj, lightSpace, _) = + getShadowMatrices(node, transform, sunLightDirection) + + # Save viewport and framebuffer. + var + oldViewport: array[4, GLint] + oldFramebuffer: GLint + glGetIntegerv(GL_VIEWPORT, oldViewport[0].addr) + glGetIntegerv(GL_FRAMEBUFFER_BINDING, oldFramebuffer.addr) + + # Depth pass. + glViewport(0, 0, ShadowMapSize, ShadowMapSize) + glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFbo) + glClear(GL_DEPTH_BUFFER_BIT) + glUseProgram(shadowDepthShader) + glEnable(GL_CULL_FACE) + glCullFace(GL_BACK) + glEnable(GL_DEPTH_TEST) + glDepthMask(GL_TRUE) + + renderShadowNode(node, node, transform, lightView, lightProj, tint, applyTrs=true) + + glBindFramebuffer(GL_FRAMEBUFFER, oldFramebuffer.GLuint) + + # Restore viewport. + glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]) + + # Main pass with shadow sampling. + var blended: seq[BlendEntry] + + renderPbrNode( + node, + transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow=true, + lightSpace=lightSpace, + shadowTex=shadowMapTex, + deferBlend=true, + blended=blended, + root=node + ) + + if blended.len > 0: + glDepthMask(GL_FALSE) + var sorted = blended + sorted.sort(proc(a, b: BlendEntry): int = + let pa = (a.transform * vec4(0, 0, 0, 1)).xyz + let pb = (b.transform * vec4(0, 0, 0, 1)).xyz + let da = (cameraPosition - pa).lengthSq + let db = (cameraPosition - pb).lengthSq + if da > db: -1 elif da < db: 1 else: 0 + ) + for entry in sorted: + var dummy: seq[BlendEntry] + renderPbrPrimitive( + entry.primitive, + entry.transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + debugView, + cameraPosition, + useShadow=false, + lightSpace=mat4(), + shadowTex=0, + deferBlend=false, + blended=dummy, + owner=entry.node, + root=node + ) + glDepthMask(GL_TRUE) + +proc newRenderer*(window: Window): Renderer = + result = Renderer(window: window) + setupPbr() + +proc beginFrame*(renderer: Renderer; window: Window; size: IVec2) = + discard renderer + discard window + glViewport(0, 0, size.x, size.y) + glCullFace(GL_BACK) + glFrontFace(GL_CCW) + glEnable(GL_CULL_FACE) + glEnable(GL_DEPTH_TEST) + glDepthMask(GL_TRUE) + +proc clearScreen*(renderer: Renderer; color: ColorRGBX) = + discard renderer + let c = color.color + glClearColor(c.r, c.g, c.b, c.a) + glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT) + +proc clearScreen*(renderer: Renderer; color: Color) = + discard renderer + glClearColor(color.r, color.g, color.b, color.a) + glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT) + +proc render*(renderer: Renderer; node: Node; params: RenderParams) = + discard renderer + if node == nil: + return + if params.drawSkybox: + drawSkybox(params.view, params.proj, params.skyboxLod) + if params.useShadows and params.debugView == dvLit: + node.drawPbrWithShadow( + params.transform, + params.view, + params.proj, + params.tint, + sunLightDirection = params.sunLightDirection, + useTrs = params.useTrs, + ambientLightColor = params.ambientLightColor, + sunLightColor = params.sunLightColor, + rimLightDirection = params.rimLightDirection, + rimLightColor = params.rimLightColor, + debugView = params.debugView, + cameraPosition = params.cameraPosition + ) + else: + node.drawPbr( + params.transform, + params.view, + params.proj, + params.tint, + useTrs = params.useTrs, + ambientLightColor = params.ambientLightColor, + sunLightDirection = params.sunLightDirection, + sunLightColor = params.sunLightColor, + rimLightDirection = params.rimLightDirection, + rimLightColor = params.rimLightColor, + debugView = params.debugView, + cameraPosition = params.cameraPosition + ) + +proc render*(renderer: Renderer; file: GltfFile; params: RenderParams) = + if file != nil: + renderer.render(file.root, params) + +proc endFrame*(renderer: Renderer) = + discard renderer + +proc captureScreenshot*(renderer: Renderer): Image = + discard renderer + var viewport: array[4, GLint] + glGetIntegerv(GL_VIEWPORT, viewport[0].addr) + let + width = max(1, viewport[2].int) + height = max(1, viewport[3].int) + var pixels = newSeq[uint8](width * height * 4) + glPixelStorei(GL_PACK_ALIGNMENT, 1) + glReadBuffer(GL_BACK) + glReadPixels( + 0, + 0, + width.GLsizei, + height.GLsizei, + GL_RGBA, + GL_UNSIGNED_BYTE, + cast[pointer](pixels[0].addr) + ) + result = newImage(width, height) + for y in 0 ..< height: + let srcY = height - 1 - y + for x in 0 ..< width: + let + src = (srcY * width + x) * 4 + dst = y * width + x + result.data[dst] = rgbx( + pixels[src + 0], + pixels[src + 1], + pixels[src + 2], + pixels[src + 3] + ) + +proc release*(renderer: Renderer; node: Node) = + discard renderer + node.clearFromGpu() + +proc shutdown*(renderer: Renderer) = + discard renderer diff --git a/src/gltf/backends/shaders.nim b/src/gltf/backends/shaders.nim new file mode 100644 index 0000000..c006b6c --- /dev/null +++ b/src/gltf/backends/shaders.nim @@ -0,0 +1,442 @@ +## Unified glTF shader sources generated through Shady. + +import + std/math, + shady, vmath + +const ShaderPi = 3.1415926535897932384626433832795'f + +var + model*: Uniform[Mat4] + view*: Uniform[Mat4] + proj*: Uniform[Mat4] + lightSpace*: Uniform[Mat4] + useSkinning*: Uniform[bool] + jointMatrices*: Uniform[array[128, Mat4]] + + invProj*: Uniform[Mat4] + invView*: Uniform[Mat4] + lod*: Uniform[float32] + + sampleTex*: Uniform[Sampler2d] + baseColorTexture*: Uniform[Sampler2d] + baseColorFactor*: Uniform[Vec4] + baseColorTexCoord*: Uniform[int] + baseColorUvOffset*: Uniform[Vec2] + baseColorUvScale*: Uniform[Vec2] + baseColorUvRotation*: Uniform[float32] + + metallicRoughnessTexture*: Uniform[Sampler2d] + metallicFactor*: Uniform[float32] + roughnessFactor*: Uniform[float32] + transmissionFactor*: Uniform[float32] + metallicRoughnessTexCoord*: Uniform[int] + metallicRoughnessUvOffset*: Uniform[Vec2] + metallicRoughnessUvScale*: Uniform[Vec2] + metallicRoughnessUvRotation*: Uniform[float32] + + normalTexture*: Uniform[Sampler2d] + normalScale*: Uniform[float32] + normalTexCoord*: Uniform[int] + normalUvOffset*: Uniform[Vec2] + normalUvScale*: Uniform[Vec2] + normalUvRotation*: Uniform[float32] + + occlusionTexture*: Uniform[Sampler2d] + occlusionStrength*: Uniform[float32] + occlusionTexCoord*: Uniform[int] + occlusionUvOffset*: Uniform[Vec2] + occlusionUvScale*: Uniform[Vec2] + occlusionUvRotation*: Uniform[float32] + + emissiveTexture*: Uniform[Sampler2d] + emissiveFactor*: Uniform[Vec3] + emissiveTexCoord*: Uniform[int] + emissiveUvOffset*: Uniform[Vec2] + emissiveUvScale*: Uniform[Vec2] + emissiveUvRotation*: Uniform[float32] + + environmentMap*: Uniform[SamplerCube] + environmentMipCount*: Uniform[float32] + shadowMap*: Uniform[Sampler2dShadow] + shadowMapTexelSize*: Uniform[Vec2] + tint*: Uniform[Vec4] + + useShadow*: Uniform[bool] + useNormalTexture*: Uniform[bool] + alphaCutoff*: Uniform[float32] + + ambientLightColor*: Uniform[Vec4] + sunLightDirection*: Uniform[Vec3] + sunLightColor*: Uniform[Vec4] + rimLightDirection*: Uniform[Vec3] + rimLightColor*: Uniform[Vec4] + cameraPosition*: Uniform[Vec3] + debugViewMode*: Uniform[int] + shadowBias*: Uniform[float32] + +func identityMat4(): Mat4 = + mat4(1.0'f) + +proc skinMatrix(vertexJoints: UVec4, vertexWeights: Vec4): Mat4 = + result = + vertexWeights.x * jointMatrices[vertexJoints.x.int] + + vertexWeights.y * jointMatrices[vertexJoints.y.int] + + vertexWeights.z * jointMatrices[vertexJoints.z.int] + + vertexWeights.w * jointMatrices[vertexJoints.w.int] + +func transformUv(uv, offset, scale: Vec2, rotation: float32): Vec2 = + let + c = cos(rotation) + s = sin(rotation) + scaled = uv * scale + result = vec2( + c * scaled.x - s * scaled.y, + s * scaled.x + c * scaled.y + ) + offset + +func selectUv(texCoord: int, uv, uv1: Vec2): Vec2 = + if texCoord == 1: + uv1 + else: + uv + +func safeNormalize(v: Vec3): Vec3 = + if length(v) > 0.0'f: + normalize(v) + else: + vec3(0.0'f, 0.0'f, 0.0'f) + +proc gltfPbrVert*( + vertexPosition: Vec3, + vertexColor: Vec4, + vertexNormal: Vec3, + vertexUV: Vec2, + vertexTangent: Vec4, + vertexJoints: UVec4, + vertexWeights: Vec4, + vertexUV1: Vec2, + gl_Position: var Vec4, + worldPos: var Vec3, + color: var Vec4, + normal: var Vec3, + uv: var Vec2, + uv1: var Vec2, + tangent: var Vec3, + bitangent: var Vec3, + vPosLightSpace: var Vec4 +) = + var skin = identityMat4() + if useSkinning: + skin = + vertexWeights.x * jointMatrices[vertexJoints.x.int] + + vertexWeights.y * jointMatrices[vertexJoints.y.int] + + vertexWeights.z * jointMatrices[vertexJoints.z.int] + + vertexWeights.w * jointMatrices[vertexJoints.w.int] + let + skinnedPosition = skin * vec4(vertexPosition, 1.0'f) + skinnedNormal = (skin * vec4(vertexNormal, 0.0'f)).xyz + skinnedTangent = (skin * vec4(vertexTangent.xyz, 0.0'f)).xyz + worldPos = (model * skinnedPosition).xyz + color = vertexColor + uv = vertexUV + uv1 = vertexUV1 + + let n: Vec3 = safeNormalize((model * vec4(skinnedNormal, 0.0'f)).xyz) + normal = n + tangent = safeNormalize((model * vec4(skinnedTangent, 0.0'f)).xyz) + bitangent = cross(n, tangent) * vertexTangent.w + vPosLightSpace = lightSpace * vec4(worldPos, 1.0'f) + gl_Position = proj * view * model * skinnedPosition + +proc gltfPbrFrag*( + worldPos: Vec3, + color: Vec4, + normal: Vec3, + uv: Vec2, + uv1: Vec2, + tangent: Vec3, + bitangent: Vec3, + vPosLightSpace: Vec4, + fragColor: var Vec4 +) = + let + baseColorUv = transformUv( + selectUv(baseColorTexCoord, uv, uv1), + baseColorUvOffset, + baseColorUvScale, + baseColorUvRotation + ) + metallicRoughnessUv = transformUv( + selectUv(metallicRoughnessTexCoord, uv, uv1), + metallicRoughnessUvOffset, + metallicRoughnessUvScale, + metallicRoughnessUvRotation + ) + normalUv = transformUv( + selectUv(normalTexCoord, uv, uv1), + normalUvOffset, + normalUvScale, + normalUvRotation + ) + occlusionUv = transformUv( + selectUv(occlusionTexCoord, uv, uv1), + occlusionUvOffset, + occlusionUvScale, + occlusionUvRotation + ) + emissiveUv = transformUv( + selectUv(emissiveTexCoord, uv, uv1), + emissiveUvOffset, + emissiveUvScale, + emissiveUvRotation + ) + + fragColor = texture(baseColorTexture, baseColorUv) + fragColor = fragColor * baseColorFactor + fragColor = fragColor * color + if fragColor.a < alphaCutoff: + discardFragment() + + let + albedo: Vec3 = fragColor.rgb + roughness = + texture(metallicRoughnessTexture, metallicRoughnessUv).g * + roughnessFactor + metallic = + texture(metallicRoughnessTexture, metallicRoughnessUv).b * + metallicFactor + transmission = clamp(transmissionFactor, 0.0'f, 1.0'f) + ambientOcclusion = + texture(occlusionTexture, occlusionUv).g * + occlusionStrength + emissiveValue: Vec3 = + texture(emissiveTexture, emissiveUv).rgb * + emissiveFactor + geometricNormal: Vec3 = normalize(cross(dFdx(worldPos), dFdy(worldPos))) + meshNormal: Vec3 = + if length(normal) > 0.0'f: + normalize(normal) + else: + geometricNormal + normalValue: Vec3 = + normalize(texture(normalTexture, normalUv).rgb * 2.0'f - + vec3(1.0'f, 1.0'f, 1.0'f)) * + normalScale + mappedNormal: Vec3 = normalize( + tangent * normalValue.x + + bitangent * normalValue.y + + normal * normalValue.z + ) + var computedNormal: Vec3 = + if useNormalTexture: + mappedNormal + else: + meshNormal + if not gl_FrontFacing: + computedNormal = -computedNormal + let + sunDir: Vec3 = normalize(-sunLightDirection) + rimDir: Vec3 = normalize(-rimLightDirection) + viewDir: Vec3 = normalize(cameraPosition - worldPos) + reflectDir: Vec3 = reflect(-viewDir, computedNormal) + halfVector: Vec3 = normalize(viewDir + sunDir) + nDotH = max(dot(computedNormal, halfVector), 0.0'f) + nDotL = max(dot(computedNormal, sunDir), 0.0'f) + f0 = vec3(0.04'f, 0.04'f, 0.04'f) + f0mix: Vec3 = mix(f0, albedo, metallic) + cosTheta = max(dot(computedNormal, viewDir), 0.0'f) + fresnel: Vec3 = f0mix + + (vec3(1.0'f, 1.0'f, 1.0'f) - f0mix) * + pow(1.0'f - cosTheta, 5.0'f) + specComponent = nDotL * pow(nDotH, 2.0'f / (roughness + 0.0001'f)) + specular: Vec3 = + sunLightColor.rgb * + sunLightColor.a * + fresnel * + specComponent + diffuse: Vec3 = + sunLightColor.rgb * + sunLightColor.a * + (1.0'f - metallic) * + albedo * + nDotL + rim = + pow(1.0'f - max(dot(computedNormal, viewDir), 0.0'f), 2.5'f) * + (1.0'f - metallic) + rimFacing = max(dot(computedNormal, rimDir), 0.0'f) + rimLight: Vec3 = + rimLightColor.rgb * + rimLightColor.a * + rim * + (0.15'f + 0.85'f * rimFacing) * + (0.15'f + 0.35'f * ambientOcclusion) + mipLevel = roughness * environmentMipCount + envColor: Vec3 = textureLod(environmentMap, reflectDir, mipLevel).rgb + var shadow = 0.0'f + if useShadow: + var projCoords = vPosLightSpace.xyz / vPosLightSpace.w + projCoords = projCoords * 0.5'f + vec3(0.5'f, 0.5'f, 0.5'f) + if projCoords.z <= 1.0'f and + projCoords.x >= 0.0'f and projCoords.x <= 1.0'f and + projCoords.y >= 0.0'f and projCoords.y <= 1.0'f: + let bias = + max(shadowBias, 0.002'f * (1.0'f - dot(computedNormal, sunDir))) + var + total = 0.0'f + weightSum = 0.0'f + for ix in 0 ..< 5: + for iy in 0 ..< 5: + let + x = ix - 2 + y = iy - 2 + wx = 1.0'f - abs(x.float32) * 0.25'f + wy = 1.0'f - abs(y.float32) * 0.25'f + w = wx * wy + offset = vec2(x.float32, y.float32) * shadowMapTexelSize + lit = texture( + shadowMap, + vec3(projCoords.xy + offset, projCoords.z - bias) + ) + total += w * lit + weightSum += w + shadow = 1.0'f - total / weightSum + + if debugViewMode == 1: + fragColor = vec4(albedo * tint.rgb, fragColor.a * tint.a) + elif debugViewMode == 2: + fragColor = vec4(computedNormal * 0.5'f + + vec3(0.5'f, 0.5'f, 0.5'f), fragColor.a) + elif debugViewMode == 3: + fragColor = vec4( + ambientOcclusion, + ambientOcclusion, + ambientOcclusion, + fragColor.a + ) + elif debugViewMode == 4: + fragColor = vec4(metallic, metallic, metallic, fragColor.a) + elif debugViewMode == 5: + let specularMap = + clamp( + dot(f0mix, vec3(0.3333333'f, 0.3333333'f, 0.3333333'f)) * + (1.0'f - roughness * 0.5'f), + 0.0'f, + 1.0'f + ) + fragColor = vec4(specularMap, specularMap, specularMap, fragColor.a) + else: + let + direct: Vec3 = (specular + diffuse) * (1.0'f - shadow) + lo: Vec3 = + direct + + ambientLightColor.rgb * ambientLightColor.a * ambientOcclusion + + rimLight + var litColor: Vec3 = mix(lo, envColor, fresnel * metallic) + + if transmission > 0.0'f: + let glassMix = + clamp( + 0.35'f + transmission * 0.55'f + roughness * 0.1'f, + 0.0'f, + 1.0'f + ) + litColor = mix(litColor, envColor, glassMix) + litColor = litColor * mix(1.0'f, 0.82'f, transmission) + fragColor.a = + fragColor.a * mix(1.0'f, 0.08'f + roughness * 0.2'f, transmission) + + litColor += emissiveValue + fragColor = vec4(litColor, fragColor.a) * tint + +proc gltfSkyboxVert*( + vertexPosition: Vec2, + gl_Position: var Vec4, + rayDir: var Vec3 +) = + gl_Position = vec4(vertexPosition.x, vertexPosition.y, 1.0'f, 1.0'f) + let far = invProj * + vec4(vertexPosition.x, vertexPosition.y, 1.0'f, 1.0'f) + rayDir = mat3(invView) * far.xyz + +proc gltfSkyboxFrag*(rayDir: Vec3, fragColor: var Vec4) = + let + dir: Vec3 = normalize(rayDir) + up = + if abs(dir.z) < 0.999'f: + vec3(0.0'f, 0.0'f, 1.0'f) + else: + vec3(1.0'f, 0.0'f, 0.0'f) + tangent: Vec3 = normalize(cross(up, dir)) + bitangent: Vec3 = cross(dir, tangent) + spread = lod * 0.015'f + var colorValue = vec4(0.0'f, 0.0'f, 0.0'f, 0.0'f) + for i in 0 ..< 16: + let + t = i.float32 / 16.0'f + angle = t * 2.0'f * ShaderPi + r = sqrt(t) + offset = vec2(cos(angle), sin(angle)) * r * spread + sampleDir: Vec3 = + normalize(dir + tangent * offset.x + bitangent * offset.y) + colorValue += textureLod(environmentMap, sampleDir, lod) + fragColor = colorValue / 16.0'f + +proc gltfShadowDepthVert*( + vertexPosition: Vec3, + vertexUV: Vec2, + vertexJoints: UVec4, + vertexWeights: Vec4, + gl_Position: var Vec4, + fragmentUv: var Vec2 +) = + fragmentUv = vertexUV + let skin = + if useSkinning: + skinMatrix(vertexJoints, vertexWeights) + else: + identityMat4() + gl_Position = proj * view * model * skin * vec4(vertexPosition, 1.0'f) + +proc gltfShadowDepthFrag*(fragmentUv: Vec2) = + if alphaCutoff >= 0.0'f: + let texColor = texture(sampleTex, fragmentUv) * baseColorFactor + if texColor.a < alphaCutoff: + discardFragment() + +const + PbrVertSrc* = toShader(gltfPbrVert, glsl4Desktop, shaderVertex) + PbrFragSrc* = toShader(gltfPbrFrag, glsl4Desktop, shaderFragment) + SkyboxVertSrc* = toShader(gltfSkyboxVert, glsl4Desktop, shaderVertex) + SkyboxFragSrc* = toShader(gltfSkyboxFrag, glsl4Desktop, shaderFragment) + ShadowDepthVertSrc* = + toShader(gltfShadowDepthVert, glsl4Desktop, shaderVertex) + ShadowDepthFragSrc* = + toShader(gltfShadowDepthFrag, glsl4Desktop, shaderFragment) + + PbrVertHlsl* = toShader(gltfPbrVert, hlslDX12, shaderVertex) + PbrFragHlsl* = toShader(gltfPbrFrag, hlslDX12, shaderFragment) + SkyboxVertHlsl* = toShader(gltfSkyboxVert, hlslDX12, shaderVertex) + SkyboxFragHlsl* = toShader(gltfSkyboxFrag, hlslDX12, shaderFragment) + ShadowDepthVertHlsl* = + toShader(gltfShadowDepthVert, hlslDX12, shaderVertex) + ShadowDepthFragHlsl* = + toShader(gltfShadowDepthFrag, hlslDX12, shaderFragment) + + PbrVertVulkan* = toShader(gltfPbrVert, vulkanGlsl450, shaderVertex) + PbrFragVulkan* = toShader(gltfPbrFrag, vulkanGlsl450, shaderFragment) + SkyboxVertVulkan* = toShader(gltfSkyboxVert, vulkanGlsl450, shaderVertex) + SkyboxFragVulkan* = toShader(gltfSkyboxFrag, vulkanGlsl450, shaderFragment) + ShadowDepthVertVulkan* = + toShader(gltfShadowDepthVert, vulkanGlsl450, shaderVertex) + ShadowDepthFragVulkan* = + toShader(gltfShadowDepthFrag, vulkanGlsl450, shaderFragment) + + PbrVertMsl* = toShader(gltfPbrVert, metalMSL, shaderVertex) + PbrFragMsl* = toShader(gltfPbrFrag, metalMSL, shaderFragment) + SkyboxVertMsl* = toShader(gltfSkyboxVert, metalMSL, shaderVertex) + SkyboxFragMsl* = toShader(gltfSkyboxFrag, metalMSL, shaderFragment) + ShadowDepthVertMsl* = + toShader(gltfShadowDepthVert, metalMSL, shaderVertex) + ShadowDepthFragMsl* = + toShader(gltfShadowDepthFrag, metalMSL, shaderFragment) diff --git a/src/gltf/backends/shaders/gltf_pbr.frag.spv b/src/gltf/backends/shaders/gltf_pbr.frag.spv new file mode 100644 index 0000000..ef202c5 Binary files /dev/null and b/src/gltf/backends/shaders/gltf_pbr.frag.spv differ diff --git a/src/gltf/backends/shaders/gltf_pbr.vert.spv b/src/gltf/backends/shaders/gltf_pbr.vert.spv new file mode 100644 index 0000000..2b62cb8 Binary files /dev/null and b/src/gltf/backends/shaders/gltf_pbr.vert.spv differ diff --git a/src/gltf/backends/vulkan/common.nim b/src/gltf/backends/vulkan/common.nim new file mode 100644 index 0000000..a909591 --- /dev/null +++ b/src/gltf/backends/vulkan/common.nim @@ -0,0 +1,46 @@ +when not defined(windows): + {.error: "The glTF Vulkan backend currently requires Windows.".} + +import pkg/vk14 except Window + +type + PipelineKey* = object + topology*: uint32 + doubleSided*: bool + blended*: bool + + VkTextureData* = ref object + image*: VkImage + memory*: VkDeviceMemory + view*: VkImageView + sampler*: VkSampler + format*: VkFormat + mipLevels*: int + layers*: int + isCube*: bool + + MaterialData* = ref object + materialVersion*: uint64 + descriptorPool*: VkDescriptorPool + descriptorSet*: VkDescriptorSet + textures*: seq[VkTextureData] + + PrimitiveData* = ref object + geometryVersion*: uint64 + vertexBuffer*: VkBuffer + vertexMemory*: VkDeviceMemory + vertexPtr*: pointer + vertexCapacity*: int + indexBuffer*: VkBuffer + indexMemory*: VkDeviceMemory + indexPtr*: pointer + indexCapacity*: int + indexCount*: int + topology*: VkPrimitiveTopology + + GltfFileData* = ref object + sceneVersion*: uint64 + + VkTexture* = VkTextureData + VkPrimitive* = PrimitiveData + VkMaterial* = MaterialData diff --git a/src/gltf/backends/vulkan/renderer.nim b/src/gltf/backends/vulkan/renderer.nim new file mode 100644 index 0000000..c586ba9 --- /dev/null +++ b/src/gltf/backends/vulkan/renderer.nim @@ -0,0 +1,2245 @@ +## Vulkan backend shader sources and renderer. + +when not defined(windows): + {.error: "The glTF Vulkan backend requires Windows.".} + +import + std/[algorithm, math, tables], + chroma, pixie, vmath, windy, + ../../common, ../../models, + ./common, + ../shaders as shaderSources + +import pkg/vk14 except Window + +when not defined(shadyBinaryShaders): + import std/os + import shady + +const + VertexEntryPoint* = "main" + FragmentEntryPoint* = "main" + + PbrVertexShader* = shaderSources.PbrVertVulkan + PbrFragmentShader* = shaderSources.PbrFragVulkan + SkyboxVertexShader* = shaderSources.SkyboxVertVulkan + SkyboxFragmentShader* = shaderSources.SkyboxFragVulkan + ShadowDepthVertexShader* = shaderSources.ShadowDepthVertVulkan + ShadowDepthFragmentShader* = shaderSources.ShadowDepthFragVulkan + + TextureDescriptorCount = 7 + VertexUniformBinding = 0 + PixelUniformBinding = 1 + MaxFrameUniformSets = 8192 + StudioEnvSize = 8 + DepthFormat = VK_FORMAT_D32_SFLOAT + PreferredMsaaSamples = 8'u32 + +when not defined(shadyBinaryShaders): + const + ShaderCacheDir = getTempDir() / "gltf-shady-vulkan" + PbrVertexGlslPath = ShaderCacheDir / "gltf_pbr.vert" + PbrFragmentGlslPath = ShaderCacheDir / "gltf_pbr.frag" + PbrVertexSpvPath = ShaderCacheDir / "gltf_pbr.vert.spv" + PbrFragmentSpvPath = ShaderCacheDir / "gltf_pbr.frag.spv" + +type + VkVertex {.packed.} = object + position: array[3, float32] + color: array[4, float32] + normal: array[3, float32] + uv: array[2, float32] + tangent: array[4, float32] + joints: array[4, uint16] + weights: array[4, float32] + uv1: array[2, float32] + + RgbaSubresource = object + width, height: int + pixels: seq[ColorRGBX] + + FrameBuffer = object + buffer: VkBuffer + memory: VkDeviceMemory + + BlendEntry = object + node: Node + primitive: Primitive + transform: Mat4 + + Std140Writer = object + data: seq[uint32] + offset: int + + Renderer* = ref object + window: Window + ctx: VulkanContext + materialSetLayout: VkDescriptorSetLayout + uniformSetLayout: VkDescriptorSetLayout + pipelineLayout: VkPipelineLayout + pipelineStates: Table[PipelineKey, VkPipeline] + sampleCount: VkSampleCountFlagBits + imageViews: seq[VkImageView] + imageLayouts: seq[VkImageLayout] + colorImages: seq[VkImage] + colorMemories: seq[VkDeviceMemory] + colorViews: seq[VkImageView] + depthImages: seq[VkImage] + depthMemories: seq[VkDeviceMemory] + depthViews: seq[VkImageView] + commandBuffers: seq[VkCommandBuffer] + frameDescriptorPool: VkDescriptorPool + frameBuffers: seq[FrameBuffer] + readbackBuffer: VkBuffer + readbackMemory: VkDeviceMemory + readbackSize: IVec2 + +proc f32bits(value: float32): uint32 = + cast[uint32](value) + +proc align(value, alignment: int): int = + ((value + alignment - 1) div alignment) * alignment + +proc alignOffset(writer: var Std140Writer, alignment: int) = + writer.offset = align(writer.offset, alignment) + +proc ensureBytes(writer: var Std140Writer, byteCount: int) = + let words = (max(0, byteCount) + 3) div 4 + if writer.data.len < words: + writer.data.setLen(words) + +proc putU32(writer: var Std140Writer, value: uint32) = + writer.ensureBytes(writer.offset + 4) + writer.data[writer.offset div 4] = value + writer.offset += 4 + +proc putFloat(writer: var Std140Writer, value: float32) = + writer.putU32(value.f32bits) + +proc putInt(writer: var Std140Writer, value: int) = + writer.putU32(value.uint32) + +proc putBool(writer: var Std140Writer, value: bool) = + writer.putU32(value.ord.uint32) + +proc putVec2(writer: var Std140Writer, value: Vec2) = + writer.alignOffset(8) + writer.putFloat(value.x) + writer.putFloat(value.y) + +proc putVec3(writer: var Std140Writer, value: Vec3) = + writer.alignOffset(16) + writer.putFloat(value.x) + writer.putFloat(value.y) + writer.putFloat(value.z) + +proc putColor(writer: var Std140Writer, value: Color) = + writer.alignOffset(16) + writer.putFloat(value.r) + writer.putFloat(value.g) + writer.putFloat(value.b) + writer.putFloat(value.a) + +proc putMat4(writer: var Std140Writer, value: Mat4) = + writer.alignOffset(16) + for i in 0 ..< 4: + for j in 0 ..< 4: + writer.putFloat(value[i, j]) + +proc putMat4Array(writer: var Std140Writer, values: openArray[Mat4], count: int) = + writer.alignOffset(16) + for i in 0 ..< count: + if i < values.len: + writer.putMat4(values[i]) + else: + writer.ensureBytes(writer.offset + 64) + writer.offset += 64 + +proc finish(writer: var Std140Writer): seq[uint32] = + writer.ensureBytes(max(4, align(writer.offset, 4))) + writer.data + +proc perspectiveVkRh*(fovY, aspect, nearPlane, farPlane: float32): Mat4 = + ## Vulkan right-handed projection matrix for vmath camera transforms. + let + h = 1.0'f32 / tan(degToRad(fovY) * 0.5'f32) + w = h / aspect + depth = nearPlane - farPlane + result[0, 0] = w + result[1, 1] = h + result[2, 2] = farPlane / depth + result[2, 3] = -1.0'f32 + result[3, 2] = (nearPlane * farPlane) / depth + +proc requiresSwapChainRecreate(vkResult: VkResult): bool = + let code = vkResult.int32 + code == VK_SUBOPTIMAL_KHR.int32 or + code == VK_ERROR_OUT_OF_DATE_KHR.int32 + +proc createShaderModule(device: VkDevice, code: string): VkShaderModule = + var createInfo = VkShaderModuleCreateInfo( + sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + codeSize: code.len.uint32, + pCode: cast[ptr uint32](code[0].unsafeAddr) + ) + checkVk(vkCreateShaderModule(device, createInfo.addr, nil, result.addr), + "Creating Vulkan shader module") + +proc findMemoryType( + ctx: VulkanContext, typeFilter: uint32, properties: uint32 +): uint32 = + var memProperties: VkPhysicalDeviceMemoryProperties + vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProperties.addr) + for i in 0'u32 ..< memProperties.memoryTypeCount: + let flags = memProperties.memoryTypes[i].propertyFlags.uint32 + if ((typeFilter shr i) and 1'u32) == 1'u32 and + (flags and properties) == properties: + return i + raise newException(GltfError, "Failed to find suitable Vulkan memory type.") + +proc chooseMsaaSampleCount(ctx: VulkanContext): VkSampleCountFlagBits = + var props: VkPhysicalDeviceProperties + vkGetPhysicalDeviceProperties(ctx.physicalDevice, props.addr) + let counts = props.limits.framebufferColorSampleCounts.uint32 and + props.limits.framebufferDepthSampleCounts.uint32 + if PreferredMsaaSamples >= 8 and + (counts and VK_SAMPLE_COUNT_8_BIT.uint32) != 0: + return VK_SAMPLE_COUNT_8_BIT + if PreferredMsaaSamples >= 4 and + (counts and VK_SAMPLE_COUNT_4_BIT.uint32) != 0: + return VK_SAMPLE_COUNT_4_BIT + if (counts and VK_SAMPLE_COUNT_2_BIT.uint32) != 0: + return VK_SAMPLE_COUNT_2_BIT + VK_SAMPLE_COUNT_1_BIT + +proc msaaEnabled(renderer: Renderer): bool = + renderer.sampleCount != VK_SAMPLE_COUNT_1_BIT + +proc createBuffer( + ctx: VulkanContext, + size: VkDeviceSize, + usage, properties: uint32, + buffer: var VkBuffer, + memory: var VkDeviceMemory +) = + var bufferInfo = VkBufferCreateInfo( + sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + size: max(VkDeviceSize(1), size), + usage: VkBufferUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE + ) + checkVk(vkCreateBuffer(ctx.device, bufferInfo.addr, nil, buffer.addr), + "Creating Vulkan buffer") + + var memRequirements: VkMemoryRequirements + vkGetBufferMemoryRequirements(ctx.device, buffer, memRequirements.addr) + + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), + "Allocating Vulkan buffer memory") + checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), + "Binding Vulkan buffer memory") + +proc createImage( + ctx: VulkanContext, + width, height, mipLevels, layers: int, + format: VkFormat, + usage: uint32, + imageFlags: uint32, + samples: VkSampleCountFlagBits, + image: var VkImage, + memory: var VkDeviceMemory +) = + var imageInfo = VkImageCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + flags: VkImageCreateFlags(imageFlags), + imageType: VK_IMAGE_TYPE_2D, + format: format, + extent: VkExtent3D(width: width.uint32, height: height.uint32, depth: 1), + mipLevels: mipLevels.uint32, + arrayLayers: layers.uint32, + samples: samples, + tiling: VK_IMAGE_TILING_OPTIMAL, + usage: VkImageUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE, + initialLayout: VK_IMAGE_LAYOUT_UNDEFINED + ) + checkVk(vkCreateImage(ctx.device, imageInfo.addr, nil, image.addr), + "Creating Vulkan image") + + var memRequirements: VkMemoryRequirements + vkGetImageMemoryRequirements(ctx.device, image, memRequirements.addr) + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, + memRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), + "Allocating Vulkan image memory") + checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), + "Binding Vulkan image memory") + +proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: 1 + ) + checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, result.addr), + "Allocating Vulkan upload command buffer") + var beginInfo = VkCommandBufferBeginInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) + ) + checkVk(vkBeginCommandBuffer(result, beginInfo.addr), + "Beginning Vulkan upload command buffer") + +proc endSingleTimeCommands(ctx: VulkanContext, commandBuffer: VkCommandBuffer) = + checkVk(vkEndCommandBuffer(commandBuffer), + "Ending Vulkan upload command buffer") + var submitInfo = VkSubmitInfo( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO, + commandBufferCount: 1, + pCommandBuffers: unsafeAddr commandBuffer + ) + checkVk(vkQueueSubmit(ctx.graphicsQueue, 1, submitInfo.addr, VkFence(0)), + "Submitting Vulkan upload command buffer") + checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "Waiting for Vulkan upload") + vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr commandBuffer) + +proc transitionImageLayout( + ctx: VulkanContext, + image: VkImage, + aspect: VkImageAspectFlags, + mipLevels, layers: int, + oldLayout, newLayout: VkImageLayout +) = + let commandBuffer = beginSingleTimeCommands(ctx) + var barrier = VkImageMemoryBarrier( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + oldLayout: oldLayout, + newLayout: newLayout, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: image, + subresourceRange: VkImageSubresourceRange( + aspectMask: aspect, + baseMipLevel: 0, + levelCount: mipLevels.uint32, + baseArrayLayer: 0, + layerCount: layers.uint32 + ) + ) + + var sourceStage, destinationStage: VkPipelineStageFlags + if oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and + newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + barrier.srcAccessMask = VkAccessFlags(0) + barrier.dstAccessMask = VkAccessFlags(VK_ACCESS_TRANSFER_WRITE_BIT) + sourceStage = VkPipelineStageFlags(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT) + destinationStage = VkPipelineStageFlags(VK_PIPELINE_STAGE_TRANSFER_BIT) + elif oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL and + newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + barrier.srcAccessMask = VkAccessFlags(VK_ACCESS_TRANSFER_WRITE_BIT) + barrier.dstAccessMask = VkAccessFlags(VK_ACCESS_SHADER_READ_BIT) + sourceStage = VkPipelineStageFlags(VK_PIPELINE_STAGE_TRANSFER_BIT) + destinationStage = VkPipelineStageFlags(VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT) + else: + raise newException(GltfError, "Unsupported Vulkan image layout transition.") + + vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage, + VkDependencyFlags(0), 0, nil, 0, nil, 1, barrier.addr) + endSingleTimeCommands(ctx, commandBuffer) + +proc downsample(src: RgbaSubresource): RgbaSubresource = + result.width = max(1, src.width div 2) + result.height = max(1, src.height div 2) + result.pixels = newSeq[ColorRGBX](result.width * result.height) + for y in 0 ..< result.height: + let + sy0 = y * src.height div result.height + sy1 = min(src.height, max(sy0 + 1, (y + 1) * src.height div result.height)) + for x in 0 ..< result.width: + let + sx0 = x * src.width div result.width + sx1 = min(src.width, max(sx0 + 1, (x + 1) * src.width div result.width)) + var r, g, b, a, count: uint32 + for sy in sy0 ..< sy1: + for sx in sx0 ..< sx1: + let pixel = src.pixels[sy * src.width + sx] + r += pixel.r.uint32 + g += pixel.g.uint32 + b += pixel.b.uint32 + a += pixel.a.uint32 + inc count + result.pixels[y * result.width + x] = rgbx( + uint8(r div count), + uint8(g div count), + uint8(b div count), + uint8(a div count) + ) + +proc buildMipChain(base: RgbaSubresource): seq[RgbaSubresource] = + result.add(base) + while result[^1].width > 1 or result[^1].height > 1: + result.add(result[^1].downsample()) + +proc buildImageMips(image: Image): seq[RgbaSubresource] = + buildMipChain(RgbaSubresource( + width: image.width, + height: image.height, + pixels: image.data + )) + +proc studioFaceDirection(face, x, y, size: int): Vec3 = + let + u = ((x.float32 + 0.5'f32) / size.float32) * 2.0'f32 - 1.0'f32 + v = ((y.float32 + 0.5'f32) / size.float32) * 2.0'f32 - 1.0'f32 + case face + of 0: normalize(vec3(1.0'f32, -v, -u)) + of 1: normalize(vec3(-1.0'f32, -v, u)) + of 2: normalize(vec3(u, 1.0'f32, v)) + of 3: normalize(vec3(u, -1.0'f32, -v)) + of 4: normalize(vec3(u, -v, 1.0'f32)) + of 5: normalize(vec3(-u, -v, -1.0'f32)) + else: vec3(0, 1, 0) + +proc studioColor(dir: Vec3): ColorRGBX = + let + hemi = clamp(dir.y * 0.5'f32 + 0.5'f32, 0.0'f32, 1.0'f32) + keyDir = normalize(vec3(0.35'f32, 0.85'f32, 0.25'f32)) + fillDir = normalize(vec3(-0.45'f32, 0.65'f32, -0.35'f32)) + key = pow(max(dot(dir, keyDir), 0.0'f32), 24.0'f32) + fill = pow(max(dot(dir, fillDir), 0.0'f32), 8.0'f32) + cool = vec3(0.18'f32, 0.19'f32, 0.21'f32) + neutral = vec3(0.58'f32, 0.6'f32, 0.63'f32) + sky = vec3(0.92'f32, 0.94'f32, 0.97'f32) + var color = mix(cool, neutral, hemi) + color = mix(color, sky, hemi * hemi) + color += vec3(0.28'f32, 0.27'f32, 0.25'f32) * key + color += vec3(0.10'f32, 0.11'f32, 0.12'f32) * fill + rgbx( + uint8(clamp((color.x * 255.0'f32).int, 0, 255)), + uint8(clamp((color.y * 255.0'f32).int, 0, 255)), + uint8(clamp((color.z * 255.0'f32).int, 0, 255)), + 255 + ) + +proc buildStudioCubeMips(): seq[RgbaSubresource] = + for face in 0 ..< 6: + var base = RgbaSubresource( + width: StudioEnvSize, + height: StudioEnvSize, + pixels: newSeq[ColorRGBX](StudioEnvSize * StudioEnvSize) + ) + for y in 0 ..< StudioEnvSize: + for x in 0 ..< StudioEnvSize: + base.pixels[y * StudioEnvSize + x] = + studioColor(studioFaceDirection(face, x, y, StudioEnvSize)) + for mip in base.buildMipChain(): + result.add(mip) + +proc createSampler( + renderer: Renderer, + mipLevels: int, + clampToEdge = false, + compare = false +): VkSampler = + let addressMode = + if clampToEdge: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE + else: VK_SAMPLER_ADDRESS_MODE_REPEAT + var samplerInfo = VkSamplerCreateInfo( + sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + magFilter: VK_FILTER_LINEAR, + minFilter: VK_FILTER_LINEAR, + mipmapMode: VK_SAMPLER_MIPMAP_MODE_LINEAR, + addressModeU: addressMode, + addressModeV: addressMode, + addressModeW: addressMode, + anisotropyEnable: VkBool32(VK_FALSE), + maxAnisotropy: 1.0, + compareEnable: VkBool32(if compare: VK_TRUE else: VK_FALSE), + compareOp: if compare: VK_COMPARE_OP_LESS_OR_EQUAL else: VK_COMPARE_OP_ALWAYS, + minLod: 0.0, + maxLod: max(0, mipLevels - 1).float32, + borderColor: VK_BORDER_COLOR_INT_OPAQUE_WHITE, + unnormalizedCoordinates: VkBool32(VK_FALSE) + ) + checkVk(vkCreateSampler(renderer.ctx.device, samplerInfo.addr, nil, result.addr), + "Creating Vulkan sampler") + +proc uploadRgbaSubresources( + renderer: Renderer, + width, height, mipLevels, layers: int, + subresources: openArray[RgbaSubresource], + isCube = false +): VkTexture = + let + subresourceCount = mipLevels * layers + imageFlags = + if isCube: VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT.uint32 + else: 0'u32 + var offsets = newSeq[VkDeviceSize](subresourceCount) + var totalBytes = VkDeviceSize(0) + for i in 0 ..< subresourceCount: + offsets[i] = totalBytes + totalBytes += VkDeviceSize(subresources[i].width * subresources[i].height * 4) + + var stagingBuffer: VkBuffer + var stagingMemory: VkDeviceMemory + createBuffer(renderer.ctx, totalBytes, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + stagingBuffer, stagingMemory) + + var mapped: pointer + checkVk(vkMapMemory(renderer.ctx.device, stagingMemory, + VkDeviceSize(0), totalBytes, VkMemoryMapFlags(0), mapped.addr), + "Mapping Vulkan texture staging memory") + let base = cast[uint](mapped) + for i in 0 ..< subresourceCount: + let src = subresources[i] + let dst = cast[pointer](base + uint(offsets[i])) + copyMem(dst, unsafeAddr src.pixels[0], src.width * src.height * 4) + vkUnmapMemory(renderer.ctx.device, stagingMemory) + + var image: VkImage + var memory: VkDeviceMemory + createImage(renderer.ctx, width, height, mipLevels, layers, + VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_TRANSFER_DST_BIT.uint32 or VK_IMAGE_USAGE_SAMPLED_BIT.uint32, + imageFlags, VK_SAMPLE_COUNT_1_BIT, image, memory) + + transitionImageLayout(renderer.ctx, image, + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + mipLevels, layers, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + + var regions = newSeq[VkBufferImageCopy](subresourceCount) + for i in 0 ..< subresourceCount: + let + face = i div mipLevels + mip = i mod mipLevels + src = subresources[i] + regions[i] = VkBufferImageCopy( + bufferOffset: offsets[i], + imageSubresource: VkImageSubresourceLayers( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + mipLevel: mip.uint32, + baseArrayLayer: face.uint32, + layerCount: 1 + ), + imageExtent: VkExtent3D( + width: src.width.uint32, + height: src.height.uint32, + depth: 1 + ) + ) + let commandBuffer = beginSingleTimeCommands(renderer.ctx) + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + regions.len.uint32, regions[0].addr) + endSingleTimeCommands(renderer.ctx, commandBuffer) + + transitionImageLayout(renderer.ctx, image, + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + mipLevels, layers, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + + vkDestroyBuffer(renderer.ctx.device, stagingBuffer, nil) + vkFreeMemory(renderer.ctx.device, stagingMemory, nil) + + var view: VkImageView + var viewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: image, + viewType: if isCube: VK_IMAGE_VIEW_TYPE_CUBE else: VK_IMAGE_VIEW_TYPE_2D, + format: VK_FORMAT_R8G8B8A8_UNORM, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: mipLevels.uint32, + baseArrayLayer: 0, + layerCount: layers.uint32 + ) + ) + checkVk(vkCreateImageView(renderer.ctx.device, viewInfo.addr, nil, view.addr), + "Creating Vulkan texture view") + + VkTexture( + image: image, + memory: memory, + view: view, + sampler: renderer.createSampler(mipLevels, clampToEdge = isCube), + format: VK_FORMAT_R8G8B8A8_UNORM, + mipLevels: mipLevels, + layers: layers, + isCube: isCube + ) + +proc uploadImage(renderer: Renderer, image: Image): VkTexture = + let mips = image.buildImageMips() + renderer.uploadRgbaSubresources(image.width, image.height, mips.len, 1, mips) + +proc uploadSolidImage(renderer: Renderer, color: ColorRGBX): VkTexture = + var image = newImage(1, 1) + image.fill(color) + renderer.uploadImage(image) + +proc uploadStudioCube(renderer: Renderer): VkTexture = + let mips = buildStudioCubeMips() + renderer.uploadRgbaSubresources( + StudioEnvSize, + StudioEnvSize, + mips.len div 6, + 6, + mips, + isCube = true + ) + +proc uploadShadowPlaceholder(renderer: Renderer): VkTexture = + var pixel = 1.0'f32 + var stagingBuffer: VkBuffer + var stagingMemory: VkDeviceMemory + createBuffer(renderer.ctx, VkDeviceSize(sizeof(float32)), + VK_BUFFER_USAGE_TRANSFER_SRC_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + stagingBuffer, stagingMemory) + var mapped: pointer + checkVk(vkMapMemory(renderer.ctx.device, stagingMemory, + VkDeviceSize(0), VkDeviceSize(sizeof(float32)), + VkMemoryMapFlags(0), mapped.addr), + "Mapping Vulkan shadow placeholder staging memory") + copyMem(mapped, pixel.addr, sizeof(float32)) + vkUnmapMemory(renderer.ctx.device, stagingMemory) + + var image: VkImage + var memory: VkDeviceMemory + createImage(renderer.ctx, 1, 1, 1, 1, DepthFormat, + VK_IMAGE_USAGE_TRANSFER_DST_BIT.uint32 or VK_IMAGE_USAGE_SAMPLED_BIT.uint32, + 0, VK_SAMPLE_COUNT_1_BIT, image, memory) + transitionImageLayout(renderer.ctx, image, + VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), 1, 1, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + + var region = VkBufferImageCopy( + bufferOffset: VkDeviceSize(0), + imageSubresource: VkImageSubresourceLayers( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + mipLevel: 0, + baseArrayLayer: 0, + layerCount: 1 + ), + imageExtent: VkExtent3D(width: 1, height: 1, depth: 1) + ) + let commandBuffer = beginSingleTimeCommands(renderer.ctx) + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, region.addr) + endSingleTimeCommands(renderer.ctx, commandBuffer) + + transitionImageLayout(renderer.ctx, image, + VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), 1, 1, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + + vkDestroyBuffer(renderer.ctx.device, stagingBuffer, nil) + vkFreeMemory(renderer.ctx.device, stagingMemory, nil) + + var view: VkImageView + var viewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: image, + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: DepthFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(renderer.ctx.device, viewInfo.addr, nil, view.addr), + "Creating Vulkan shadow placeholder view") + + VkTexture( + image: image, + memory: memory, + view: view, + sampler: renderer.createSampler(1, clampToEdge = true, compare = true), + format: DepthFormat, + mipLevels: 1, + layers: 1 + ) + +proc createSwapChainImageViews(renderer: Renderer) = + renderer.imageViews.setLen(renderer.ctx.swapChainImages.len) + renderer.imageLayouts.setLen(renderer.ctx.swapChainImages.len) + for i, image in renderer.ctx.swapChainImages: + renderer.imageLayouts[i] = VK_IMAGE_LAYOUT_UNDEFINED + var createInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: image, + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: renderer.ctx.swapChainImageFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(renderer.ctx.device, createInfo.addr, nil, + renderer.imageViews[i].addr), + "Creating Vulkan swapchain image view") + +proc createColorResources(renderer: Renderer) = + renderer.colorImages.setLen(0) + renderer.colorMemories.setLen(0) + renderer.colorViews.setLen(0) + if not renderer.msaaEnabled: + return + + let count = renderer.ctx.swapChainImages.len + renderer.colorImages.setLen(count) + renderer.colorMemories.setLen(count) + renderer.colorViews.setLen(count) + for i in 0 ..< count: + createImage(renderer.ctx, + renderer.ctx.swapChainExtent.width.int, + renderer.ctx.swapChainExtent.height.int, + 1, 1, renderer.ctx.swapChainImageFormat, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT.uint32 or + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT.uint32, + 0, + renderer.sampleCount, + renderer.colorImages[i], + renderer.colorMemories[i]) + var imageViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.colorImages[i], + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: renderer.ctx.swapChainImageFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(renderer.ctx.device, imageViewInfo.addr, nil, + renderer.colorViews[i].addr), + "Creating Vulkan multisample color image view") + +proc createDepthResources(renderer: Renderer) = + let count = renderer.ctx.swapChainImages.len + renderer.depthImages.setLen(count) + renderer.depthMemories.setLen(count) + renderer.depthViews.setLen(count) + for i in 0 ..< count: + createImage(renderer.ctx, + renderer.ctx.swapChainExtent.width.int, + renderer.ctx.swapChainExtent.height.int, + 1, 1, DepthFormat, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.uint32, + 0, + renderer.sampleCount, + renderer.depthImages[i], + renderer.depthMemories[i]) + var imageViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.depthImages[i], + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: DepthFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(renderer.ctx.device, imageViewInfo.addr, nil, + renderer.depthViews[i].addr), + "Creating Vulkan depth image view") + +proc createReadbackBuffer(renderer: Renderer, size: IVec2) = + if renderer.readbackBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, renderer.readbackBuffer, nil) + vkFreeMemory(renderer.ctx.device, renderer.readbackMemory, nil) + renderer.readbackBuffer = VkBuffer(0) + renderer.readbackMemory = VkDeviceMemory(0) + + createBuffer(renderer.ctx, VkDeviceSize(size.x.int * size.y.int * 4), + VK_BUFFER_USAGE_TRANSFER_DST_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + renderer.readbackBuffer, + renderer.readbackMemory) + renderer.readbackSize = size + +proc allocateCommandBuffers(renderer: Renderer) = + renderer.commandBuffers.setLen(renderer.ctx.swapChainImages.len) + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: renderer.ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: renderer.commandBuffers.len.uint32 + ) + checkVk(vkAllocateCommandBuffers(renderer.ctx.device, allocInfo.addr, + renderer.commandBuffers[0].addr), + "Allocating Vulkan draw command buffers") + +proc destroySwapChainResources(renderer: Renderer) = + if renderer.commandBuffers.len > 0: + vkFreeCommandBuffers(renderer.ctx.device, renderer.ctx.commandPool, + renderer.commandBuffers.len.uint32, renderer.commandBuffers[0].addr) + renderer.commandBuffers.setLen(0) + for view in renderer.imageViews: + vkDestroyImageView(renderer.ctx.device, view, nil) + renderer.imageViews.setLen(0) + renderer.imageLayouts.setLen(0) + for view in renderer.colorViews: + vkDestroyImageView(renderer.ctx.device, view, nil) + renderer.colorViews.setLen(0) + for image in renderer.colorImages: + vkDestroyImage(renderer.ctx.device, image, nil) + renderer.colorImages.setLen(0) + for memory in renderer.colorMemories: + vkFreeMemory(renderer.ctx.device, memory, nil) + renderer.colorMemories.setLen(0) + for view in renderer.depthViews: + vkDestroyImageView(renderer.ctx.device, view, nil) + renderer.depthViews.setLen(0) + for image in renderer.depthImages: + vkDestroyImage(renderer.ctx.device, image, nil) + renderer.depthImages.setLen(0) + for memory in renderer.depthMemories: + vkFreeMemory(renderer.ctx.device, memory, nil) + renderer.depthMemories.setLen(0) + for pipeline in renderer.pipelineStates.values: + vkDestroyPipeline(renderer.ctx.device, pipeline, nil) + renderer.pipelineStates.clear() + +proc createSwapChainResources(renderer: Renderer) = + renderer.createSwapChainImageViews() + renderer.createColorResources() + renderer.createDepthResources() + renderer.allocateCommandBuffers() + renderer.createReadbackBuffer(ivec2( + renderer.ctx.swapChainExtent.width.int32, + renderer.ctx.swapChainExtent.height.int32 + )) + +proc createDescriptorSetLayouts(renderer: Renderer) = + var materialBindings: array[TextureDescriptorCount, VkDescriptorSetLayoutBinding] + for i in 0 ..< TextureDescriptorCount: + materialBindings[i] = VkDescriptorSetLayoutBinding( + binding: i.uint32, + descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + descriptorCount: 1, + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT) + ) + var materialLayoutInfo = VkDescriptorSetLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + bindingCount: materialBindings.len.uint32, + pBindings: materialBindings[0].addr + ) + checkVk(vkCreateDescriptorSetLayout(renderer.ctx.device, + materialLayoutInfo.addr, nil, renderer.materialSetLayout.addr), + "Creating Vulkan material descriptor set layout") + + var uniformBindings = [ + VkDescriptorSetLayoutBinding( + binding: VertexUniformBinding.uint32, + descriptorType: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + descriptorCount: 1, + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT) + ), + VkDescriptorSetLayoutBinding( + binding: PixelUniformBinding.uint32, + descriptorType: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + descriptorCount: 1, + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT) + ) + ] + var uniformLayoutInfo = VkDescriptorSetLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + bindingCount: uniformBindings.len.uint32, + pBindings: uniformBindings[0].addr + ) + checkVk(vkCreateDescriptorSetLayout(renderer.ctx.device, + uniformLayoutInfo.addr, nil, renderer.uniformSetLayout.addr), + "Creating Vulkan uniform descriptor set layout") + +proc createPipelineLayout(renderer: Renderer) = + var layouts = [renderer.materialSetLayout, renderer.uniformSetLayout] + var pipelineLayoutInfo = VkPipelineLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + setLayoutCount: layouts.len.uint32, + pSetLayouts: layouts[0].addr + ) + checkVk(vkCreatePipelineLayout(renderer.ctx.device, + pipelineLayoutInfo.addr, nil, renderer.pipelineLayout.addr), + "Creating Vulkan pipeline layout") + +proc createFrameDescriptorPool(renderer: Renderer) = + var poolSize = VkDescriptorPoolSize( + `type`: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + descriptorCount: (MaxFrameUniformSets * 2).uint32 + ) + var poolInfo = VkDescriptorPoolCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + maxSets: MaxFrameUniformSets.uint32, + poolSizeCount: 1, + pPoolSizes: poolSize.addr + ) + checkVk(vkCreateDescriptorPool(renderer.ctx.device, poolInfo.addr, nil, + renderer.frameDescriptorPool.addr), + "Creating Vulkan frame descriptor pool") + +proc createPipeline( + renderer: Renderer, + key: PipelineKey +): VkPipeline = + when defined(shadyBinaryShaders): + const + vertShaderCode = staticRead("../shaders/gltf_pbr.vert.spv") + fragShaderCode = staticRead("../shaders/gltf_pbr.frag.spv") + else: + const + vertShaderCode = compileSpirvShader( + PbrVertexShader, + PbrVertexGlslPath, + PbrVertexSpvPath, + binaryVertex + ) + fragShaderCode = compileSpirvShader( + PbrFragmentShader, + PbrFragmentGlslPath, + PbrFragmentSpvPath, + binaryFragment + ) + let + vertModule = createShaderModule(renderer.ctx.device, vertShaderCode) + fragModule = createShaderModule(renderer.ctx.device, fragShaderCode) + try: + var + colorFormat = renderer.ctx.swapChainImageFormat + renderingInfo = VkPipelineRenderingCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + colorAttachmentCount: 1, + pColorAttachmentFormats: colorFormat.addr, + depthAttachmentFormat: DepthFormat + ) + dynamicStates = [ + VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), + VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) + ] + dynamicState = VkPipelineDynamicStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + dynamicStateCount: dynamicStates.len.uint32, + pDynamicStates: dynamicStates[0].addr + ) + vertStage = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_VERTEX_BIT, + module: vertModule, + pName: "main" + ) + fragStage = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_FRAGMENT_BIT, + module: fragModule, + pName: "main" + ) + shaderStages = [vertStage, fragStage] + bindingDesc = VkVertexInputBindingDescription( + binding: 0, + stride: sizeof(VkVertex).uint32, + inputRate: VK_VERTEX_INPUT_RATE_VERTEX + ) + attributeDescs = [ + VkVertexInputAttributeDescription( + location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), + VkVertexInputAttributeDescription( + location: 1, binding: 0, format: VK_FORMAT_R32G32B32A32_SFLOAT, offset: 12), + VkVertexInputAttributeDescription( + location: 2, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 28), + VkVertexInputAttributeDescription( + location: 3, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: 40), + VkVertexInputAttributeDescription( + location: 4, binding: 0, format: VK_FORMAT_R32G32B32A32_SFLOAT, offset: 48), + VkVertexInputAttributeDescription( + location: 5, binding: 0, format: VK_FORMAT_R16G16B16A16_UINT, offset: 64), + VkVertexInputAttributeDescription( + location: 6, binding: 0, format: VK_FORMAT_R32G32B32A32_SFLOAT, offset: 72), + VkVertexInputAttributeDescription( + location: 7, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: 88) + ] + vertexInputInfo = VkPipelineVertexInputStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + vertexBindingDescriptionCount: 1, + pVertexBindingDescriptions: bindingDesc.addr, + vertexAttributeDescriptionCount: attributeDescs.len.uint32, + pVertexAttributeDescriptions: attributeDescs[0].addr + ) + inputAssembly = VkPipelineInputAssemblyStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + topology: key.topology, + primitiveRestartEnable: VkBool32(VK_FALSE) + ) + viewportState = VkPipelineViewportStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + viewportCount: 1, + scissorCount: 1 + ) + rasterizer = VkPipelineRasterizationStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + depthClampEnable: VkBool32(VK_FALSE), + rasterizerDiscardEnable: VkBool32(VK_FALSE), + polygonMode: VK_POLYGON_MODE_FILL, + lineWidth: 1.0, + cullMode: + if key.doubleSided: + VkCullModeFlags(VK_CULL_MODE_NONE) + else: + VkCullModeFlags(VK_CULL_MODE_BACK_BIT), + frontFace: VK_FRONT_FACE_COUNTER_CLOCKWISE, + depthBiasEnable: VkBool32(VK_FALSE) + ) + multisampling = VkPipelineMultisampleStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + sampleShadingEnable: VkBool32(VK_FALSE), + rasterizationSamples: renderer.sampleCount + ) + depthStencil = VkPipelineDepthStencilStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + depthTestEnable: VkBool32(VK_TRUE), + depthWriteEnable: VkBool32(if key.blended: VK_FALSE else: VK_TRUE), + depthCompareOp: VK_COMPARE_OP_LESS, + depthBoundsTestEnable: VkBool32(VK_FALSE), + stencilTestEnable: VkBool32(VK_FALSE), + minDepthBounds: 0, + maxDepthBounds: 1 + ) + colorBlendAttachment = VkPipelineColorBlendAttachmentState( + colorWriteMask: VkColorComponentFlags(0x0000000F), + blendEnable: VkBool32(if key.blended: VK_TRUE else: VK_FALSE), + srcColorBlendFactor: if key.blended: VK_BLEND_FACTOR_SRC_ALPHA else: VK_BLEND_FACTOR_ONE, + dstColorBlendFactor: if key.blended: VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA else: VK_BLEND_FACTOR_ZERO, + colorBlendOp: VK_BLEND_OP_ADD, + srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, + dstAlphaBlendFactor: if key.blended: VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA else: VK_BLEND_FACTOR_ZERO, + alphaBlendOp: VK_BLEND_OP_ADD + ) + colorBlending = VkPipelineColorBlendStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + logicOpEnable: VkBool32(VK_FALSE), + logicOp: VK_LOGIC_OP_COPY, + attachmentCount: 1, + pAttachments: colorBlendAttachment.addr, + blendConstants: [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] + ) + pipelineInfo = VkGraphicsPipelineCreateInfo( + sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + pNext: renderingInfo.addr, + stageCount: shaderStages.len.uint32, + pStages: shaderStages[0].addr, + pVertexInputState: vertexInputInfo.addr, + pInputAssemblyState: inputAssembly.addr, + pViewportState: viewportState.addr, + pRasterizationState: rasterizer.addr, + pMultisampleState: multisampling.addr, + pDepthStencilState: depthStencil.addr, + pColorBlendState: colorBlending.addr, + pDynamicState: dynamicState.addr, + layout: renderer.pipelineLayout + ) + checkVk(vkCreateGraphicsPipelines(renderer.ctx.device, VkPipelineCache(0), + 1, pipelineInfo.addr, nil, result.addr), + "Creating Vulkan graphics pipeline") + finally: + vkDestroyShaderModule(renderer.ctx.device, vertModule, nil) + vkDestroyShaderModule(renderer.ctx.device, fragModule, nil) + +proc getPipeline(renderer: Renderer, key: PipelineKey): VkPipeline = + if key notin renderer.pipelineStates: + renderer.pipelineStates[key] = renderer.createPipeline(key) + renderer.pipelineStates[key] + +proc newRenderer*(window: Window): Renderer = + ## Creates a Vulkan renderer bound to a Windy window. + let safeSize = ivec2(max(1'i32, window.size.x), max(1'i32, window.size.y)) + result = Renderer(window: window) + let hwnd = window.getHWND() + if hwnd == 0: + raise newException(GltfError, "Failed to acquire HWND for Vulkan renderer.") + result.ctx.initDevice(hwnd, safeSize.x.int, safeSize.y.int, window.vsync) + result.sampleCount = chooseMsaaSampleCount(result.ctx) + result.createDescriptorSetLayouts() + result.createPipelineLayout() + result.createFrameDescriptorPool() + result.createSwapChainResources() + +proc releaseTexture(renderer: Renderer, texture: VkTexture) = + if texture == nil: + return + if texture.sampler.int64 != 0: + vkDestroySampler(renderer.ctx.device, texture.sampler, nil) + texture.sampler = VkSampler(0) + if texture.view.int64 != 0: + vkDestroyImageView(renderer.ctx.device, texture.view, nil) + texture.view = VkImageView(0) + if texture.image.int64 != 0: + vkDestroyImage(renderer.ctx.device, texture.image, nil) + texture.image = VkImage(0) + if texture.memory.int64 != 0: + vkFreeMemory(renderer.ctx.device, texture.memory, nil) + texture.memory = VkDeviceMemory(0) + +proc releaseMaterial(renderer: Renderer, material: VkMaterial) = + if material == nil: + return + for texture in material.textures: + renderer.releaseTexture(texture) + material.textures.setLen(0) + if material.descriptorPool.int64 != 0: + vkDestroyDescriptorPool(renderer.ctx.device, material.descriptorPool, nil) + material.descriptorPool = VkDescriptorPool(0) + +proc releasePrimitive(renderer: Renderer, primitive: VkPrimitive) = + if primitive == nil: + return + if primitive.vertexPtr != nil: + vkUnmapMemory(renderer.ctx.device, primitive.vertexMemory) + primitive.vertexPtr = nil + if primitive.vertexBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, primitive.vertexBuffer, nil) + primitive.vertexBuffer = VkBuffer(0) + if primitive.vertexMemory.int64 != 0: + vkFreeMemory(renderer.ctx.device, primitive.vertexMemory, nil) + primitive.vertexMemory = VkDeviceMemory(0) + if primitive.indexPtr != nil: + vkUnmapMemory(renderer.ctx.device, primitive.indexMemory) + primitive.indexPtr = nil + if primitive.indexBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, primitive.indexBuffer, nil) + primitive.indexBuffer = VkBuffer(0) + if primitive.indexMemory.int64 != 0: + vkFreeMemory(renderer.ctx.device, primitive.indexMemory, nil) + primitive.indexMemory = VkDeviceMemory(0) + +proc releaseFrameBuffers(renderer: Renderer) = + for frameBuffer in renderer.frameBuffers: + if frameBuffer.buffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, frameBuffer.buffer, nil) + if frameBuffer.memory.int64 != 0: + vkFreeMemory(renderer.ctx.device, frameBuffer.memory, nil) + renderer.frameBuffers.setLen(0) + +proc resetFrameResources(renderer: Renderer) = + renderer.releaseFrameBuffers() + if renderer.frameDescriptorPool.int64 != 0: + checkVk(vkResetDescriptorPool(renderer.ctx.device, + renderer.frameDescriptorPool, VkDescriptorPoolResetFlags(0)), + "Resetting Vulkan frame descriptor pool") + +proc resize(renderer: Renderer, size: IVec2) = + let safeSize = ivec2(max(1'i32, size.x), max(1'i32, size.y)) + if renderer.readbackSize == safeSize: + return + discard vkDeviceWaitIdle(renderer.ctx.device) + renderer.destroySwapChainResources() + recreateSwapChain(renderer.ctx, safeSize.x.int, safeSize.y.int) + renderer.createSwapChainResources() + +proc vertexAt(primitive: Primitive, index: int): VkVertex = + let colorValue = + if index < primitive.colors.len: + primitive.colors[index].color + else: + color(1, 1, 1, 1) + result.position = [ + primitive.points[index].x, + primitive.points[index].y, + primitive.points[index].z + ] + result.color = [colorValue.r, colorValue.g, colorValue.b, colorValue.a] + let normal = + if index < primitive.normals.len: + primitive.normals[index] + else: + vec3(0, 0, 0) + result.normal = [normal.x, normal.y, normal.z] + let uv = + if index < primitive.uvs.len: + primitive.uvs[index] + else: + vec2(0, 0) + result.uv = [uv.x, uv.y] + let tangent = + if index < primitive.tangents.len: + primitive.tangents[index] + else: + vec4(1, 0, 0, 1) + result.tangent = [tangent.x, tangent.y, tangent.z, tangent.w] + result.joints = + if index < primitive.jointIds.len: + primitive.jointIds[index] + else: + [0'u16, 0'u16, 0'u16, 0'u16] + let weights = + if index < primitive.jointWeights.len: + primitive.jointWeights[index] + else: + vec4(0, 0, 0, 0) + result.weights = [weights.x, weights.y, weights.z, weights.w] + let uv1 = + if index < primitive.uvs1.len: + primitive.uvs1[index] + else: + vec2(0, 0) + result.uv1 = [uv1.x, uv1.y] + +proc primitiveSourceIndices(primitive: Primitive): seq[uint32] = + if primitive.indices32.len > 0: + result = primitive.indices32 + elif primitive.indices16.len > 0: + result.setLen(primitive.indices16.len) + for i, value in primitive.indices16: + result[i] = value.uint32 + else: + result.setLen(primitive.points.len) + for i in 0 ..< primitive.points.len: + result[i] = i.uint32 + +proc buildIndexData( + primitive: Primitive, + topology: var VkPrimitiveTopology +): seq[uint32] = + let src = primitive.primitiveSourceIndices() + case primitive.mode.int + of 0: # GL_POINTS + topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST + result = src + of 1: # GL_LINES + topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST + result = src + of 3: # GL_LINE_STRIP + topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP + result = src + of 2: # GL_LINE_LOOP + topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST + if src.len >= 2: + for i in 0 ..< src.len: + result.add(src[i]) + result.add(src[(i + 1) mod src.len]) + of 5: # GL_TRIANGLE_STRIP + topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP + result = src + of 6: # GL_TRIANGLE_FAN + topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + if src.len >= 3: + for i in 1 ..< src.len - 1: + result.add(src[0]) + result.add(src[i]) + result.add(src[i + 1]) + else: + topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST + result = src + +proc ensurePrimitive(renderer: Renderer, primitive: Primitive): VkPrimitive = + if primitive.normals.len == 0 and primitive.mode.int == 4: + primitive.computeSmoothNormals() + + if primitive.data == nil: + primitive.data = VkPrimitive() + result = primitive.data + + if primitive.points.len > result.vertexCapacity: + if result.vertexPtr != nil: + vkUnmapMemory(renderer.ctx.device, result.vertexMemory) + result.vertexPtr = nil + if result.vertexBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, result.vertexBuffer, nil) + vkFreeMemory(renderer.ctx.device, result.vertexMemory, nil) + result.vertexCapacity = max(primitive.points.len, 1) + createBuffer(renderer.ctx, VkDeviceSize(result.vertexCapacity * sizeof(VkVertex)), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + result.vertexBuffer, + result.vertexMemory) + checkVk(vkMapMemory(renderer.ctx.device, result.vertexMemory, + VkDeviceSize(0), VkDeviceSize(result.vertexCapacity * sizeof(VkVertex)), + VkMemoryMapFlags(0), result.vertexPtr.addr), + "Mapping Vulkan vertex buffer") + + var vertices = newSeq[VkVertex](primitive.points.len) + for i in 0 ..< primitive.points.len: + vertices[i] = primitive.vertexAt(i) + if vertices.len > 0: + copyMem(result.vertexPtr, unsafeAddr vertices[0], vertices.len * sizeof(VkVertex)) + + var topology: VkPrimitiveTopology + let indices = primitive.buildIndexData(topology) + result.topology = topology + result.indexCount = indices.len + if indices.len > result.indexCapacity: + if result.indexPtr != nil: + vkUnmapMemory(renderer.ctx.device, result.indexMemory) + result.indexPtr = nil + if result.indexBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, result.indexBuffer, nil) + vkFreeMemory(renderer.ctx.device, result.indexMemory, nil) + result.indexCapacity = max(indices.len, 1) + createBuffer(renderer.ctx, VkDeviceSize(result.indexCapacity * sizeof(uint32)), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + result.indexBuffer, + result.indexMemory) + checkVk(vkMapMemory(renderer.ctx.device, result.indexMemory, + VkDeviceSize(0), VkDeviceSize(result.indexCapacity * sizeof(uint32)), + VkMemoryMapFlags(0), result.indexPtr.addr), + "Mapping Vulkan index buffer") + if indices.len > 0: + copyMem(result.indexPtr, unsafeAddr indices[0], indices.len * sizeof(uint32)) + result.geometryVersion = primitive.geometryVersion + +proc ensureMaterial(renderer: Renderer, material: Material): VkMaterial = + if material == nil: + return nil + if material.data != nil and material.data.materialVersion == material.materialVersion: + return material.data + if material.data != nil: + renderer.releaseMaterial(material.data) + + result = VkMaterial() + material.data = result + + let + baseColor = + if material != nil and material.baseColor != nil: + renderer.uploadImage(material.baseColor) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + metallicRoughness = + if material != nil and material.metallicRoughness != nil: + renderer.uploadImage(material.metallicRoughness) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + occlusion = + if material != nil and material.occlusion != nil: + renderer.uploadImage(material.occlusion) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + emissive = + if material != nil and material.emissive != nil: + renderer.uploadImage(material.emissive) + else: + renderer.uploadSolidImage(rgbx(255, 255, 255, 255)) + normal = + if material != nil and material.normal != nil: + renderer.uploadImage(material.normal) + else: + renderer.uploadSolidImage(rgbx(128, 128, 255, 255)) + shadow = renderer.uploadShadowPlaceholder() + environment = renderer.uploadStudioCube() + + result.textures = @[ + baseColor, + metallicRoughness, + occlusion, + emissive, + normal, + environment, + shadow + ] + + var poolSize = VkDescriptorPoolSize( + `type`: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + descriptorCount: TextureDescriptorCount.uint32 + ) + var poolInfo = VkDescriptorPoolCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + maxSets: 1, + poolSizeCount: 1, + pPoolSizes: poolSize.addr + ) + checkVk(vkCreateDescriptorPool(renderer.ctx.device, poolInfo.addr, nil, + result.descriptorPool.addr), + "Creating Vulkan material descriptor pool") + var layout = renderer.materialSetLayout + var allocInfo = VkDescriptorSetAllocateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + descriptorPool: result.descriptorPool, + descriptorSetCount: 1, + pSetLayouts: layout.addr + ) + checkVk(vkAllocateDescriptorSets(renderer.ctx.device, allocInfo.addr, + result.descriptorSet.addr), + "Allocating Vulkan material descriptor set") + + var imageInfos: array[TextureDescriptorCount, VkDescriptorImageInfo] + var writes: array[TextureDescriptorCount, VkWriteDescriptorSet] + for i, texture in result.textures: + imageInfos[i] = VkDescriptorImageInfo( + sampler: texture.sampler, + imageView: texture.view, + imageLayout: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + ) + writes[i] = VkWriteDescriptorSet( + sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + dstSet: result.descriptorSet, + dstBinding: i.uint32, + dstArrayElement: 0, + descriptorCount: 1, + descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + pImageInfo: imageInfos[i].addr + ) + vkUpdateDescriptorSets(renderer.ctx.device, writes.len.uint32, + writes[0].addr, 0, nil) + result.materialVersion = material.materialVersion + +proc prepareNodeResources(renderer: Renderer, node: Node) = + if node == nil: + return + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.hasGeometry(): + discard renderer.ensurePrimitive(primitive) + discard renderer.ensureMaterial(primitive.material) + for child in node.nodes: + renderer.prepareNodeResources(child) + +proc putTextureTransform( + writer: var Std140Writer, + transform: TextureTransform +) = + writer.putInt(transform.texCoord) + writer.putVec2(transform.offset) + writer.putVec2(transform.scale) + writer.putFloat(transform.rotation) + +proc shadyVertexConstants( + owner, + root: Node, + transform, + view, + proj: Mat4 +): seq[uint32] = + var writer: Std140Writer + let jointMatrices = root.skinMatrices(owner) + writer.putBool(jointMatrices.len > 0) + writer.putMat4Array(jointMatrices, 128) + writer.putMat4(transform) + writer.putMat4(mat4()) + writer.putMat4(proj) + writer.putMat4(view) + writer.finish() + +proc shadyPixelConstants( + primitive: Primitive, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3 +): seq[uint32] = + var writer: Std140Writer + let material = primitive.material + if material != nil: + writer.putTextureTransform(material.baseColorTransform) + writer.putTextureTransform(material.metallicRoughnessTransform) + writer.putTextureTransform(material.normalTransform) + writer.putTextureTransform(material.occlusionTransform) + writer.putTextureTransform(material.emissiveTransform) + writer.putColor(material.baseColorFactor) + writer.putFloat( + if material.alphaMode == MaskAlphaMode: material.alphaCutoff else: -1.0'f32 + ) + writer.putFloat(material.roughnessFactor) + writer.putFloat(material.metallicFactor) + writer.putFloat(material.transmissionFactor) + writer.putFloat(material.occlusionStrength) + writer.putVec3(vec3( + material.emissiveFactor.r, + material.emissiveFactor.g, + material.emissiveFactor.b + )) + writer.putFloat(material.normalScale) + writer.putBool( + material.hasNormalTexture and + primitive.normals.len > 0 and + primitive.tangents.len > 0 + ) + else: + let identityTransform = TextureTransform( + texCoord: 0, + offset: vec2(0, 0), + scale: vec2(1, 1), + rotation: 0.0'f32 + ) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putTextureTransform(identityTransform) + writer.putColor(color(1, 1, 1, 1)) + writer.putFloat(-1.0'f32) + writer.putFloat(1.0'f32) + writer.putFloat(1.0'f32) + writer.putFloat(0.0'f32) + writer.putFloat(1.0'f32) + writer.putVec3(vec3(0, 0, 0)) + writer.putFloat(1.0'f32) + writer.putBool(false) + + writer.putVec3(sunLightDirection) + writer.putVec3(rimLightDirection) + writer.putVec3(cameraPosition) + writer.putColor(sunLightColor) + writer.putColor(rimLightColor) + writer.putFloat(3.0'f32) + writer.putBool(false) + writer.putFloat(0.0005'f32) + writer.putVec2(vec2(1.0'f32 / 2048.0'f32, 1.0'f32 / 2048.0'f32)) + writer.putInt(0) + writer.putColor(tint) + writer.putColor(ambientLightColor) + writer.finish() + +proc createFrameBufferWithData( + renderer: Renderer, + data: openArray[uint32] +): FrameBuffer = + let byteSize = VkDeviceSize(max(4, data.len * sizeof(uint32))) + createBuffer(renderer.ctx, byteSize, + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + result.buffer, + result.memory) + var mapped: pointer + checkVk(vkMapMemory(renderer.ctx.device, result.memory, + VkDeviceSize(0), byteSize, VkMemoryMapFlags(0), mapped.addr), + "Mapping Vulkan uniform buffer") + if data.len > 0: + copyMem(mapped, unsafeAddr data[0], data.len * sizeof(uint32)) + vkUnmapMemory(renderer.ctx.device, result.memory) + renderer.frameBuffers.add(result) + +proc createUniformDescriptorSet( + renderer: Renderer, + vertexConstants, + pixelConstants: openArray[uint32] +): VkDescriptorSet = + let + vertexBuffer = renderer.createFrameBufferWithData(vertexConstants) + pixelBuffer = renderer.createFrameBufferWithData(pixelConstants) + var layout = renderer.uniformSetLayout + var allocInfo = VkDescriptorSetAllocateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + descriptorPool: renderer.frameDescriptorPool, + descriptorSetCount: 1, + pSetLayouts: layout.addr + ) + checkVk(vkAllocateDescriptorSets(renderer.ctx.device, allocInfo.addr, + result.addr), + "Allocating Vulkan uniform descriptor set") + + var bufferInfos = [ + VkDescriptorBufferInfo( + buffer: vertexBuffer.buffer, + offset: VkDeviceSize(0), + range: VkDeviceSize(vertexConstants.len * sizeof(uint32)) + ), + VkDescriptorBufferInfo( + buffer: pixelBuffer.buffer, + offset: VkDeviceSize(0), + range: VkDeviceSize(pixelConstants.len * sizeof(uint32)) + ) + ] + var writes = [ + VkWriteDescriptorSet( + sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + dstSet: result, + dstBinding: VertexUniformBinding.uint32, + dstArrayElement: 0, + descriptorCount: 1, + descriptorType: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + pBufferInfo: bufferInfos[0].addr + ), + VkWriteDescriptorSet( + sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + dstSet: result, + dstBinding: PixelUniformBinding.uint32, + dstArrayElement: 0, + descriptorCount: 1, + descriptorType: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + pBufferInfo: bufferInfos[1].addr + ) + ] + vkUpdateDescriptorSets(renderer.ctx.device, writes.len.uint32, + writes[0].addr, 0, nil) + +proc cmdImageBarrier( + commandBuffer: VkCommandBuffer, + image: VkImage, + aspect: VkImageAspectFlags, + oldLayout, + newLayout: VkImageLayout, + srcStage, + dstStage: VkPipelineStageFlags2, + srcAccess, + dstAccess: VkAccessFlags2 +) = + var barrier = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: srcStage, + srcAccessMask: srcAccess, + dstStageMask: dstStage, + dstAccessMask: dstAccess, + oldLayout: oldLayout, + newLayout: newLayout, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: image, + subresourceRange: VkImageSubresourceRange( + aspectMask: aspect, + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + var dependencyInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: 1, + pImageMemoryBarriers: barrier.addr + ) + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + +proc drawPrimitive( + renderer: Renderer, + commandBuffer: VkCommandBuffer, + primitive: Primitive, + owner, + root: Node, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + blendedPass: bool +) = + if primitive == nil or not primitive.hasGeometry(): + return + + let isBlend = + primitive.material != nil and primitive.material.alphaMode == BlendAlphaMode + if isBlend != blendedPass: + return + + let vkPrimitive = renderer.ensurePrimitive(primitive) + if vkPrimitive.indexCount == 0: + return + let vkMaterial = renderer.ensureMaterial(primitive.material) + let key = PipelineKey( + topology: vkPrimitive.topology.uint32, + doubleSided: primitive.material != nil and primitive.material.doubleSided, + blended: isBlend + ) + let pipeline = renderer.getPipeline(key) + let + vertexConstants = shadyVertexConstants(owner, root, transform, view, proj) + pixelConstants = shadyPixelConstants( + primitive, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition + ) + uniformSet = renderer.createUniformDescriptorSet( + vertexConstants, + pixelConstants + ) + var descriptorSets = [vkMaterial.descriptorSet, uniformSet] + var vertexBuffers = [vkPrimitive.vertexBuffer] + var offsets = [VkDeviceSize(0)] + + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + renderer.pipelineLayout, 0, descriptorSets.len.uint32, + descriptorSets[0].addr, 0, nil) + vkCmdBindVertexBuffers(commandBuffer, 0, 1, + vertexBuffers[0].addr, offsets[0].addr) + vkCmdBindIndexBuffer(commandBuffer, vkPrimitive.indexBuffer, + VkDeviceSize(0), VK_INDEX_TYPE_UINT32) + vkCmdDrawIndexed(commandBuffer, vkPrimitive.indexCount.uint32, 1, 0, 0, 0) + +proc collectOrDrawNode( + renderer: Renderer, + commandBuffer: VkCommandBuffer, + node, + root: Node, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + blended: var seq[BlendEntry] +) = + if node == nil or not node.visible: + return + node.mat = transform * node.trs + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.material != nil and primitive.material.alphaMode == BlendAlphaMode: + blended.add(BlendEntry(node: node, primitive: primitive, transform: node.mat)) + else: + renderer.drawPrimitive( + commandBuffer, + primitive, + node, + root, + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blendedPass = false + ) + for child in node.nodes: + renderer.collectOrDrawNode( + commandBuffer, + child, + root, + node.mat, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blended + ) + +proc recordFrame( + renderer: Renderer, + commandBuffer: VkCommandBuffer, + imageIndex: uint32, + clearColor: Color, + node: Node, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3 +) = + discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) + var beginInfo = VkCommandBufferBeginInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO + ) + checkVk(vkBeginCommandBuffer(commandBuffer, beginInfo.addr), + "Beginning Vulkan draw command buffer") + + commandBuffer.cmdImageBarrier( + renderer.ctx.swapChainImages[imageIndex], + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + renderer.imageLayouts[imageIndex], + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + VkAccessFlags2(VK_ACCESS_2_NONE), + VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT) + ) + if renderer.msaaEnabled: + commandBuffer.cmdImageBarrier( + renderer.colorImages[imageIndex], + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + VkAccessFlags2(VK_ACCESS_2_NONE), + VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT) + ) + commandBuffer.cmdImageBarrier( + renderer.depthImages[imageIndex], + VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT), + VkAccessFlags2(VK_ACCESS_2_NONE), + VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT) + ) + + var + colorClear = VkClearValue(color: VkClearColorValue( + float32: [clearColor.r, clearColor.g, clearColor.b, clearColor.a])) + depthClear = VkClearValue( + depthStencil: VkClearDepthStencilValue(depth: 1.0'f32, stencil: 0)) + colorAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: + if renderer.msaaEnabled: + renderer.colorViews[imageIndex] + else: + renderer.imageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + resolveMode: + if renderer.msaaEnabled: + VK_RESOLVE_MODE_AVERAGE_BIT + else: + VK_RESOLVE_MODE_NONE, + resolveImageView: + if renderer.msaaEnabled: + renderer.imageViews[imageIndex] + else: + VkImageView(0), + resolveImageLayout: + if renderer.msaaEnabled: + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + else: + VK_IMAGE_LAYOUT_UNDEFINED, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: + if renderer.msaaEnabled: + VK_ATTACHMENT_STORE_OP_DONT_CARE + else: + VK_ATTACHMENT_STORE_OP_STORE, + clearValue: colorClear + ) + depthAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: renderer.depthViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: VK_ATTACHMENT_STORE_OP_DONT_CARE, + clearValue: depthClear + ) + renderingInfo = VkRenderingInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_INFO, + renderArea: VkRect2D( + offset: VkOffset2D(x: 0, y: 0), + extent: renderer.ctx.swapChainExtent), + layerCount: 1, + colorAttachmentCount: 1, + pColorAttachments: colorAttachment.addr, + pDepthAttachment: depthAttachment.addr + ) + viewport = VkViewport( + x: 0, + y: 0, + width: renderer.ctx.swapChainExtent.width.float32, + height: renderer.ctx.swapChainExtent.height.float32, + minDepth: 0, + maxDepth: 1 + ) + scissor = VkRect2D( + offset: VkOffset2D(x: 0, y: 0), + extent: renderer.ctx.swapChainExtent + ) + + vkCmdBeginRendering(commandBuffer, renderingInfo.addr) + vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) + vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) + + if node != nil and node.visible: + node.updateTransforms(transform, true) + var blended: seq[BlendEntry] + renderer.collectOrDrawNode( + commandBuffer, + node, + node, + transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blended + ) + if blended.len > 0: + blended.sort(proc(a, b: BlendEntry): int = + let + pa = (a.transform * vec4(0, 0, 0, 1)).xyz + pb = (b.transform * vec4(0, 0, 0, 1)).xyz + da = (cameraPosition - pa).lengthSq + db = (cameraPosition - pb).lengthSq + if da > db: -1 elif da < db: 1 else: 0 + ) + for entry in blended: + renderer.drawPrimitive( + commandBuffer, + entry.primitive, + entry.node, + node, + entry.transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition, + blendedPass = true + ) + + vkCmdEndRendering(commandBuffer) + + commandBuffer.cmdImageBarrier( + renderer.ctx.swapChainImages[imageIndex], + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT), + VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + VkAccessFlags2(VK_ACCESS_2_TRANSFER_READ_BIT) + ) + var copyRegion = VkBufferImageCopy( + bufferOffset: VkDeviceSize(0), + imageSubresource: VkImageSubresourceLayers( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + mipLevel: 0, + baseArrayLayer: 0, + layerCount: 1 + ), + imageExtent: VkExtent3D( + width: renderer.ctx.swapChainExtent.width, + height: renderer.ctx.swapChainExtent.height, + depth: 1 + ) + ) + vkCmdCopyImageToBuffer(commandBuffer, + renderer.ctx.swapChainImages[imageIndex], + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + renderer.readbackBuffer, + 1, + copyRegion.addr) + commandBuffer.cmdImageBarrier( + renderer.ctx.swapChainImages[imageIndex], + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT), + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + VkAccessFlags2(VK_ACCESS_2_TRANSFER_READ_BIT), + VkAccessFlags2(VK_ACCESS_2_NONE) + ) + renderer.imageLayouts[imageIndex] = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR + checkVk(vkEndCommandBuffer(commandBuffer), "Ending Vulkan draw command buffer") + +proc drawPbrFrame*( + renderer: Renderer, + node: Node, + size: IVec2, + clearColor: Color, + transform, + view, + proj: Mat4, + tint: Color, + ambientLightColor: Color, + sunLightDirection: Vec3, + sunLightColor: Color, + rimLightDirection: Vec3, + rimLightColor: Color, + cameraPosition: Vec3, + vsync = true +) = + ## Draws a full glTF PBR frame through Vulkan. + discard vsync + renderer.resize(size) + renderer.resetFrameResources() + + if node != nil: + renderer.prepareNodeResources(node) + + let frame = renderer.ctx.currentFrame + let fence = renderer.ctx.inFlightFences[frame] + discard vkWaitForFences(renderer.ctx.device, 1, unsafeAddr fence, + VkBool32(VK_TRUE), uint64.high) + discard vkResetFences(renderer.ctx.device, 1, unsafeAddr fence) + + var imageIndex: uint32 + let acquireResult = vkAcquireNextImageKHR( + renderer.ctx.device, + renderer.ctx.swapChain, + uint64.high, + renderer.ctx.imageAvailableSemaphores[frame], + VkFence(0), + imageIndex.addr + ) + if requiresSwapChainRecreate(acquireResult): + renderer.resize(size) + return + checkVk(acquireResult, "Acquiring Vulkan swapchain image") + + let commandBuffer = renderer.commandBuffers[imageIndex] + renderer.recordFrame( + commandBuffer, + imageIndex, + clearColor, + node, + transform, + view, + proj, + tint, + ambientLightColor, + sunLightDirection, + sunLightColor, + rimLightDirection, + rimLightColor, + cameraPosition + ) + + var + waitInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: renderer.ctx.imageAvailableSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + commandBufferInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: commandBuffer, + deviceMask: 0 + ) + signalInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: renderer.ctx.renderFinishedSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + submitInfo = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + waitSemaphoreInfoCount: 1, + pWaitSemaphoreInfos: waitInfo.addr, + commandBufferInfoCount: 1, + pCommandBufferInfos: commandBufferInfo.addr, + signalSemaphoreInfoCount: 1, + pSignalSemaphoreInfos: signalInfo.addr + ) + checkVk(vkQueueSubmit2(renderer.ctx.graphicsQueue, 1, + submitInfo.addr, fence), + "Submitting Vulkan draw command buffer") + discard vkWaitForFences(renderer.ctx.device, 1, unsafeAddr fence, + VkBool32(VK_TRUE), uint64.high) + + var + swapChains = [renderer.ctx.swapChain] + presentInfo = VkPresentInfoKHR( + sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + waitSemaphoreCount: 1, + pWaitSemaphores: signalInfo.semaphore.addr, + swapchainCount: 1, + pSwapchains: swapChains[0].addr, + pImageIndices: imageIndex.addr + ) + let presentResult = vkQueuePresentKHR(renderer.ctx.presentQueue, + presentInfo.addr) + if requiresSwapChainRecreate(presentResult): + renderer.resize(size) + else: + checkVk(presentResult, "Presenting Vulkan frame") + renderer.ctx.currentFrame = (renderer.ctx.currentFrame + 1) mod FRAME_COUNT + +proc captureScreenshot*(renderer: Renderer): Image = + ## Reads the most recently rendered Vulkan frame. + let + width = renderer.readbackSize.x.int + height = renderer.readbackSize.y.int + swapBgra = + renderer.ctx.swapChainImageFormat == VK_FORMAT_B8G8R8A8_UNORM or + renderer.ctx.swapChainImageFormat == VK_FORMAT_B8G8R8A8_SRGB + result = newImage(width, height) + var mapped: pointer + checkVk(vkMapMemory(renderer.ctx.device, renderer.readbackMemory, + VkDeviceSize(0), VkDeviceSize(width * height * 4), + VkMemoryMapFlags(0), mapped.addr), + "Mapping Vulkan readback buffer") + let pixels = cast[ptr UncheckedArray[uint8]](mapped) + for y in 0 ..< height: + for x in 0 ..< width: + let src = (y * width + x) * 4 + let alpha = pixels[src + 3] + let + red = if swapBgra: pixels[src + 2] else: pixels[src + 0] + green = pixels[src + 1] + blue = if swapBgra: pixels[src + 0] else: pixels[src + 2] + result.data[result.dataIndex(x, y)] = rgbx( + min(red, alpha), + min(green, alpha), + min(blue, alpha), + alpha + ) + vkUnmapMemory(renderer.ctx.device, renderer.readbackMemory) + +proc clearNode*(renderer: Renderer, node: Node) = + ## Releases Vulkan resources associated with a loaded node tree. + if node == nil: + return + discard vkDeviceWaitIdle(renderer.ctx.device) + if node.mesh != nil: + for primitive in node.mesh.primitives: + if primitive.data != nil: + renderer.releasePrimitive(primitive.data) + primitive.data = nil + if primitive.material != nil: + if primitive.material.data != nil: + renderer.releaseMaterial(primitive.material.data) + primitive.material.data = nil + for child in node.nodes: + renderer.clearNode(child) + +proc shutdown*(renderer: Renderer) = + ## Releases all Vulkan resources owned by the renderer. + if renderer == nil: + return + discard vkDeviceWaitIdle(renderer.ctx.device) + renderer.releaseFrameBuffers() + renderer.destroySwapChainResources() + if renderer.readbackBuffer.int64 != 0: + vkDestroyBuffer(renderer.ctx.device, renderer.readbackBuffer, nil) + renderer.readbackBuffer = VkBuffer(0) + if renderer.readbackMemory.int64 != 0: + vkFreeMemory(renderer.ctx.device, renderer.readbackMemory, nil) + renderer.readbackMemory = VkDeviceMemory(0) + if renderer.frameDescriptorPool.int64 != 0: + vkDestroyDescriptorPool(renderer.ctx.device, renderer.frameDescriptorPool, nil) + renderer.frameDescriptorPool = VkDescriptorPool(0) + if renderer.pipelineLayout.int64 != 0: + vkDestroyPipelineLayout(renderer.ctx.device, renderer.pipelineLayout, nil) + renderer.pipelineLayout = VkPipelineLayout(0) + if renderer.uniformSetLayout.int64 != 0: + vkDestroyDescriptorSetLayout(renderer.ctx.device, renderer.uniformSetLayout, nil) + renderer.uniformSetLayout = VkDescriptorSetLayout(0) + if renderer.materialSetLayout.int64 != 0: + vkDestroyDescriptorSetLayout(renderer.ctx.device, renderer.materialSetLayout, nil) + renderer.materialSetLayout = VkDescriptorSetLayout(0) + cleanup(renderer.ctx) + +proc beginFrame*(renderer: Renderer; window: Window; size: IVec2) = + discard renderer + discard window + discard size + +proc clearScreen*(renderer: Renderer; color: ColorRGBX) = + discard renderer + discard color + +proc clearScreen*(renderer: Renderer; color: Color) = + discard renderer + discard color + +proc render*(renderer: Renderer; node: Node; params: RenderParams) = + renderer.drawPbrFrame( + node, + params.size, + params.clearColor, + params.transform, + params.view, + params.proj, + tint = params.tint, + ambientLightColor = params.ambientLightColor, + sunLightDirection = params.sunLightDirection, + sunLightColor = params.sunLightColor, + rimLightDirection = params.rimLightDirection, + rimLightColor = params.rimLightColor, + cameraPosition = params.cameraPosition, + vsync = params.vsync + ) + +proc render*(renderer: Renderer; file: GltfFile; params: RenderParams) = + if file != nil: + renderer.render(file.root, params) + +proc endFrame*(renderer: Renderer) = + discard renderer + +proc release*(renderer: Renderer; node: Node) = + renderer.clearNode(node) diff --git a/src/gltf/common.nim b/src/gltf/common.nim index 2d1aa1d..ff8901e 100644 --- a/src/gltf/common.nim +++ b/src/gltf/common.nim @@ -1,9 +1,52 @@ import - chroma, opengl, pixie, vmath + chroma, pixie, vmath + +when defined(useDirectX): + import backends/directx/common +elif defined(useVulkan): + import backends/vulkan/common +elif defined(useMetal4): + import backends/metal/common +else: + import backends/opengl/common type GltfError* = object of CatchableError + ComponentType* = enum + ByteComponent = 5120 + UnsignedByteComponent = 5121 + ShortComponent = 5122 + UnsignedShortComponent = 5123 + UnsignedIntComponent = 5125 + FloatComponent = 5126 + + TextureMagFilter* = enum + NearestMagFilter = 9728 + LinearMagFilter = 9729 + + TextureMinFilter* = enum + NearestMinFilter = 9728 + LinearMinFilter = 9729 + NearestMipmapNearestMinFilter = 9984 + LinearMipmapNearestMinFilter = 9985 + NearestMipmapLinearMinFilter = 9986 + LinearMipmapLinearMinFilter = 9987 + + TextureWrap* = enum + ClampToEdgeWrap = 33071 + MirroredRepeatWrap = 33648 + RepeatWrap = 10497 + + PrimitiveMode* = enum + PointsMode = 0 + LinesMode = 1 + LineLoopMode = 2 + LineStripMode = 3 + TrianglesMode = 4 + TriangleStripMode = 5 + TriangleFanMode = 6 + TextureTransform* = object texCoord*: int offset*: Vec2 @@ -11,16 +54,16 @@ type rotation*: float32 TextureSampler* = object - magFilter*: GLint - minFilter*: GLint - wrapS*: GLint - wrapT*: GLint + magFilter*: TextureMagFilter + minFilter*: TextureMinFilter + wrapS*: TextureWrap + wrapT*: TextureWrap AlphaMode* = enum OpaqueAlphaMode, MaskAlphaMode, BlendAlphaMode CameraKind* = enum - ckPerspective, ckOrthographic + PerspectiveLens, OrthographicLens PerspectiveCamera* = object yfov*: float32 @@ -63,19 +106,9 @@ type indices32*: seq[uint32] morphTargets*: seq[MorphTarget] material*: Material - mode*: GLenum - - uploaded*: bool - vertexArrayId*: GLuint - pointsId*: GLuint - uvsId*: GLuint - uvs1Id*: GLuint - normalsId*: GLuint - tangentsId*: GLuint - colorsId*: GLuint - jointIdsId*: GLuint - jointWeightsId*: GLuint - indicesId*: GLuint + mode*: PrimitiveMode + geometryVersion*: uint64 + data*: PrimitiveData Mesh* = ref object name*: string @@ -148,28 +181,33 @@ type Material* = ref object name*: string baseColor*: Image + baseColorKtx2*: string baseColorName*: string baseColorTransform*: TextureTransform baseColorSampler*: TextureSampler baseColorFactor*: Color metallicRoughness*: Image + metallicRoughnessKtx2*: string metallicRoughnessName*: string metallicRoughnessTransform*: TextureTransform metallicRoughnessSampler*: TextureSampler metallicFactor*: float32 roughnessFactor*: float32 normal*: Image + normalKtx2*: string normalName*: string normalTransform*: TextureTransform normalSampler*: TextureSampler hasNormalTexture*: bool normalScale*: float32 occlusion*: Image + occlusionKtx2*: string occlusionName*: string occlusionTransform*: TextureTransform occlusionSampler*: TextureSampler occlusionStrength*: float32 emissive*: Image + emissiveKtx2*: string emissiveName*: string emissiveTransform*: TextureTransform emissiveSampler*: TextureSampler @@ -179,12 +217,8 @@ type alphaCutoff*: float32 doubleSided*: bool transmissionFactor*: float32 - - baseColorId*: GLuint - metallicRoughnessId*: GLuint - normalId*: GLuint - occlusionId*: GLuint - emissiveId*: GLuint + materialVersion*: uint64 + data*: MaterialData AABounds* = object min*, max*: Vec3 @@ -206,3 +240,33 @@ type cameras*: seq[Camera] skins*: seq[Skin] unsupportedUsedExtensions*: seq[string] + sceneVersion*: uint64 + data*: GltfFileData + + DebugView* = enum + dvLit, + dvUnlit, + dvNormals, + dvAoBake, + dvMetallic, + dvSpecular + + RenderParams* = object + size*: IVec2 + clearColor*: Color + transform*: Mat4 + view*: Mat4 + proj*: Mat4 + tint*: Color + useTrs*: bool + ambientLightColor*: Color + sunLightDirection*: Vec3 + sunLightColor*: Color + rimLightDirection*: Vec3 + rimLightColor*: Color + debugView*: DebugView + cameraPosition*: Vec3 + useShadows*: bool + drawSkybox*: bool + skyboxLod*: float32 + vsync*: bool diff --git a/src/gltf/internal.nim b/src/gltf/internal.nim index 23f3304..3c9facd 100644 --- a/src/gltf/internal.nim +++ b/src/gltf/internal.nim @@ -1,5 +1,6 @@ import - chroma, opengl, vmath + chroma, vmath, + common type BufferView* = object @@ -9,7 +10,7 @@ type SparseIndices* = object bufferView*: int byteOffset*: int - componentType*: GLenum + componentType*: ComponentType SparseValues* = object bufferView*: int @@ -27,7 +28,7 @@ type Accessor* = object bufferView*: int byteOffset*, count*: int - componentType*: GLenum + componentType*: ComponentType kind*: AccessorKind normalized*: bool sparse*: SparseInfo @@ -37,7 +38,9 @@ type sampler*: int Sampler* = object - magFilter*, minFilter*, wrapS*, wrapT*: GLint + magFilter*: TextureMagFilter + minFilter*: TextureMinFilter + wrapS*, wrapT*: TextureWrap MaterialTexture* = object index*: int @@ -83,7 +86,7 @@ type PrimitiveInfo* = object attributes*: PrimitiveAttributes indices*, material*: int - mode*: GLenum + mode*: PrimitiveMode morphTargets*: seq[MorphTargetInfo] SkinInfo* = object diff --git a/src/gltf/ktx2.nim b/src/gltf/ktx2.nim index 194f8e2..96d71cb 100644 --- a/src/gltf/ktx2.nim +++ b/src/gltf/ktx2.nim @@ -97,12 +97,32 @@ type proc defaultKtx2Sampler(): TextureSampler = TextureSampler( - magFilter: GL_LINEAR, - minFilter: GL_LINEAR_MIPMAP_LINEAR, - wrapS: GL_REPEAT, - wrapT: GL_REPEAT + magFilter: LinearMagFilter, + minFilter: LinearMipmapLinearMinFilter, + wrapS: RepeatWrap, + wrapT: RepeatWrap ) +proc glValue(filter: TextureMagFilter): GLint = + case filter + of NearestMagFilter: GL_NEAREST.GLint + of LinearMagFilter: GL_LINEAR.GLint + +proc glValue(filter: TextureMinFilter): GLint = + case filter + of NearestMinFilter: GL_NEAREST.GLint + of LinearMinFilter: GL_LINEAR.GLint + of NearestMipmapNearestMinFilter: GL_NEAREST_MIPMAP_NEAREST.GLint + of LinearMipmapNearestMinFilter: GL_LINEAR_MIPMAP_NEAREST.GLint + of NearestMipmapLinearMinFilter: GL_NEAREST_MIPMAP_LINEAR.GLint + of LinearMipmapLinearMinFilter: GL_LINEAR_MIPMAP_LINEAR.GLint + +proc glValue(wrap: TextureWrap): GLint = + case wrap + of RepeatWrap: GL_REPEAT.GLint + of ClampToEdgeWrap: GL_CLAMP_TO_EDGE.GLint + of MirroredRepeatWrap: GL_MIRRORED_REPEAT.GLint + proc raiseKtx2Error(message: string) {.noreturn.} = raise newException(GltfError, "KTX2: " & message) @@ -648,10 +668,10 @@ proc uploadKtx2*(info: Ktx2TextureInfo, data: string, sampler = defaultKtx2Sampl cast[pointer](levelPtr) ) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, sampler.magFilter) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, sampler.minFilter) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sampler.wrapS) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, sampler.wrapT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, sampler.magFilter.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, sampler.minFilter.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sampler.wrapS.glValue) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, sampler.wrapT.glValue) let err = glGetError() glBindTexture(GL_TEXTURE_2D, 0) diff --git a/src/gltf/models.nim b/src/gltf/models.nim index 8e7ac22..c49b9c1 100644 --- a/src/gltf/models.nim +++ b/src/gltf/models.nim @@ -1,6 +1,6 @@ import std/[strformat, math], - vmath, chroma, pixie, opengl, + vmath, chroma, pixie, common proc trs*(node: Node): Mat4 @@ -31,39 +31,42 @@ proc shallowCopy*(node: Node): Node = proc defaultTextureSampler*(): TextureSampler = TextureSampler( - magFilter: GL_LINEAR, - minFilter: GL_LINEAR_MIPMAP_LINEAR, - wrapS: GL_REPEAT, - wrapT: GL_REPEAT + magFilter: LinearMagFilter, + minFilter: LinearMipmapLinearMinFilter, + wrapS: RepeatWrap, + wrapT: RepeatWrap ) -proc wrapModeName(mode: GLint): string = +proc wrapModeName(mode: TextureWrap): string = case mode - of GL_REPEAT: + of RepeatWrap: "repeat" - of GL_CLAMP_TO_EDGE: + of ClampToEdgeWrap: "clamp_to_edge" - of GL_MIRRORED_REPEAT: + of MirroredRepeatWrap: "mirrored_repeat" - else: - $mode -proc filterName(mode: GLint): string = +proc filterName(mode: TextureMagFilter): string = + case mode + of NearestMagFilter: + "nearest" + of LinearMagFilter: + "linear" + +proc filterName(mode: TextureMinFilter): string = case mode - of GL_NEAREST: + of NearestMinFilter: "nearest" - of GL_LINEAR: + of LinearMinFilter: "linear" - of GL_NEAREST_MIPMAP_NEAREST: + of NearestMipmapNearestMinFilter: "nearest_mipmap_nearest" - of GL_LINEAR_MIPMAP_NEAREST: + of LinearMipmapNearestMinFilter: "linear_mipmap_nearest" - of GL_NEAREST_MIPMAP_LINEAR: + of NearestMipmapLinearMinFilter: "nearest_mipmap_linear" - of GL_LINEAR_MIPMAP_LINEAR: + of LinearMipmapLinearMinFilter: "linear_mipmap_linear" - else: - $mode proc `$`*(sampler: TextureSampler): string = &"(magFilter: {filterName(sampler.magFilter)}, " & @@ -71,25 +74,23 @@ proc `$`*(sampler: TextureSampler): string = &"wrapS: {wrapModeName(sampler.wrapS)}, " & &"wrapT: {wrapModeName(sampler.wrapT)})" -proc primitiveModeName*(mode: GLenum): string = +proc primitiveModeName*(mode: PrimitiveMode): string = ## Returns a readable primitive mode label. case mode - of GL_POINTS: + of PointsMode: "POINTS" - of GL_LINES: + of LinesMode: "LINES" - of GL_LINE_LOOP: + of LineLoopMode: "LINE_LOOP" - of GL_LINE_STRIP: + of LineStripMode: "LINE_STRIP" - of GL_TRIANGLES: + of TrianglesMode: "TRIANGLES" - of GL_TRIANGLE_STRIP: + of TriangleStripMode: "TRIANGLE_STRIP" - of GL_TRIANGLE_FAN: + of TriangleFanMode: "TRIANGLE_FAN" - else: - $mode.int proc updateTransforms*( node: Node, @@ -128,6 +129,21 @@ proc hasGeometry*(node: Node): bool = return true false +proc markGeometryDirty*(primitive: Primitive) = + ## Marks CPU geometry changed so the selected backend can refresh resources. + if primitive != nil: + inc primitive.geometryVersion + +proc markMaterialDirty*(material: Material) = + ## Marks CPU material data changed so the selected backend can refresh resources. + if material != nil: + inc material.materialVersion + +proc markSceneDirty*(file: GltfFile) = + ## Marks CPU scene data changed so the selected backend can refresh resources. + if file != nil: + inc file.sceneVersion + proc computeSmoothNormals*(prim: Primitive) = ## Generates smooth normals by accumulating face normals per vertex. if prim.normals.len > 0 or prim.points.len == 0: @@ -177,421 +193,6 @@ proc ensureNormals*(node: Node) = for child in node.nodes: child.ensureNormals() -proc uploadTextureToGpu( - textureId: var GLuint, - image: Image, - sampler = defaultTextureSampler() -) = - ## Uploads a texture to the GPU. - glGenTextures(1, textureId.addr) - glBindTexture(GL_TEXTURE_2D, textureId) - if image.isOpaque(): - var data = newSeq[uint8](image.width * image.height * 3) - for i, rgbx in image.data: - data[i * 3 + 0] = rgbx.r - data[i * 3 + 1] = rgbx.g - data[i * 3 + 2] = rgbx.b - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RGB.GLint, - image.width.GLint, - image.height.GLint, - 0, - GL_RGB, - GL_UNSIGNED_BYTE, - data[0].addr - ) - else: - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_RGBA.GLint, - image.width.GLint, - image.height.GLint, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - image.data[0].addr - ) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, sampler.magFilter) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, sampler.minFilter) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sampler.wrapS) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, sampler.wrapT) - glGenerateMipmap(GL_TEXTURE_2D) - -proc uploadMaterialToGpu(material: Material) = - if material == nil: - return - if material.baseColor != nil and material.baseColorId == 0: - uploadTextureToGpu( - material.baseColorId, - material.baseColor, - material.baseColorSampler - ) - if material.metallicRoughness != nil and material.metallicRoughnessId == 0: - uploadTextureToGpu( - material.metallicRoughnessId, - material.metallicRoughness, - material.metallicRoughnessSampler - ) - if material.normal != nil and material.normalId == 0: - uploadTextureToGpu( - material.normalId, - material.normal, - material.normalSampler - ) - if material.occlusion != nil and material.occlusionId == 0: - uploadTextureToGpu( - material.occlusionId, - material.occlusion, - material.occlusionSampler - ) - if material.emissive != nil and material.emissiveId == 0: - uploadTextureToGpu( - material.emissiveId, - material.emissive, - material.emissiveSampler - ) - -proc clearMaterialFromGpu(material: Material) = - if material == nil: - return - if material.baseColorId != 0.GLuint: - glDeleteTextures(1, material.baseColorId.addr) - material.baseColorId = 0 - if material.metallicRoughnessId != 0.GLuint: - glDeleteTextures(1, material.metallicRoughnessId.addr) - material.metallicRoughnessId = 0 - if material.normalId != 0.GLuint: - glDeleteTextures(1, material.normalId.addr) - material.normalId = 0 - if material.occlusionId != 0.GLuint: - glDeleteTextures(1, material.occlusionId.addr) - material.occlusionId = 0 - if material.emissiveId != 0.GLuint: - glDeleteTextures(1, material.emissiveId.addr) - material.emissiveId = 0 - -proc uploadToGpu*(primitive: Primitive) = - ## Upload the primitive data to the GPU. - if primitive == nil or primitive.uploaded: - return - - glGenVertexArrays(1, primitive.vertexArrayId.addr) - glBindVertexArray(primitive.vertexArrayId) - - if primitive.indices32.len > 0: - glGenBuffers(1, primitive.indicesId.addr) - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glBufferData( - GL_ELEMENT_ARRAY_BUFFER, - primitive.indices32.len * sizeof(uint32), - primitive.indices32[0].addr, - GL_STATIC_DRAW - ) - elif primitive.indices16.len > 0: - glGenBuffers(1, primitive.indicesId.addr) - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glBufferData( - GL_ELEMENT_ARRAY_BUFFER, - primitive.indices16.len * sizeof(uint16), - primitive.indices16[0].addr, - GL_STATIC_DRAW - ) - - if primitive.points.len > 0: - glGenBuffers(1, primitive.pointsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.pointsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.points.len * sizeof(Vec3), - primitive.points[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(0) - glVertexAttribPointer(0, 3, cGL_FLOAT, GL_FALSE, 0, nil) - - if primitive.uvs.len > 0: - glGenBuffers(1, primitive.uvsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.uvsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.uvs.len * sizeof(Vec2), - primitive.uvs[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(3) - glVertexAttribPointer(3, 2, cGL_FLOAT, GL_FALSE, 0, nil) - - if primitive.uvs1.len > 0: - glGenBuffers(1, primitive.uvs1Id.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.uvs1Id) - glBufferData( - GL_ARRAY_BUFFER, - primitive.uvs1.len * sizeof(Vec2), - primitive.uvs1[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(7) - glVertexAttribPointer(7, 2, cGL_FLOAT, GL_FALSE, 0, nil) - else: - glDisableVertexAttribArray(7) - glVertexAttrib2f(7, 0.0, 0.0) - - if primitive.normals.len > 0: - glGenBuffers(1, primitive.normalsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.normalsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.normals.len * sizeof(Vec3), - primitive.normals[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(2) - glVertexAttribPointer(2, 3, cGL_FLOAT, GL_FALSE, 0, nil) - - if primitive.tangents.len > 0: - glGenBuffers(1, primitive.tangentsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.tangentsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.tangents.len * sizeof(Vec4), - primitive.tangents[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(4) - glVertexAttribPointer(4, 4, cGL_FLOAT, GL_FALSE, 0, nil) - - if primitive.colors.len > 0: - glGenBuffers(1, primitive.colorsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.colorsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.colors.len * sizeof(ColorRGBX), - primitive.colors[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(1) - glVertexAttribPointer(1, 4, cGL_UNSIGNED_BYTE, GL_TRUE, 0, nil) - else: - glDisableVertexAttribArray(1) - glVertexAttrib4f(1, 1.0, 1.0, 1.0, 1.0) - - if primitive.jointIds.len > 0: - glGenBuffers(1, primitive.jointIdsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.jointIdsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.jointIds.len * sizeof(JointIds), - primitive.jointIds[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(5) - glVertexAttribIPointer(5, 4, cGL_UNSIGNED_SHORT, 0, nil) - else: - glDisableVertexAttribArray(5) - glVertexAttribI4ui(5, 0, 0, 0, 0) - - if primitive.jointWeights.len > 0: - glGenBuffers(1, primitive.jointWeightsId.addr) - glBindBuffer(GL_ARRAY_BUFFER, primitive.jointWeightsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.jointWeights.len * sizeof(Vec4), - primitive.jointWeights[0].addr, - GL_STATIC_DRAW - ) - glEnableVertexAttribArray(6) - glVertexAttribPointer(6, 4, cGL_FLOAT, GL_FALSE, 0, nil) - else: - glDisableVertexAttribArray(6) - glVertexAttrib4f(6, 0.0, 0.0, 0.0, 0.0) - - uploadMaterialToGpu(primitive.material) - primitive.uploaded = true - -proc clearFromGpu*(primitive: Primitive) = - ## Clear primitive data from the GPU. - if primitive == nil: - return - if primitive.uploaded: - glDeleteVertexArrays(1, primitive.vertexArrayId.addr) - if primitive.indices16.len > 0 or primitive.indices32.len > 0: - glDeleteBuffers(1, primitive.indicesId.addr) - if primitive.points.len > 0: - glDeleteBuffers(1, primitive.pointsId.addr) - if primitive.uvs.len > 0: - glDeleteBuffers(1, primitive.uvsId.addr) - if primitive.uvs1.len > 0: - glDeleteBuffers(1, primitive.uvs1Id.addr) - if primitive.normals.len > 0: - glDeleteBuffers(1, primitive.normalsId.addr) - if primitive.tangents.len > 0: - glDeleteBuffers(1, primitive.tangentsId.addr) - if primitive.colors.len > 0: - glDeleteBuffers(1, primitive.colorsId.addr) - if primitive.jointIds.len > 0: - glDeleteBuffers(1, primitive.jointIdsId.addr) - if primitive.jointWeights.len > 0: - glDeleteBuffers(1, primitive.jointWeightsId.addr) - primitive.vertexArrayId = 0 - primitive.pointsId = 0 - primitive.uvsId = 0 - primitive.uvs1Id = 0 - primitive.normalsId = 0 - primitive.tangentsId = 0 - primitive.colorsId = 0 - primitive.jointIdsId = 0 - primitive.jointWeightsId = 0 - primitive.indicesId = 0 - clearMaterialFromGpu(primitive.material) - primitive.uploaded = false - -proc updateOnGpu*(primitive: Primitive) = - ## Update primitive data on the GPU. - if primitive == nil: - return - if not primitive.uploaded: - primitive.uploadToGpu() - else: - glBindVertexArray(primitive.vertexArrayId) - - if primitive.points.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.pointsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.points.len * sizeof(Vec3), - primitive.points[0].addr, - GL_STATIC_DRAW - ) - - if primitive.uvs.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.uvsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.uvs.len * sizeof(Vec2), - primitive.uvs[0].addr, - GL_STATIC_DRAW - ) - - if primitive.uvs1.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.uvs1Id) - glBufferData( - GL_ARRAY_BUFFER, - primitive.uvs1.len * sizeof(Vec2), - primitive.uvs1[0].addr, - GL_STATIC_DRAW - ) - - if primitive.normals.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.normalsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.normals.len * sizeof(Vec3), - primitive.normals[0].addr, - GL_STATIC_DRAW - ) - - if primitive.tangents.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.tangentsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.tangents.len * sizeof(Vec4), - primitive.tangents[0].addr, - GL_STATIC_DRAW - ) - - if primitive.colors.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.colorsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.colors.len * sizeof(ColorRGBX), - primitive.colors[0].addr, - GL_STATIC_DRAW - ) - - if primitive.jointIds.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.jointIdsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.jointIds.len * sizeof(JointIds), - primitive.jointIds[0].addr, - GL_STATIC_DRAW - ) - - if primitive.jointWeights.len > 0: - glBindBuffer(GL_ARRAY_BUFFER, primitive.jointWeightsId) - glBufferData( - GL_ARRAY_BUFFER, - primitive.jointWeights.len * sizeof(Vec4), - primitive.jointWeights[0].addr, - GL_STATIC_DRAW - ) - - if primitive.indices32.len > 0: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glBufferData( - GL_ELEMENT_ARRAY_BUFFER, - primitive.indices32.len * sizeof(uint32), - primitive.indices32[0].addr, - GL_STATIC_DRAW - ) - elif primitive.indices16.len > 0: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glBufferData( - GL_ELEMENT_ARRAY_BUFFER, - primitive.indices16.len * sizeof(uint16), - primitive.indices16[0].addr, - GL_STATIC_DRAW - ) - -proc uploadToGpu*(mesh: Mesh) = - if mesh == nil: - return - for primitive in mesh.primitives: - primitive.uploadToGpu() - -proc clearFromGpu*(mesh: Mesh) = - if mesh == nil: - return - for primitive in mesh.primitives: - primitive.clearFromGpu() - -proc updateOnGpu*(mesh: Mesh) = - if mesh == nil: - return - for primitive in mesh.primitives: - primitive.updateOnGpu() - -proc uploadToGpu*(node: Node) = - ## Upload the node subtree to the GPU. - if node == nil: - return - node.mesh.uploadToGpu() - for child in node.nodes: - child.uploadToGpu() - -proc clearFromGpu*(node: Node) = - ## Clear the node subtree from the GPU. - if node == nil: - return - node.mesh.clearFromGpu() - for child in node.nodes: - child.clearFromGpu() - -proc updateOnGpu*(node: Node) = - ## Update the node subtree on the GPU. - if node == nil: - return - node.mesh.updateOnGpu() - for child in node.nodes: - child.updateOnGpu() - -proc clearTreeFromGpu*(node: Node) = - ## Clear the vertex data from the GPU for the whole tree. - node.clearFromGpu() - proc trs*(node: Node): Mat4 = ## Get the transformation matrix of a node. translate(node.pos) * @@ -629,182 +230,6 @@ proc skinMatrices*(root, node: Node): seq[Mat4] = mat4() result[i] = inverseNode * joint.mat * inverseBind -proc toMat4(m: DMat4): Mat4 = - ## Converts a double-precision matrix to float32. - gmat4( - m[0, 0].float32, m[0, 1].float32, m[0, 2].float32, m[0, 3].float32, - m[1, 0].float32, m[1, 1].float32, m[1, 2].float32, m[1, 3].float32, - m[2, 0].float32, m[2, 1].float32, m[2, 2].float32, m[2, 3].float32, - m[3, 0].float32, m[3, 1].float32, m[3, 2].float32, m[3, 3].float32 - ) - -proc drawPrimitive( - primitive: Primitive, - shader: GLuint, - tint: Color, - skipBlend = false -) = - if primitive == nil or not primitive.hasGeometry(): - return - - if skipBlend and - primitive.material != nil and - primitive.material.alphaMode == BlendAlphaMode: - return - - if not primitive.uploaded: - primitive.uploadToGpu() - - glBindVertexArray(primitive.vertexArrayId) - - if primitive.material != nil and - not (skipBlend and primitive.material.alphaMode == BlendAlphaMode): - glActiveTexture(GL_TEXTURE0) - glBindTexture(GL_TEXTURE_2D, primitive.material.baseColorId) - - let sampleTexUniform = glGetUniformLocation(shader, "sampleTex") - if sampleTexUniform >= 0: - glUniform1i(sampleTexUniform, 0) - - let baseColorFactorUniform = glGetUniformLocation(shader, "baseColorFactor") - if baseColorFactorUniform >= 0: - glUniform4f( - baseColorFactorUniform, - primitive.material.baseColorFactor.r, - primitive.material.baseColorFactor.g, - primitive.material.baseColorFactor.b, - primitive.material.baseColorFactor.a - ) - - let alphaCutoffUniform = glGetUniformLocation(shader, "alphaCutoff") - if alphaCutoffUniform >= 0: - var cutoff = -1.0 - if primitive.material.alphaMode == MaskAlphaMode: - cutoff = primitive.material.alphaCutoff - glUniform1f(alphaCutoffUniform, cutoff) - else: - glBindTexture(GL_TEXTURE_2D, 0) - let sampleTexUniform = glGetUniformLocation(shader, "sampleTex") - if sampleTexUniform >= 0: - glUniform1i(sampleTexUniform, 0) - - if primitive.material != nil and primitive.material.doubleSided: - glDisable(GL_CULL_FACE) - else: - glEnable(GL_CULL_FACE) - - let colorTintUniform = glGetUniformLocation(shader, "tint") - glUniform4f(colorTintUniform, tint.r, tint.g, tint.b, tint.a) - - if primitive.mode == GL_POINTS: - glPointSize(1.0) - - if primitive.indices32.len > 0: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glDrawElements( - primitive.mode, - primitive.indices32.len.GLint, - GL_UNSIGNED_INT, - nil - ) - elif primitive.indices16.len > 0: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - glDrawElements( - primitive.mode, - primitive.indices16.len.GLint, - GL_UNSIGNED_SHORT, - nil - ) - else: - glDrawArrays(primitive.mode, 0, primitive.points.len.cint) - -proc draw*( - node: Node, - shader: GLuint, - transform, view, proj: Mat4, - tint: Color, - useTrs = true, - skipBlend = false, - root: Node = nil -) = - ## Draws the node with a float32 transform. - if not node.visible: - return - - let rootNode = - if root == nil: - node - else: - root - if root == nil: - rootNode.updateTransforms(transform, useTrs) - - node.mat = - if useTrs: - transform * node.trs - else: - transform - let - modelUniform = glGetUniformLocation(shader, "model") - viewUniform = glGetUniformLocation(shader, "view") - projUniform = glGetUniformLocation(shader, "proj") - - var - modelArray = node.mat - viewArray = view - projArray = proj - glUniformMatrix4fv( - modelUniform, - 1, - GL_FALSE, - cast[ptr float32](modelArray.addr) - ) - glUniformMatrix4fv( - viewUniform, - 1, - GL_FALSE, - cast[ptr float32](viewArray.addr) - ) - glUniformMatrix4fv( - projUniform, - 1, - GL_FALSE, - cast[ptr float32](projArray.addr) - ) - - let jointMatrices = rootNode.skinMatrices(node) - let useSkinning = - jointMatrices.len > 0 and - node.mesh != nil - let useSkinningUniform = glGetUniformLocation(shader, "useSkinning") - if useSkinningUniform >= 0: - glUniform1i(useSkinningUniform, useSkinning.ord.GLint) - if useSkinning: - let jointMatricesUniform = glGetUniformLocation(shader, "jointMatrices") - if jointMatricesUniform >= 0: - glUniformMatrix4fv( - jointMatricesUniform, - jointMatrices.len.GLsizei, - GL_FALSE, - cast[ptr float32](jointMatrices[0].addr) - ) - - if node.mesh != nil: - for primitive in node.mesh.primitives: - primitive.drawPrimitive(shader, tint, skipBlend) - - for n in node.nodes: - n.draw( - shader, - node.mat, - view, - proj, - tint, - useTrs=true, - skipBlend=skipBlend, - root=rootNode - ) - proc dumpTree*(node: Node, indent: string = "") = ## Prints the node tree for debugging. echo &"{indent}{node.name}" @@ -997,20 +422,3 @@ proc `[]`*(node: Node, name: string): Node = for n in node.nodes: if n.name == name: return n - -proc draw*( - node: Node, - shader: GLuint, - transform, view, proj: DMat4, - tint: Color, - useTrs = true -) = - ## Draw the node using a double precision transformation matrix. - node.draw( - shader, - transform.toMat4(), - view.toMat4(), - proj.toMat4(), - tint, - useTrs - ) diff --git a/src/gltf/pbr.nim b/src/gltf/pbr.nim index b8daeca..4e80ae7 100644 --- a/src/gltf/pbr.nim +++ b/src/gltf/pbr.nim @@ -1,939 +1,15 @@ -## This file handles rendering for PBR. - -import - std/[strutils, algorithm], - opengl, windy, pixie, vmath, - common, models, shaders - -const - envMapSize* = 512 # Size of the environment map. - StudioEnvSize = 8 - - PbrVertSrc = staticRead("../../data/shaders/pbr.vert") - PbrFragSrc = staticRead("../../data/shaders/pbr.frag") - SkyboxVertSrc = staticRead("../../data/shaders/skybox.vert") - SkyboxFragSrc = staticRead("../../data/shaders/skybox.frag") - ShadowDepthVertSrc = staticRead("../../experiments/shaders/shadow_depth.vert") - ShadowDepthFragSrc = staticRead("../../experiments/shaders/shadow_depth.frag") - -var - envMapFBO*: GLuint # Framebuffer object for environment map. - environmentMapId*: GLuint # Texture ID for environment map. - rboDepth*: GLuint # Renderbuffer object for depth buffer. - - pbrShader*: GLuint # Shader program for PBR rendering. - skyboxShader*: GLuint # Shader program for skybox rendering. - skyboxVao*: GLuint # VAO for skybox rendering. - skyboxVbo*: GLuint # VBO for skybox rendering. - - shadowMapFbo*: GLuint - shadowMapTex*: GLuint - shadowDepthShader*: GLuint - -const - ShadowMapSize = 2048 - -proc setupPbr*() = - ## Sets up the PBR rendering system. - ## * Create Environment Map and Framebuffer. - - # Reduce visible seams when sampling blurred cubemap mip levels. - glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS) - - glGenTextures(1, addr environmentMapId) - glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) - - for i in 0 ..< 6: - glTexImage2D( - (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, - 0, - GL_RGB.GLint, - envMapSize, - envMapSize, - 0, - GL_RGB, - GL_UNSIGNED_BYTE, - nil - ) - - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) - - # Set up the offscreen framebuffer. - glGenFramebuffers(1, addr envMapFBO) - glBindFramebuffer(GL_FRAMEBUFFER, envMapFBO) - - # Create a depth renderbuffer. - glGenRenderbuffers(1, addr rboDepth) - glBindRenderbuffer(GL_RENDERBUFFER, rboDepth) - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, envMapSize, envMapSize) - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth) - - if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE: - echo "Framebuffer not complete!" - - glBindFramebuffer(GL_FRAMEBUFFER, 0) - - pbrShader = compileShaderFiles( - PbrVertSrc, - PbrFragSrc - ) - - skyboxShader = compileShaderFiles( - SkyboxVertSrc, - SkyboxFragSrc - ) - - shadowDepthShader = compileShaderFiles( - ShadowDepthVertSrc, - ShadowDepthFragSrc - ) - - # Shadow map resources. - glGenFramebuffers(1, addr shadowMapFbo) - glGenTextures(1, addr shadowMapTex) - glBindTexture(GL_TEXTURE_2D, shadowMapTex) - glTexImage2D( - GL_TEXTURE_2D, - 0, - GL_DEPTH_COMPONENT.GLint, - ShadowMapSize, - ShadowMapSize, - 0, - GL_DEPTH_COMPONENT, - cGL_FLOAT, - nil - ) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.cint) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.cint) - var borderColor = [1.0.GLfloat, 1.0, 1.0, 1.0] - glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor[0].addr) - - glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFbo) - glFramebufferTexture2D( - GL_FRAMEBUFFER, - GL_DEPTH_ATTACHMENT, - GL_TEXTURE_2D, - shadowMapTex, - 0 - ) - glDrawBuffer(GL_NONE) - glReadBuffer(GL_NONE) - glBindFramebuffer(GL_FRAMEBUFFER, 0) - - # Bind shadow map to texture unit 6 so the sampler2DShadow uniform always - # points to a valid depth texture, even when shadows are disabled. - glUseProgram(pbrShader) - glUniform1i(glGetUniformLocation(pbrShader, "shadowMap"), 6) - glActiveTexture(GL_TEXTURE6) - glBindTexture(GL_TEXTURE_2D, shadowMapTex) - glUseProgram(0) - - # Full-screen triangle data. - var skyboxVertices = [ - -1.0f32, -1.0f32, - 3.0f32, -1.0f32, - -1.0f32, 3.0f32 - ] - - glGenVertexArrays(1, addr skyboxVao) - glGenBuffers(1, addr skyboxVbo) - - glBindVertexArray(skyboxVao) - glBindBuffer(GL_ARRAY_BUFFER, skyboxVbo) - glBufferData(GL_ARRAY_BUFFER, skyboxVertices.sizeof, addr skyboxVertices, GL_STATIC_DRAW) - - glEnableVertexAttribArray(0) - glVertexAttribPointer(0, 2, cGL_FLOAT, GL_FALSE, 0, nil) - - glBindVertexArray(0) - -proc loadCubeTexture(path: string): GLuint = - ## Creates a cube texture and returns its OpenGL ID. - - var textureId: GLuint - glGenTextures(1, addr(textureId)) - glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) - - let directions = [ - "px", "nx", "py", "ny", "pz", "nz" - ] - for i, direction in directions: - let image = readImage(path.replace("*", direction)) - glTexImage2D( - (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, - 0, - GL_RGBA.GLint, - image.width.GLsizei, - image.height.GLsizei, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - image.data[0].addr - ) - - # Set texture parameters for the cube map. - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) - - # Generate mipmaps for the cube map. - glGenerateMipmap(GL_TEXTURE_CUBE_MAP) - - return textureId - -proc createSolidCubeTexture(color: ColorRGBX): GLuint = - ## Creates a simple solid-color cube texture. - var - textureId: GLuint - pixel = [color.r, color.g, color.b, color.a] - - glGenTextures(1, textureId.addr) - glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) - - for i in 0 ..< 6: - glTexImage2D( - (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + i).GLenum, - 0, - GL_RGBA.GLint, - 1, - 1, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - pixel[0].addr - ) - - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) - textureId - -proc studioFaceDirection(face, x, y, size: int): Vec3 = - ## Returns a normalized direction for a cubemap texel. - let - u = ((x.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f - v = ((y.float32 + 0.5'f) / size.float32) * 2.0'f - 1.0'f - case face - of 0: - normalize(vec3(1.0'f, -v, -u)) - of 1: - normalize(vec3(-1.0'f, -v, u)) - of 2: - normalize(vec3(u, 1.0'f, v)) - of 3: - normalize(vec3(u, -1.0'f, -v)) - of 4: - normalize(vec3(u, -v, 1.0'f)) - of 5: - normalize(vec3(-u, -v, -1.0'f)) - else: - vec3(0, 1, 0) - -proc studioColor(dir: Vec3): ColorRGBX = - ## Returns a tiny neutral studio-light sample color. - let - hemi = clamp(dir.y * 0.5'f + 0.5'f, 0.0'f, 1.0'f) - keyDir = normalize(vec3(0.35'f, 0.85'f, 0.25'f)) - fillDir = normalize(vec3(-0.45'f, 0.65'f, -0.35'f)) - key = pow(max(dot(dir, keyDir), 0.0'f), 24.0'f) - fill = pow(max(dot(dir, fillDir), 0.0'f), 8.0'f) - cool = vec3(0.18'f, 0.19'f, 0.21'f) - neutral = vec3(0.58'f, 0.6'f, 0.63'f) - sky = vec3(0.92'f, 0.94'f, 0.97'f) - var color = mix(cool, neutral, hemi) - color = mix(color, sky, hemi * hemi) - color += vec3(0.28'f, 0.27'f, 0.25'f) * key - color += vec3(0.10'f, 0.11'f, 0.12'f) * fill - rgbx( - clamp((color.x * 255.0'f).int, 0, 255).uint8, - clamp((color.y * 255.0'f).int, 0, 255).uint8, - clamp((color.z * 255.0'f).int, 0, 255).uint8, - 255 - ) - -proc createStudioCubeTexture(): GLuint = - ## Creates a tiny procedural cubemap for neutral studio lighting. - var textureId: GLuint - glGenTextures(1, textureId.addr) - glBindTexture(GL_TEXTURE_CUBE_MAP, textureId) - - for face in 0 ..< 6: - let image = newImage(StudioEnvSize, StudioEnvSize) - for y in 0 ..< StudioEnvSize: - for x in 0 ..< StudioEnvSize: - image[x, y] = studioColor( - studioFaceDirection(face, x, y, StudioEnvSize) - ) - glTexImage2D( - (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + face).GLenum, - 0, - GL_RGBA.GLint, - image.width.GLsizei, - image.height.GLsizei, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - image.data[0].addr - ) - - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri( - GL_TEXTURE_CUBE_MAP, - GL_TEXTURE_MIN_FILTER, - GL_LINEAR_MIPMAP_LINEAR - ) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) - glGenerateMipmap(GL_TEXTURE_CUBE_MAP) - textureId - -proc loadEnvironmentMap*(cubeMapPath: string) = - ## Loads an environment map from a cube texture path. - if environmentMapId != 0: - glDeleteTextures(1, environmentMapId.addr) - environmentMapId = loadCubeTexture(cubeMapPath) - -proc loadSolidEnvironmentMap*(color = rgbx(180, 190, 220, 255)) = - ## Loads a solid-color cubemap. - if environmentMapId != 0: - glDeleteTextures(1, environmentMapId.addr) - environmentMapId = createSolidCubeTexture(color) - -proc loadDefaultEnvironmentMap*() = - ## Loads a small procedural studio cubemap. - if environmentMapId != 0: - glDeleteTextures(1, environmentMapId.addr) - environmentMapId = createStudioCubeTexture() - -proc drawSkybox*(view, proj: Mat4, lod: float32 = 0.0) = - ## Draws the skybox using a full-screen quad. - glUseProgram(skyboxShader) - - var - invProj = proj.inverse - invView = view.inverse - - glUniformMatrix4fv( - glGetUniformLocation(skyboxShader, "invProj"), - 1, - GL_FALSE, - cast[ptr float32](invProj.addr) - ) - glUniformMatrix4fv( - glGetUniformLocation(skyboxShader, "invView"), - 1, - GL_FALSE, - cast[ptr float32](invView.addr) - ) - - glActiveTexture(GL_TEXTURE0) - glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) - glUniform1i(glGetUniformLocation(skyboxShader, "environmentMap"), 0) - glUniform1f(glGetUniformLocation(skyboxShader, "lod"), lod) - - # Draw a single triangle that covers the whole screen. - glDisable(GL_CULL_FACE) - glDisable(GL_DEPTH_TEST) - glDepthMask(GL_FALSE) - - glBindVertexArray(skyboxVao) - glDrawArrays(GL_TRIANGLES, 0, 3) - glBindVertexArray(0) - - glDepthMask(GL_TRUE) - glEnable(GL_DEPTH_TEST) - -type - DebugView* = enum - dvLit, - dvUnlit, - dvNormals, - dvAoBake, - dvMetallic, - dvSpecular - - BlendEntry = object - node: Node - primitive: Primitive - transform: Mat4 - -proc setTextureTransformUniform(shader: GLuint, prefix: string, transform: TextureTransform) = - ## Sets UV transform uniforms for a texture input. - let - texCoordName = (prefix & "TexCoord").cstring - offsetName = (prefix & "UvOffset").cstring - scaleName = (prefix & "UvScale").cstring - rotationName = (prefix & "UvRotation").cstring - glUniform1i( - glGetUniformLocation(shader, texCoordName), - transform.texCoord.GLint - ) - glUniform2f( - glGetUniformLocation(shader, offsetName), - transform.offset.x, - transform.offset.y - ) - glUniform2f( - glGetUniformLocation(shader, scaleName), - transform.scale.x, - transform.scale.y - ) - glUniform1f( - glGetUniformLocation(shader, rotationName), - transform.rotation - ) - -proc renderPbrPrimitive( - primitive: Primitive, - transform, view, proj: Mat4, - tint: Color, - ambientLightColor: Color, - sunLightDirection: Vec3, - sunLightColor: Color, - rimLightDirection: Vec3, - rimLightColor: Color, - debugView: DebugView, - cameraPosition: Vec3, - useShadow: bool, - lightSpace: Mat4, - shadowTex: GLuint, - deferBlend: bool, - blended: var seq[BlendEntry], - owner: Node, - root: Node -) = - if primitive == nil: - return - - let isBlend = - primitive.material != nil and - primitive.material.alphaMode == BlendAlphaMode - if deferBlend and isBlend: - blended.add(BlendEntry(node: owner, primitive: primitive, transform: transform)) - return - - glUseProgram(pbrShader) - - let - modelUniform = glGetUniformLocation(pbrShader, "model") - viewUniform = glGetUniformLocation(pbrShader, "view") - projUniform = glGetUniformLocation(pbrShader, "proj") - lightSpaceUniform = glGetUniformLocation(pbrShader, "lightSpace") - - var - modelArray = transform - viewArray = view - projArray = proj - lightSpaceArray = lightSpace - glUniformMatrix4fv(modelUniform, 1, GL_FALSE, cast[ptr float32](modelArray.addr)) - glUniformMatrix4fv(viewUniform, 1, GL_FALSE, cast[ptr float32](viewArray.addr)) - glUniformMatrix4fv(projUniform, 1, GL_FALSE, cast[ptr float32](projArray.addr)) - glUniformMatrix4fv(lightSpaceUniform, 1, GL_FALSE, cast[ptr float32](lightSpaceArray.addr)) - - let jointMatrices = root.skinMatrices(owner) - let useSkinning = jointMatrices.len > 0 - glUniform1i( - glGetUniformLocation(pbrShader, "useSkinning"), - useSkinning.ord.GLint - ) - if useSkinning: - glUniformMatrix4fv( - glGetUniformLocation(pbrShader, "jointMatrices"), - jointMatrices.len.GLsizei, - GL_FALSE, - cast[ptr float32](jointMatrices[0].addr) - ) - - if not primitive.uploaded: - primitive.uploadToGpu() - glBindVertexArray(primitive.vertexArrayId) - - if primitive.material != nil: - let useNormalTexture = - primitive.material.hasNormalTexture and - primitive.normals.len > 0 and - primitive.tangents.len > 0 - - glActiveTexture(GL_TEXTURE0) - glUniform1i(glGetUniformLocation(pbrShader, "baseColorTexture"), 0) - glBindTexture(GL_TEXTURE_2D, primitive.material.baseColorId) - - glUniform4f( - glGetUniformLocation(pbrShader, "baseColorFactor"), - primitive.material.baseColorFactor.r, - primitive.material.baseColorFactor.g, - primitive.material.baseColorFactor.b, - primitive.material.baseColorFactor.a - ) - setTextureTransformUniform(pbrShader, "baseColor", primitive.material.baseColorTransform) - - glActiveTexture(GL_TEXTURE1) - glUniform1i(glGetUniformLocation(pbrShader, "metallicRoughnessTexture"), 1) - glBindTexture(GL_TEXTURE_2D, primitive.material.metallicRoughnessId) - glUniform1f(glGetUniformLocation(pbrShader, "metallicFactor"), primitive.material.metallicFactor) - glUniform1f(glGetUniformLocation(pbrShader, "roughnessFactor"), primitive.material.roughnessFactor) - glUniform1f(glGetUniformLocation(pbrShader, "transmissionFactor"), primitive.material.transmissionFactor) - setTextureTransformUniform( - pbrShader, - "metallicRoughness", - primitive.material.metallicRoughnessTransform - ) - - glActiveTexture(GL_TEXTURE2) - glUniform1i(glGetUniformLocation(pbrShader, "normalTexture"), 2) - glBindTexture(GL_TEXTURE_2D, primitive.material.normalId) - glUniform1f(glGetUniformLocation(pbrShader, "normalScale"), primitive.material.normalScale) - setTextureTransformUniform(pbrShader, "normal", primitive.material.normalTransform) - glUniform1i(glGetUniformLocation(pbrShader, "useNormalTexture"), useNormalTexture.ord.GLint) - - glActiveTexture(GL_TEXTURE3) - glUniform1i(glGetUniformLocation(pbrShader, "occlusionTexture"), 3) - glBindTexture(GL_TEXTURE_2D, primitive.material.occlusionId) - glUniform1f(glGetUniformLocation(pbrShader, "occlusionStrength"), primitive.material.occlusionStrength) - setTextureTransformUniform(pbrShader, "occlusion", primitive.material.occlusionTransform) - - glActiveTexture(GL_TEXTURE4) - glUniform1i(glGetUniformLocation(pbrShader, "emissiveTexture"), 4) - glBindTexture(GL_TEXTURE_2D, primitive.material.emissiveId) - glUniform3f( - glGetUniformLocation(pbrShader, "emissiveFactor"), - primitive.material.emissiveFactor.r, - primitive.material.emissiveFactor.g, - primitive.material.emissiveFactor.b - ) - setTextureTransformUniform(pbrShader, "emissive", primitive.material.emissiveTransform) - - glActiveTexture(GL_TEXTURE5) - glUniform1i(glGetUniformLocation(pbrShader, "environmentMap"), 5) - glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMapId) - - if useShadow: - glActiveTexture(GL_TEXTURE6) - glUniform1i(glGetUniformLocation(pbrShader, "shadowMap"), 6) - glBindTexture(GL_TEXTURE_2D, shadowTex) - - var cutoff = primitive.material.alphaCutoff - case primitive.material.alphaMode - of MaskAlphaMode: - glDisable(GL_BLEND) - glDepthMask(GL_TRUE) - cutoff = primitive.material.alphaCutoff - of BlendAlphaMode: - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glDepthMask(GL_FALSE) - cutoff = -1.0 - else: - glDisable(GL_BLEND) - glDepthMask(GL_TRUE) - cutoff = -1.0 - glUniform1f(glGetUniformLocation(pbrShader, "alphaCutoff"), cutoff) - - if primitive.material.doubleSided: - glDisable(GL_CULL_FACE) - else: - glEnable(GL_CULL_FACE) - else: - glBindTexture(GL_TEXTURE_2D, 0) - - glUniform4f( - glGetUniformLocation(pbrShader, "ambientLightColor"), - ambientLightColor.r, - ambientLightColor.g, - ambientLightColor.b, - ambientLightColor.a - ) - glUniform3f( - glGetUniformLocation(pbrShader, "sunLightDirection"), - sunLightDirection.x, - sunLightDirection.y, - sunLightDirection.z - ) - glUniform4f( - glGetUniformLocation(pbrShader, "sunLightColor"), - sunLightColor.r, - sunLightColor.g, - sunLightColor.b, - sunLightColor.a - ) - glUniform3f( - glGetUniformLocation(pbrShader, "rimLightDirection"), - rimLightDirection.x, - rimLightDirection.y, - rimLightDirection.z - ) - glUniform4f( - glGetUniformLocation(pbrShader, "rimLightColor"), - rimLightColor.r, - rimLightColor.g, - rimLightColor.b, - rimLightColor.a - ) - glUniform1i(glGetUniformLocation(pbrShader, "debugViewMode"), debugView.int.GLint) - glUniform3f( - glGetUniformLocation(pbrShader, "cameraPosition"), - cameraPosition.x, - cameraPosition.y, - cameraPosition.z - ) - glUniform4f(glGetUniformLocation(pbrShader, "tint"), tint.r, tint.g, tint.b, tint.a) - glUniform1i(glGetUniformLocation(pbrShader, "useShadow"), useShadow.Glint) - - if primitive.mode == GL_POINTS: - glPointSize(1.0) - - if primitive.indices16.len == 0 and primitive.indices32.len == 0: - glDrawArrays(primitive.mode, 0, primitive.points.len.cint) - else: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive.indicesId) - if primitive.indices16.len > 0: - glDrawElements( - primitive.mode, - primitive.indices16.len.GLint, - GL_UNSIGNED_SHORT, - nil - ) - elif primitive.indices32.len > 0: - glDrawElements( - primitive.mode, - primitive.indices32.len.GLint, - GL_UNSIGNED_INT, - nil - ) - else: - raise newException(GltfError, "Invalid indices") - - glDisable(GL_BLEND) - glDepthMask(GL_TRUE) - glEnable(GL_CULL_FACE) - -proc renderPbrNode( - node: Node, - transform, view, proj: Mat4, - tint: Color, - ambientLightColor: Color, - sunLightDirection: Vec3, - sunLightColor: Color, - rimLightDirection: Vec3, - rimLightColor: Color, - debugView: DebugView, - cameraPosition: Vec3, - useShadow: bool, - lightSpace: Mat4, - shadowTex: GLuint, - deferBlend: bool, - blended: var seq[BlendEntry], - drawChildren = true, - applyTrs = true, - root: Node = nil -) = - ## Renders a node with the PBR shader. - if not node.visible: - return - - let rootNode = - if root == nil: - node - else: - root - - node.mat = - if applyTrs: - transform * node.trs - else: - transform - - if node.mesh != nil: - for primitive in node.mesh.primitives: - renderPbrPrimitive( - primitive, - node.mat, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow, - lightSpace, - shadowTex, - deferBlend, - blended, - node, - rootNode - ) - - if drawChildren: - for n in node.nodes: - n.renderPbrNode( - node.mat, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow, - lightSpace, - shadowTex, - deferBlend, - blended, - drawChildren=true, - applyTrs=true, - root=rootNode - ) - -proc drawPbr*( - node: Node, - transform, view, proj: Mat4, - tint: Color, - useTrs = true, - ambientLightColor = color(0.1, 0.1, 0.1, 1), - sunLightDirection = vec3(1, 4, 2), - sunLightColor = color(1, 1, 1, 1), - rimLightDirection = vec3(-1, 1, -1), - rimLightColor = color(0, 0, 0, 0), - debugView = dvLit, - cameraPosition = vec3(0, 0, 10) -) = - ## Draws a node tree with PBR shading. - if not node.visible: - return - - node.updateTransforms(transform, useTrs) - - var blended: seq[BlendEntry] - - renderPbrNode( - node, - transform, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow=false, - lightSpace=mat4(), - shadowTex=0, - deferBlend=true, - blended=blended, - root=node - ) - - if blended.len > 0: - glDepthMask(GL_FALSE) - var sorted = blended - sorted.sort(proc(a, b: BlendEntry): int = - let - pa = (a.transform * vec4(0, 0, 0, 1)).xyz - pb = (b.transform * vec4(0, 0, 0, 1)).xyz - da = (cameraPosition - pa).lengthSq - db = (cameraPosition - pb).lengthSq - if da > db: -1 elif da < db: 1 else: 0 - ) - for entry in sorted: - var dummy: seq[BlendEntry] - renderPbrPrimitive( - entry.primitive, - entry.transform, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow=false, - lightSpace=mat4(), - shadowTex=0, - deferBlend=false, - blended=dummy, - owner=entry.node, - root=node - ) - glDepthMask(GL_TRUE) - -proc shadowLookAt(eye, center, up: Vec3): Mat4 = - ## Standard OpenGL lookAt (z-backward) for shadow mapping. - let - f = normalize(center - eye) - s = normalize(cross(f, up)) - u = cross(s, f) - result[0, 0] = s.x - result[1, 0] = s.y - result[2, 0] = s.z - result[3, 0] = -dot(s, eye) - result[0, 1] = u.x - result[1, 1] = u.y - result[2, 1] = u.z - result[3, 1] = -dot(u, eye) - result[0, 2] = -f.x - result[1, 2] = -f.y - result[2, 2] = -f.z - result[3, 2] = dot(f, eye) - result[3, 3] = 1.0'f32 - -proc getShadowMatrices(node: Node, transform: Mat4, lightDir: Vec3): (Mat4, Mat4, Mat4, Vec3) = - ## Compute light view/projection for the node tree. - let - bounds = getAABounds(node, transform) - center = bounds.center - radius = bounds.radius().float32 - dir = normalize(lightDir) - lightPos = center - dir * (radius * 2.0'f) - nearPlane = max(0.1'f, radius * 0.1'f) - farPlane = radius * 4.0'f - orthoSize = radius * 1.5'f - lightView = shadowLookAt(lightPos, center, vec3(0, 1, 0)) - lightProj = ortho( - -orthoSize, - orthoSize, - -orthoSize, - orthoSize, - nearPlane, - farPlane - ) - return (lightView, lightProj, lightProj * lightView, lightPos) - -proc drawPbrWithShadow*( - node: Node, - transform, view, proj: Mat4, - tint: Color, - sunLightDirection = vec3(1, 4, 2), - useTrs = true, - ambientLightColor = color(0.1, 0.1, 0.1, 1), - sunLightColor = color(1, 1, 1, 1), - rimLightDirection = vec3(-1, 1, -1), - rimLightColor = color(0, 0, 0, 0), - debugView = dvLit, - cameraPosition = vec3(0, 0, 10) -) = - ## Draws a node tree with PBR shading and shadows. - if not node.visible: - return - - node.updateTransforms(transform, useTrs) - - let (lightView, lightProj, lightSpace, _) = - getShadowMatrices(node, transform, sunLightDirection) - - # Save viewport and framebuffer. - var - oldViewport: array[4, GLint] - oldFramebuffer: GLint - glGetIntegerv(GL_VIEWPORT, oldViewport[0].addr) - glGetIntegerv(GL_FRAMEBUFFER_BINDING, oldFramebuffer.addr) - - # Depth pass. - glViewport(0, 0, ShadowMapSize, ShadowMapSize) - glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFbo) - glClear(GL_DEPTH_BUFFER_BIT) - glUseProgram(shadowDepthShader) - glEnable(GL_CULL_FACE) - glCullFace(GL_BACK) - glEnable(GL_DEPTH_TEST) - glDepthMask(GL_TRUE) - - node.draw( - shadowDepthShader, - transform, - lightView, - lightProj, - tint, - useTrs=true, - skipBlend=true, - root=node - ) - - glBindFramebuffer(GL_FRAMEBUFFER, oldFramebuffer.GLuint) - - # Restore viewport. - glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]) - - # Main pass with shadow sampling. - var blended: seq[BlendEntry] - - renderPbrNode( - node, - transform, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow=true, - lightSpace=lightSpace, - shadowTex=shadowMapTex, - deferBlend=true, - blended=blended, - root=node - ) - - if blended.len > 0: - glDepthMask(GL_FALSE) - var sorted = blended - sorted.sort(proc(a, b: BlendEntry): int = - let pa = (a.transform * vec4(0, 0, 0, 1)).xyz - let pb = (b.transform * vec4(0, 0, 0, 1)).xyz - let da = (cameraPosition - pa).lengthSq - let db = (cameraPosition - pb).lengthSq - if da > db: -1 elif da < db: 1 else: 0 - ) - for entry in sorted: - var dummy: seq[BlendEntry] - renderPbrPrimitive( - entry.primitive, - entry.transform, - view, - proj, - tint, - ambientLightColor, - sunLightDirection, - sunLightColor, - rimLightDirection, - rimLightColor, - debugView, - cameraPosition, - useShadow=false, - lightSpace=mat4(), - shadowTex=0, - deferBlend=false, - blended=dummy, - owner=entry.node, - root=node - ) - glDepthMask(GL_TRUE) +import common + +when defined(useDirectX): + import backends/directx/renderer as backendRenderer +elif defined(useVulkan): + import backends/vulkan/renderer as backendRenderer +elif defined(useMetal4): + import backends/metal/renderer as backendRenderer +else: + import backends/opengl/renderer as backendRenderer + +export + common.DebugView, + common.RenderParams, + backendRenderer diff --git a/src/gltf/reader.nim b/src/gltf/reader.nim index cab9674..f8acd9b 100644 --- a/src/gltf/reader.nim +++ b/src/gltf/reader.nim @@ -1,7 +1,7 @@ import std/[base64, json, os, strformat, strutils], - chroma, flatty/binny, opengl, pixie, vmath, webby, - common, internal, ktx2, models + chroma, flatty/binny, pixie, vmath, webby, + common, internal, models export common @@ -13,6 +13,83 @@ const SupportedExtensions = [ "KHR_texture_basisu" ] +const + GltfArrayBufferTarget = 34962 + GltfElementArrayBufferTarget = 34963 + +proc parseComponentType(value: int): ComponentType = + case value + of ByteComponent.int: + ByteComponent + of UnsignedByteComponent.int: + UnsignedByteComponent + of ShortComponent.int: + ShortComponent + of UnsignedShortComponent.int: + UnsignedShortComponent + of UnsignedIntComponent.int: + UnsignedIntComponent + of FloatComponent.int: + FloatComponent + else: + raise newException(GltfError, &"Invalid accessor componentType {value}") + +proc parseTextureMagFilter(value: int): TextureMagFilter = + case value + of NearestMagFilter.int: + NearestMagFilter + of LinearMagFilter.int: + LinearMagFilter + else: + raise newException(GltfError, &"Invalid texture magFilter {value}") + +proc parseTextureMinFilter(value: int): TextureMinFilter = + case value + of NearestMinFilter.int: + NearestMinFilter + of LinearMinFilter.int: + LinearMinFilter + of NearestMipmapNearestMinFilter.int: + NearestMipmapNearestMinFilter + of LinearMipmapNearestMinFilter.int: + LinearMipmapNearestMinFilter + of NearestMipmapLinearMinFilter.int: + NearestMipmapLinearMinFilter + of LinearMipmapLinearMinFilter.int: + LinearMipmapLinearMinFilter + else: + raise newException(GltfError, &"Invalid texture minFilter {value}") + +proc parseTextureWrap(value: int): TextureWrap = + case value + of ClampToEdgeWrap.int: + ClampToEdgeWrap + of MirroredRepeatWrap.int: + MirroredRepeatWrap + of RepeatWrap.int: + RepeatWrap + else: + raise newException(GltfError, &"Invalid texture wrap mode {value}") + +proc parsePrimitiveMode(value: int): PrimitiveMode = + case value + of PointsMode.int: + PointsMode + of LinesMode.int: + LinesMode + of LineLoopMode.int: + LineLoopMode + of LineStripMode.int: + LineStripMode + of TrianglesMode.int: + TrianglesMode + of TriangleStripMode.int: + TriangleStripMode + of TriangleFanMode.int: + TriangleFanMode + else: + raise newException(GltfError, &"Invalid primitive mode {value}") + proc unsupportedUsedExtensions(jsonRoot: JsonNode): seq[string] = ## Returns used extensions we do not currently support. if "extensionsUsed" notin jsonRoot: @@ -40,13 +117,13 @@ proc readSparseIndices( start = view.byteOffset + accessor.sparse.indices.byteOffset result.setLen(accessor.sparse.count) case accessor.sparse.indices.componentType - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: for i in 0 ..< result.len: result[i] = buffer.readUint8(start + i).int - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: for i in 0 ..< result.len: result[i] = buffer.readUint16(start + i * 2).int - of GL_UNSIGNED_INT: + of UnsignedIntComponent: for i in 0 ..< result.len: result[i] = buffer.readUint32(start + i * 4).int else: @@ -69,13 +146,13 @@ proc readAccessorFloats( start = view.byteOffset + accessor.byteOffset elemSize = case accessor.componentType - of cGL_FLOAT: + of FloatComponent: 4 - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: 1 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: 2 - of GL_UNSIGNED_INT: + of UnsignedIntComponent: 4 else: 0 @@ -88,13 +165,13 @@ proc readAccessorFloats( for i in 0 ..< accessor.count: let off = start + i * stride case accessor.componentType - of cGL_FLOAT: + of FloatComponent: result[i] = readFloat32(buffer, off) - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: result[i] = buffer.readUint8(off).float32 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: result[i] = buffer.readUint16(off).float32 - of GL_UNSIGNED_INT: + of UnsignedIntComponent: result[i] = buffer.readUint32(off).float32 else: discard @@ -108,13 +185,13 @@ proc readAccessorFloats( for i, dstIndex in indices: let off = sparseStart + i * sparseStride case accessor.componentType - of cGL_FLOAT: + of FloatComponent: result[dstIndex] = readFloat32(sparseBuffer, off) - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: result[dstIndex] = sparseBuffer.readUint8(off).float32 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: result[dstIndex] = sparseBuffer.readUint16(off).float32 - of GL_UNSIGNED_INT: + of UnsignedIntComponent: result[dstIndex] = sparseBuffer.readUint32(off).float32 else: discard @@ -236,7 +313,7 @@ proc readAccessorVec2( start = view.byteOffset + accessor.byteOffset stride = if view.byteStride > 0: view.byteStride else: 8 assertRaise accessor.kind == atVEC2, "Unsupported vec2 accessor kind" - assertRaise accessor.componentType == cGL_FLOAT, + assertRaise accessor.componentType == FloatComponent, "Unsupported vec2 component type" result.setLen(accessor.count) for i in 0 ..< accessor.count: @@ -272,7 +349,7 @@ proc readAccessorVec4( start = view.byteOffset + accessor.byteOffset stride = if view.byteStride > 0: view.byteStride else: 16 assertRaise accessor.kind == atVEC4, "Unsupported vec4 accessor kind" - assertRaise accessor.componentType == cGL_FLOAT, + assertRaise accessor.componentType == FloatComponent, "Unsupported vec4 component type" result.setLen(accessor.count) for i in 0 ..< accessor.count: @@ -312,7 +389,7 @@ proc readAccessorMat4( start = view.byteOffset + accessor.byteOffset stride = if view.byteStride > 0: view.byteStride else: 64 assertRaise accessor.kind == atMAT4, "Unsupported mat4 accessor kind" - assertRaise accessor.componentType == cGL_FLOAT, + assertRaise accessor.componentType == FloatComponent, "Unsupported mat4 component type" result.setLen(accessor.count) for i in 0 ..< accessor.count: @@ -350,9 +427,9 @@ proc readAccessorJointIds( start = view.byteOffset + accessor.byteOffset elemSize = case accessor.componentType - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: 4 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: 8 else: 0 @@ -363,14 +440,14 @@ proc readAccessorJointIds( for i in 0 ..< accessor.count: let off = start + i * stride case accessor.componentType - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: result[i] = [ buffer.readUint8(off + 0).uint16, buffer.readUint8(off + 1).uint16, buffer.readUint8(off + 2).uint16, buffer.readUint8(off + 3).uint16 ] - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: result[i] = [ buffer.readUint16(off + 0), buffer.readUint16(off + 2), @@ -388,11 +465,11 @@ proc normalizedValue( if not accessor.normalized: return value.float32 case accessor.componentType - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: value.float32 / 255.0'f32 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: value.float32 / 65535.0'f32 - of GL_UNSIGNED_INT: + of UnsignedIntComponent: value.float32 / 4294967295.0'f32 else: value.float32 @@ -411,11 +488,11 @@ proc readAccessorWeights( start = view.byteOffset + accessor.byteOffset elemSize = case accessor.componentType - of cGL_FLOAT: + of FloatComponent: 16 - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: 4 - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: 8 else: 0 @@ -426,21 +503,21 @@ proc readAccessorWeights( for i in 0 ..< accessor.count: let off = start + i * stride case accessor.componentType - of cGL_FLOAT: + of FloatComponent: result[i] = vec4( readFloat32(buffer, off + 0), readFloat32(buffer, off + 4), readFloat32(buffer, off + 8), readFloat32(buffer, off + 12) ) - of GL_UNSIGNED_BYTE: + of UnsignedByteComponent: result[i] = vec4( normalizedValue(accessor, buffer.readUint8(off + 0)), normalizedValue(accessor, buffer.readUint8(off + 1)), normalizedValue(accessor, buffer.readUint8(off + 2)), normalizedValue(accessor, buffer.readUint8(off + 3)) ) - of cGL_UNSIGNED_SHORT: + of UnsignedShortComponent: result[i] = vec4( normalizedValue(accessor, buffer.readUint16(off + 0)), normalizedValue(accessor, buffer.readUint16(off + 2)), @@ -601,11 +678,11 @@ proc loadPrimitive( bufferViews: seq[BufferView], buffers: seq[string], images: seq[Image], + imageKtx2Data: seq[string], imageNames: seq[string], textures: seq[Texture], samplers: seq[Sampler], - materials: seq[MaterialInfo], - imageTextureIds: seq[GLuint] + materials: seq[MaterialInfo] ): Primitive = ## Loads one glTF primitive into a runtime primitive. proc getTextureSampler(textureIndex: int): TextureSampler = @@ -631,7 +708,7 @@ proc loadPrimitive( if pbr.baseColorTexture.index >= 0: let imageIndex = textures[pbr.baseColorTexture.index].source result.material.baseColor = images[imageIndex] - result.material.baseColorId = imageTextureIds[imageIndex] + result.material.baseColorKtx2 = imageKtx2Data[imageIndex] result.material.baseColorName = imageNames[imageIndex] result.material.baseColorSampler = getTextureSampler(pbr.baseColorTexture.index) @@ -649,7 +726,7 @@ proc loadPrimitive( if pbr.metallicRoughnessTexture.index >= 0: let imageIndex = textures[pbr.metallicRoughnessTexture.index].source result.material.metallicRoughness = images[imageIndex] - result.material.metallicRoughnessId = imageTextureIds[imageIndex] + result.material.metallicRoughnessKtx2 = imageKtx2Data[imageIndex] result.material.metallicRoughnessName = imageNames[imageIndex] result.material.metallicRoughnessSampler = getTextureSampler(pbr.metallicRoughnessTexture.index) @@ -668,7 +745,7 @@ proc loadPrimitive( if material.normalTexture.index >= 0: let imageIndex = textures[material.normalTexture.index].source result.material.normal = images[imageIndex] - result.material.normalId = imageTextureIds[imageIndex] + result.material.normalKtx2 = imageKtx2Data[imageIndex] result.material.normalName = imageNames[imageIndex] result.material.normalSampler = getTextureSampler(material.normalTexture.index) @@ -689,7 +766,7 @@ proc loadPrimitive( if material.occlusionTexture.index >= 0: let imageIndex = textures[material.occlusionTexture.index].source result.material.occlusion = images[imageIndex] - result.material.occlusionId = imageTextureIds[imageIndex] + result.material.occlusionKtx2 = imageKtx2Data[imageIndex] result.material.occlusionName = imageNames[imageIndex] result.material.occlusionSampler = getTextureSampler(material.occlusionTexture.index) @@ -707,7 +784,7 @@ proc loadPrimitive( if material.emissiveTexture.index >= 0: let imageIndex = textures[material.emissiveTexture.index].source result.material.emissive = images[imageIndex] - result.material.emissiveId = imageTextureIds[imageIndex] + result.material.emissiveKtx2 = imageKtx2Data[imageIndex] result.material.emissiveName = imageNames[imageIndex] result.material.emissiveSampler = getTextureSampler(material.emissiveTexture.index) @@ -747,18 +824,18 @@ proc loadPrimitive( bufferView = bufferViews[accessor.bufferView] buffer = buffers[bufferView.buffer] start = bufferView.byteOffset + accessor.byteOffset - if accessor.componentType == GL_UNSIGNED_BYTE: + if accessor.componentType == UnsignedByteComponent: assertRaise accessor.kind == atSCALAR, "Unsupported index kind" assertRaise bufferView.byteStride == 0, "Unsupported index byteStride" result.indices16.setLen(accessor.count) for i in 0 ..< accessor.count: result.indices16[i] = buffer[start + i].uint8 - elif accessor.componentType == cGL_UNSIGNED_SHORT: + elif accessor.componentType == UnsignedShortComponent: assertRaise accessor.kind == atSCALAR, "Unsupported index kind" assertRaise bufferView.byteStride == 0, "Unsupported index byteStride" result.indices16.setLen(accessor.count) copyMem(result.indices16[0].addr, buffer[start].addr, accessor.count * 2) - elif accessor.componentType == GL_UNSIGNED_INT: + elif accessor.componentType == UnsignedIntComponent: assertRaise accessor.kind == atSCALAR, "Unsupported index kind" assertRaise bufferView.byteStride == 0, "Unsupported index byteStride" result.indices32.setLen(accessor.count) @@ -771,14 +848,14 @@ proc loadPrimitive( if primInfo.attributes.position >= 0: let accessor = accessors[primInfo.attributes.position] - if accessor.componentType == cGL_FLOAT: + if accessor.componentType == FloatComponent: result.points = readAccessorVec3( primInfo.attributes.position, accessors, bufferViews, buffers ) - elif accessor.componentType == GL_UNSIGNED_SHORT: + elif accessor.componentType == UnsignedShortComponent: let bufferView = bufferViews[accessor.bufferView] buffer = buffers[bufferView.buffer] @@ -794,7 +871,7 @@ proc loadPrimitive( float32 buffer.readUint16(start + i * stride + 2), float32 buffer.readUint16(start + i * stride + 4) ) - elif accessor.componentType == GL_UNSIGNED_INT: + elif accessor.componentType == UnsignedIntComponent: let bufferView = bufferViews[accessor.bufferView] buffer = buffers[bufferView.buffer] @@ -840,7 +917,7 @@ proc loadPrimitive( buffer = buffers[bufferView.buffer] start = bufferView.byteOffset + accessor.byteOffset if accessor.kind == atVEC4: - if accessor.componentType == cGL_FLOAT: + if accessor.componentType == FloatComponent: var stride = bufferView.byteStride if stride == 0: stride = 16 @@ -851,7 +928,7 @@ proc loadPrimitive( (buffer.readFloat32(start + i * stride + 8) * 255).uint8, (buffer.readFloat32(start + i * stride + 12) * 255).uint8 ).rgbx) - elif accessor.componentType == GL_UNSIGNED_BYTE: + elif accessor.componentType == UnsignedByteComponent: result.colors.setLen(accessor.count) if bufferView.byteStride == 0 or bufferView.byteStride == 4: copyMem(result.colors[0].addr, buffer[start].addr, accessor.count * 4) @@ -870,7 +947,7 @@ proc loadPrimitive( "Invalid color component type: " & $accessor.componentType.int ) elif accessor.kind == atVEC3: - if accessor.componentType == cGL_FLOAT: + if accessor.componentType == FloatComponent: var stride = bufferView.byteStride if stride == 0: stride = 12 @@ -881,7 +958,7 @@ proc loadPrimitive( (buffer.readFloat32(start + i * stride + 8) * 255).uint8, 255 )) - elif accessor.componentType == GL_UNSIGNED_BYTE: + elif accessor.componentType == UnsignedByteComponent: result.colors.setLen(accessor.count) var stride = bufferView.byteStride if stride == 0: @@ -893,7 +970,7 @@ proc loadPrimitive( buffer.readUint8(start + i * stride + 2), 255 ) - elif accessor.componentType == GL_UNSIGNED_SHORT: + elif accessor.componentType == UnsignedShortComponent: result.colors.setLen(accessor.count) var stride = bufferView.byteStride if stride == 0: @@ -1081,7 +1158,7 @@ proc loadModelJsonInternal( if "target" in entry: let target = entry["target"].getInt() - if target notin @[GL_ARRAY_BUFFER.int, GL_ELEMENT_ARRAY_BUFFER.int]: + if target notin @[GltfArrayBufferTarget, GltfElementArrayBufferTarget]: raise newException(GltfError, &"Invalid bufferView target {target}") bufferViews.add(bufferView) @@ -1094,7 +1171,7 @@ proc loadModelJsonInternal( if "byteOffset" in entry: accessor.byteOffset = entry{"byteOffset"}.getInt() accessor.count = entry["count"].getInt() - accessor.componentType = entry["componentType"].getInt().GLenum + accessor.componentType = parseComponentType(entry["componentType"].getInt()) accessor.normalized = entry{"normalized"}.getBool() if "sparse" in entry: let sparse = entry["sparse"] @@ -1105,7 +1182,7 @@ proc loadModelJsonInternal( accessor.sparse.indices.byteOffset = sparse["indices"]{"byteOffset"}.getInt() accessor.sparse.indices.componentType = - sparse["indices"]["componentType"].getInt().GLenum + parseComponentType(sparse["indices"]["componentType"].getInt()) accessor.sparse.values.bufferView = sparse["values"]["bufferView"].getInt() accessor.sparse.values.byteOffset = @@ -1153,14 +1230,14 @@ proc loadModelJsonInternal( textures.add(texture) var images: seq[Image] + var imageKtx2Data: seq[string] var imageNames: seq[string] - var imageTextureIds: seq[GLuint] if "images" in jsonRoot: for entry in jsonRoot["images"]: var image: Image imageName = entry{"name"}.getStr() - textureId: GLuint + ktx2Data: string if "uri" in entry: let uri = entry["uri"].getStr().decodeUriComponent() if imageName.len == 0: @@ -1169,13 +1246,13 @@ proc loadModelJsonInternal( uri.startsWith("data:image/jpeg"): image = decodeImage(decode(uri.split(',')[1])) elif uri.startsWith("data:image/ktx2"): - textureId = loadKtx2Texture(decode(uri.split(',')[1])) + ktx2Data = decode(uri.split(',')[1]) elif uri.endsWith(".png") or uri.endsWith(".jpg") or uri.endsWith(".jpeg"): image = readImage(joinPath(modelDir, uri)) elif uri.endsWith(".ktx2"): - textureId = loadKtx2TextureFile(joinPath(modelDir, uri)) + ktx2Data = readFile(joinPath(modelDir, uri)) else: raise newException(GltfError, &"Unsupported file extension {uri}") elif "bufferView" in entry: @@ -1186,35 +1263,35 @@ proc loadModelJsonInternal( imageData = ib[bv.byteOffset ..< bv.byteOffset + bv.byteLength] let mimeType = entry{"mimeType"}.getStr() if mimeType == "image/ktx2": - textureId = loadKtx2Texture(imageData) + ktx2Data = imageData else: image = decodeImage(imageData) else: raise newException(GltfError, "Unsupported image type") images.add(image) + imageKtx2Data.add(ktx2Data) imageNames.add(imageName) - imageTextureIds.add(textureId) var samplers: seq[Sampler] if "samplers" in jsonRoot: for entry in jsonRoot["samplers"]: var sampler = Sampler() if "magFilter" in entry: - sampler.magFilter = entry["magFilter"].getInt().GLint + sampler.magFilter = parseTextureMagFilter(entry["magFilter"].getInt()) else: - sampler.magFilter = GL_LINEAR + sampler.magFilter = LinearMagFilter if "minFilter" in entry: - sampler.minFilter = entry["minFilter"].getInt().GLint + sampler.minFilter = parseTextureMinFilter(entry["minFilter"].getInt()) else: - sampler.minFilter = GL_LINEAR_MIPMAP_LINEAR + sampler.minFilter = LinearMipmapLinearMinFilter if "wrapS" in entry: - sampler.wrapS = entry["wrapS"].getInt().GLint + sampler.wrapS = parseTextureWrap(entry["wrapS"].getInt()) else: - sampler.wrapS = GL_REPEAT + sampler.wrapS = RepeatWrap if "wrapT" in entry: - sampler.wrapT = entry["wrapT"].getInt().GLint + sampler.wrapT = parseTextureWrap(entry["wrapT"].getInt()) else: - sampler.wrapT = GL_REPEAT + sampler.wrapT = RepeatWrap samplers.add(sampler) var materials: seq[MaterialInfo] @@ -1360,7 +1437,7 @@ proc loadModelJsonInternal( let cameraType = entry["type"].getStr() case cameraType of "perspective": - camera.kind = ckPerspective + camera.kind = PerspectiveLens let perspectiveInfo = entry["perspective"] camera.perspective.yfov = perspectiveInfo["yfov"].getFloat().float32 @@ -1377,7 +1454,7 @@ proc loadModelJsonInternal( else: camera.perspective.zfar = 0.0 of "orthographic": - camera.kind = ckOrthographic + camera.kind = OrthographicLens let orthographicInfo = entry["orthographic"] camera.orthographic.xmag = orthographicInfo["xmag"].getFloat().float32 @@ -1450,9 +1527,9 @@ proc loadModelJsonInternal( else: prim.material = -1 if "mode" in primitive: - prim.mode = primitive["mode"].getInt().GLenum + prim.mode = parsePrimitiveMode(primitive["mode"].getInt()) else: - prim.mode = GL_TRIANGLES + prim.mode = TrianglesMode if "targets" in primitive: for target in primitive["targets"]: var morphTarget = MorphTargetInfo() @@ -1871,11 +1948,11 @@ proc loadModelJsonInternal( bufferViews, buffers, images, + imageKtx2Data, imageNames, textures, samplers, - materials, - imageTextureIds + materials )) n.mesh = runtimeMesh n.morphWeights = meshInfo.weights diff --git a/src/gltf/shaders.nim b/src/gltf/shaders.nim index 57dc3d2..0c7b233 100644 --- a/src/gltf/shaders.nim +++ b/src/gltf/shaders.nim @@ -54,6 +54,17 @@ proc compileShaderFiles*(vertShaderSrc: string, fragShaderSrc: string): GLuint = # Attach shaders to a GL program. var program = glCreateProgram() + template bindAttrib(location: GLuint, name: static[string]) = + glBindAttribLocation(program, location, name.cstring) + + bindAttrib(0.GLuint, "vertexPosition") + bindAttrib(1.GLuint, "vertexColor") + bindAttrib(2.GLuint, "vertexNormal") + bindAttrib(3.GLuint, "vertexUV") + bindAttrib(4.GLuint, "vertexTangent") + bindAttrib(5.GLuint, "vertexJoints") + bindAttrib(6.GLuint, "vertexWeights") + bindAttrib(7.GLuint, "vertexUV1") glAttachShader(program, vertShader) glAttachShader(program, fragShader) diff --git a/src/gltf/writer.nim b/src/gltf/writer.nim index bf41909..6faf401 100644 --- a/src/gltf/writer.nim +++ b/src/gltf/writer.nim @@ -1,6 +1,6 @@ import std/[json, os, osproc, strutils, tables, uri], - flatty/binny, opengl, pixie, pixie/fileformats/png, vmath, + flatty/binny, pixie, pixie/fileformats/png, vmath, common, internal export common @@ -58,7 +58,7 @@ proc addAccessor( data: var string, payload: string, kind: AccessorKind, - component: GLenum, + component: ComponentType, count: int, stride = 0 ): int = @@ -287,10 +287,10 @@ proc writeGLB*( usedImageFileNames = initTable[string, int]() samplers.add(Sampler( - magFilter: GL_LINEAR, - minFilter: GL_LINEAR_MIPMAP_LINEAR, - wrapS: GL_REPEAT, - wrapT: GL_REPEAT + magFilter: LinearMagFilter, + minFilter: LinearMipmapLinearMinFilter, + wrapS: RepeatWrap, + wrapT: RepeatWrap )) var materialIds = initTable[pointer, int]() @@ -453,7 +453,7 @@ proc writeGLB*( data, payload, atVEC3, - cGL_FLOAT, + FloatComponent, primitive.points.len, 0 ) @@ -472,7 +472,7 @@ proc writeGLB*( data, payload, atVEC3, - cGL_FLOAT, + FloatComponent, primitive.normals.len, 0 ) @@ -489,7 +489,7 @@ proc writeGLB*( data, payload, atVEC2, - cGL_FLOAT, + FloatComponent, primitive.uvs.len, 0 ) @@ -509,7 +509,7 @@ proc writeGLB*( data, payload, atVEC4, - GL_UNSIGNED_BYTE, + UnsignedByteComponent, primitive.colors.len, 0 ) @@ -532,7 +532,7 @@ proc writeGLB*( data, payload, atSCALAR, - GL_UNSIGNED_BYTE, + UnsignedByteComponent, primitive.indices16.len, 0 ) @@ -546,7 +546,7 @@ proc writeGLB*( data, payload, atSCALAR, - cGL_UNSIGNED_SHORT, + UnsignedShortComponent, primitive.indices16.len, 0 ) @@ -560,7 +560,7 @@ proc writeGLB*( data, payload, atSCALAR, - GL_UNSIGNED_INT, + UnsignedIntComponent, primitive.indices32.len, 0 ) @@ -673,10 +673,10 @@ proc writeGLB*( jsonRoot["samplers"] = newJArray() for s in samplers: jsonRoot["samplers"].add(%*{ - "magFilter": s.magFilter, - "minFilter": s.minFilter, - "wrapS": s.wrapS, - "wrapT": s.wrapT + "magFilter": s.magFilter.int, + "minFilter": s.minFilter.int, + "wrapS": s.wrapS.int, + "wrapT": s.wrapT.int }) if textures.len > 0: diff --git a/tests/backend_shaders.nim b/tests/backend_shaders.nim new file mode 100644 index 0000000..287940c --- /dev/null +++ b/tests/backend_shaders.nim @@ -0,0 +1,28 @@ +import std/strutils + +import gltf/backends/shaders + +proc require(haystack, needle, label: string) = + doAssert needle in haystack, label & " missing: " & needle + +require(PbrVertSrc, "#version 410", "OpenGL PBR vertex shader") +require(PbrVertSrc, "uniform mat4 jointMatrices[128];", "OpenGL skinning") +require(PbrFragSrc, "samplerCube environmentMap", "OpenGL environment map") +require(PbrFragSrc, "sampler2DShadow shadowMap", "OpenGL shadow map") + +require(PbrVertHlsl, "float4x4 jointMatrices[128];", "DirectX skinning") +require(PbrFragHlsl, "TextureCube environmentMap", "DirectX cubemap") +require(PbrFragHlsl, "SamplerComparisonState shadowMapSampler", "DirectX shadow sampler") +require(PbrFragHlsl, "shadowMap.SampleCmpLevelZero", "DirectX shadow sample") +require(PbrFragHlsl, "SV_IsFrontFace", "DirectX front-facing input") + +require(PbrVertVulkan, "#version 450", "Vulkan version") +require(PbrVertVulkan, "layout(set = 1, binding = 0, std140)", "Vulkan vertex uniforms") +require(PbrVertVulkan, "layout(location = 0) in vec3 vertexPosition;", "Vulkan inputs") +require(PbrFragVulkan, "layout(set = 0, binding =", "Vulkan descriptors") + +require(PbrVertMsl, "vertex VertexOut vertexMain", "Metal vertex stub") +require(PbrFragMsl, "fragment float4 fragmentMain", "Metal fragment stub") + +doAssert not PbrVertSrc.startsWith("\0") +echo "glTF backend shader codegen tests passed" diff --git a/tests/config.nims b/tests/config.nims index a119208..d5dc419 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -1 +1,4 @@ --path:"../src" + +when not defined(debug): + --define:release diff --git a/tests/sample_assets.nim b/tests/sample_assets.nim index 30a9e28..db0aef3 100644 --- a/tests/sample_assets.nim +++ b/tests/sample_assets.nim @@ -1,8 +1,11 @@ import std/[algorithm, os, sequtils, strformat, strutils, tables, times], - chroma, opengl, pixie, windy, vmath, + chroma, pixie, windy, vmath, gltf +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + import opengl + const WindowSize = 512 VerticalFov = 45'f @@ -28,6 +31,8 @@ type exceptionName: string message: string +var renderer: Renderer + proc hasModel(node: Node): bool = ## Returns true when the node tree has geometry to draw. if node == nil: @@ -149,32 +154,9 @@ proc screenshotPath( proc captureScreenshot(width, height: int): Image = ## Reads the back buffer into an image. - var pixels = newSeq[uint8](width * height * 4) - glPixelStorei(GL_PACK_ALIGNMENT, 1) - glReadBuffer(GL_BACK) - glReadPixels( - 0, - 0, - width.GLsizei, - height.GLsizei, - GL_RGBA, - GL_UNSIGNED_BYTE, - cast[pointer](pixels[0].addr) - ) - - result = newImage(width, height) - for y in 0 ..< height: - let srcY = height - 1 - y - for x in 0 ..< width: - let - src = (srcY * width + x) * 4 - dst = y * width + x - result.data[dst] = rgbx( - pixels[src + 0], - pixels[src + 1], - pixels[src + 2], - pixels[src + 3] - ) + discard width + discard height + renderer.captureScreenshot() proc xray( image: Image, @@ -208,40 +190,42 @@ proc renderScene(window: Window, model: Node) = rotateY(degToRad(OrbitYaw)) * translate(-camCenter) cameraPosition = vec3(cameraMat.inverse.pos) - proj = perspective(VerticalFov, aspectRatio, 0.001, 2000) + proj = + when defined(useDirectX): + perspectiveDxRh(VerticalFov, aspectRatio, 0.001, 2000) + elif defined(useVulkan): + perspectiveVkRh(VerticalFov, aspectRatio, 0.001, 2000) + else: + perspective(VerticalFov, aspectRatio, 0.001, 2000) # The shader negates this vector, so this points the incoming light from # the upper-left/front when shading the model. sunLightDirection = safeNormalize(vec3(1, -4, -2), vec3(1, -1, -1)) rimLightDirection = safeNormalize(vec3(-1, 1, -1), vec3(-1, 1, -1)) - glViewport(0, 0, window.size.x, window.size.y) - glClearColor( - BackgroundColor.r, - BackgroundColor.g, - BackgroundColor.b, - BackgroundColor.a - ) - glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT) - glCullFace(GL_BACK) - glFrontFace(GL_CCW) - glEnable(GL_CULL_FACE) - glEnable(GL_DEPTH_TEST) - glDepthMask(GL_TRUE) - - model.drawPbr( - mat4(), - cameraMat, - proj, - tint = color(1, 1, 1, 1), - useTrs = true, - ambientLightColor = color(0.32, 0.36, 0.46, 0.18), - sunLightDirection = sunLightDirection, - sunLightColor = color(0.95, 0.96, 1.0, 1.0), - rimLightDirection = rimLightDirection, - rimLightColor = color(0.95, 0.72, 0.46, 0.25), - debugView = dvLit, - cameraPosition = cameraPosition + let params = RenderParams( + size: window.size, + clearColor: BackgroundColor, + transform: mat4(), + view: cameraMat, + proj: proj, + tint: color(1, 1, 1, 1), + useTrs: true, + ambientLightColor: color(0.32, 0.36, 0.46, 0.18), + sunLightDirection: sunLightDirection, + sunLightColor: color(0.95, 0.96, 1.0, 1.0), + rimLightDirection: rimLightDirection, + rimLightColor: color(0.95, 0.72, 0.46, 0.25), + debugView: dvLit, + cameraPosition: cameraPosition, + useShadows: false, + drawSkybox: false, + skyboxLod: 0, + vsync: false ) + renderer.beginFrame(window, window.size) + renderer.clearScreen(BackgroundColor) + renderer.render(model, params) + renderer.endFrame() proc testModel( window: Window, @@ -327,7 +311,8 @@ proc testModel( image.writeFile(outPath) result.status = "ok" result.message = "Rendered successfully; baseline screenshot not found." - window.swapBuffers() + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + window.swapBuffers() let renderElapsed = epochTime() - renderStart echo &" rendered in {renderElapsed:>7.3f}s" if result.status.len == 0: @@ -350,7 +335,7 @@ proc testModel( if model != nil: let clearStart = epochTime() echo " phase: cleanup" - model.clearFromGpu() + renderer.release(model) let clearElapsed = epochTime() - clearStart echo &" cleaned in {clearElapsed:>7.3f}s" @@ -425,6 +410,7 @@ proc writeReport(path: string, results: seq[AssetResult]) = writeFile(path, html) let rawParams = commandLineParams() + var updateBaselines = false positionalParams: seq[string] @@ -480,10 +466,14 @@ var window = newWindow( ivec2(WindowSize, WindowSize), msaa = msaa8x ) -makeContextCurrent(window) -loadExtensions() -setupPbr() -loadDefaultEnvironmentMap() +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + makeContextCurrent(window) + loadExtensions() +elif defined(useDirectX) or defined(useVulkan): + loadExtensions() +renderer = newRenderer(window) +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + loadDefaultEnvironmentMap() var results: seq[AssetResult] for i, modelPath in modelPaths: @@ -519,6 +509,8 @@ var hasFailure = false for result in results: if result.status notin ["ok", "skip"]: hasFailure = true +if renderer != nil: + renderer.shutdown() echo "done" if hasFailure: quit(1) diff --git a/tests/screenshots/AlphaBlendModeTest_glTF-Binary_AlphaBlendModeTest.glb.png b/tests/screenshots/AlphaBlendModeTest_glTF-Binary_AlphaBlendModeTest.glb.png index ea913af..737c30e 100644 Binary files a/tests/screenshots/AlphaBlendModeTest_glTF-Binary_AlphaBlendModeTest.glb.png and b/tests/screenshots/AlphaBlendModeTest_glTF-Binary_AlphaBlendModeTest.glb.png differ diff --git a/tests/screenshots/AlphaBlendModeTest_glTF_AlphaBlendModeTest.gltf.png b/tests/screenshots/AlphaBlendModeTest_glTF_AlphaBlendModeTest.gltf.png index ea913af..737c30e 100644 Binary files a/tests/screenshots/AlphaBlendModeTest_glTF_AlphaBlendModeTest.gltf.png and b/tests/screenshots/AlphaBlendModeTest_glTF_AlphaBlendModeTest.gltf.png differ diff --git a/tests/screenshots/AnimatedMorphCube_glTF-Binary_AnimatedMorphCube.glb.png b/tests/screenshots/AnimatedMorphCube_glTF-Binary_AnimatedMorphCube.glb.png index 396387f..4f0c09c 100644 Binary files a/tests/screenshots/AnimatedMorphCube_glTF-Binary_AnimatedMorphCube.glb.png and b/tests/screenshots/AnimatedMorphCube_glTF-Binary_AnimatedMorphCube.glb.png differ diff --git a/tests/screenshots/AnimatedMorphCube_glTF_AnimatedMorphCube.gltf.png b/tests/screenshots/AnimatedMorphCube_glTF_AnimatedMorphCube.gltf.png index 396387f..4f0c09c 100644 Binary files a/tests/screenshots/AnimatedMorphCube_glTF_AnimatedMorphCube.gltf.png and b/tests/screenshots/AnimatedMorphCube_glTF_AnimatedMorphCube.gltf.png differ diff --git a/tests/screenshots/AnimatedTriangle_glTF-Embedded_AnimatedTriangle.gltf.png b/tests/screenshots/AnimatedTriangle_glTF-Embedded_AnimatedTriangle.gltf.png index a1fde1d..28c8d55 100644 Binary files a/tests/screenshots/AnimatedTriangle_glTF-Embedded_AnimatedTriangle.gltf.png and b/tests/screenshots/AnimatedTriangle_glTF-Embedded_AnimatedTriangle.gltf.png differ diff --git a/tests/screenshots/AnimatedTriangle_glTF_AnimatedTriangle.gltf.png b/tests/screenshots/AnimatedTriangle_glTF_AnimatedTriangle.gltf.png index a1fde1d..28c8d55 100644 Binary files a/tests/screenshots/AnimatedTriangle_glTF_AnimatedTriangle.gltf.png and b/tests/screenshots/AnimatedTriangle_glTF_AnimatedTriangle.gltf.png differ diff --git a/tests/screenshots/AntiqueCamera_glTF-Binary_AntiqueCamera.glb.png b/tests/screenshots/AntiqueCamera_glTF-Binary_AntiqueCamera.glb.png index b129c95..7bede5a 100644 Binary files a/tests/screenshots/AntiqueCamera_glTF-Binary_AntiqueCamera.glb.png and b/tests/screenshots/AntiqueCamera_glTF-Binary_AntiqueCamera.glb.png differ diff --git a/tests/screenshots/AntiqueCamera_glTF_AntiqueCamera.gltf.png b/tests/screenshots/AntiqueCamera_glTF_AntiqueCamera.gltf.png index b129c95..7bede5a 100644 Binary files a/tests/screenshots/AntiqueCamera_glTF_AntiqueCamera.gltf.png and b/tests/screenshots/AntiqueCamera_glTF_AntiqueCamera.gltf.png differ diff --git a/tests/screenshots/AttenuationTest_glTF-Binary_AttenuationTest.glb.png b/tests/screenshots/AttenuationTest_glTF-Binary_AttenuationTest.glb.png index 924141c..8c6441b 100644 Binary files a/tests/screenshots/AttenuationTest_glTF-Binary_AttenuationTest.glb.png and b/tests/screenshots/AttenuationTest_glTF-Binary_AttenuationTest.glb.png differ diff --git a/tests/screenshots/AttenuationTest_glTF_AttenuationTest.gltf.png b/tests/screenshots/AttenuationTest_glTF_AttenuationTest.gltf.png index 924141c..8c6441b 100644 Binary files a/tests/screenshots/AttenuationTest_glTF_AttenuationTest.gltf.png and b/tests/screenshots/AttenuationTest_glTF_AttenuationTest.gltf.png differ diff --git a/tests/screenshots/Avocado_glTF-Binary_Avocado.glb.png b/tests/screenshots/Avocado_glTF-Binary_Avocado.glb.png index 807e806..4978911 100644 Binary files a/tests/screenshots/Avocado_glTF-Binary_Avocado.glb.png and b/tests/screenshots/Avocado_glTF-Binary_Avocado.glb.png differ diff --git a/tests/screenshots/Avocado_glTF_Avocado.gltf.png b/tests/screenshots/Avocado_glTF_Avocado.gltf.png index 807e806..4978911 100644 Binary files a/tests/screenshots/Avocado_glTF_Avocado.gltf.png and b/tests/screenshots/Avocado_glTF_Avocado.gltf.png differ diff --git a/tests/screenshots/BoomBoxWithAxes_glTF_BoomBoxWithAxes.gltf.png b/tests/screenshots/BoomBoxWithAxes_glTF_BoomBoxWithAxes.gltf.png index 439c1a1..586a768 100644 Binary files a/tests/screenshots/BoomBoxWithAxes_glTF_BoomBoxWithAxes.gltf.png and b/tests/screenshots/BoomBoxWithAxes_glTF_BoomBoxWithAxes.gltf.png differ diff --git a/tests/screenshots/BoomBox_glTF-Binary_BoomBox.glb.png b/tests/screenshots/BoomBox_glTF-Binary_BoomBox.glb.png index 0195fb2..187a18c 100644 Binary files a/tests/screenshots/BoomBox_glTF-Binary_BoomBox.glb.png and b/tests/screenshots/BoomBox_glTF-Binary_BoomBox.glb.png differ diff --git a/tests/screenshots/BoomBox_glTF_BoomBox.gltf.png b/tests/screenshots/BoomBox_glTF_BoomBox.gltf.png index 0195fb2..187a18c 100644 Binary files a/tests/screenshots/BoomBox_glTF_BoomBox.gltf.png and b/tests/screenshots/BoomBox_glTF_BoomBox.gltf.png differ diff --git a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Binary_BoxTexturedNonPowerOfTwo.glb.png b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Binary_BoxTexturedNonPowerOfTwo.glb.png index c7eb725..73474fd 100644 Binary files a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Binary_BoxTexturedNonPowerOfTwo.glb.png and b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Binary_BoxTexturedNonPowerOfTwo.glb.png differ diff --git a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Embedded_BoxTexturedNonPowerOfTwo.gltf.png b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Embedded_BoxTexturedNonPowerOfTwo.gltf.png index c7eb725..73474fd 100644 Binary files a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Embedded_BoxTexturedNonPowerOfTwo.gltf.png and b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF-Embedded_BoxTexturedNonPowerOfTwo.gltf.png differ diff --git a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF_BoxTexturedNonPowerOfTwo.gltf.png b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF_BoxTexturedNonPowerOfTwo.gltf.png index c7eb725..73474fd 100644 Binary files a/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF_BoxTexturedNonPowerOfTwo.gltf.png and b/tests/screenshots/BoxTexturedNonPowerOfTwo_glTF_BoxTexturedNonPowerOfTwo.gltf.png differ diff --git a/tests/screenshots/BoxTextured_glTF-Binary_BoxTextured.glb.png b/tests/screenshots/BoxTextured_glTF-Binary_BoxTextured.glb.png index 730ce87..87e8daf 100644 Binary files a/tests/screenshots/BoxTextured_glTF-Binary_BoxTextured.glb.png and b/tests/screenshots/BoxTextured_glTF-Binary_BoxTextured.glb.png differ diff --git a/tests/screenshots/BoxTextured_glTF-Embedded_BoxTextured.gltf.png b/tests/screenshots/BoxTextured_glTF-Embedded_BoxTextured.gltf.png index 730ce87..87e8daf 100644 Binary files a/tests/screenshots/BoxTextured_glTF-Embedded_BoxTextured.gltf.png and b/tests/screenshots/BoxTextured_glTF-Embedded_BoxTextured.gltf.png differ diff --git a/tests/screenshots/BoxTextured_glTF_BoxTextured.gltf.png b/tests/screenshots/BoxTextured_glTF_BoxTextured.gltf.png index 730ce87..87e8daf 100644 Binary files a/tests/screenshots/BoxTextured_glTF_BoxTextured.gltf.png and b/tests/screenshots/BoxTextured_glTF_BoxTextured.gltf.png differ diff --git a/tests/screenshots/BrainStem_glTF-Binary_BrainStem.glb.png b/tests/screenshots/BrainStem_glTF-Binary_BrainStem.glb.png index b922797..0faa425 100644 Binary files a/tests/screenshots/BrainStem_glTF-Binary_BrainStem.glb.png and b/tests/screenshots/BrainStem_glTF-Binary_BrainStem.glb.png differ diff --git a/tests/screenshots/BrainStem_glTF-Embedded_BrainStem.gltf.png b/tests/screenshots/BrainStem_glTF-Embedded_BrainStem.gltf.png index b922797..0faa425 100644 Binary files a/tests/screenshots/BrainStem_glTF-Embedded_BrainStem.gltf.png and b/tests/screenshots/BrainStem_glTF-Embedded_BrainStem.gltf.png differ diff --git a/tests/screenshots/BrainStem_glTF_BrainStem.gltf.png b/tests/screenshots/BrainStem_glTF_BrainStem.gltf.png index b922797..0faa425 100644 Binary files a/tests/screenshots/BrainStem_glTF_BrainStem.gltf.png and b/tests/screenshots/BrainStem_glTF_BrainStem.gltf.png differ diff --git a/tests/screenshots/CarConcept_glTF-Binary_CarConcept.glb.png b/tests/screenshots/CarConcept_glTF-Binary_CarConcept.glb.png index 72f7ff0..6f9bd8d 100644 Binary files a/tests/screenshots/CarConcept_glTF-Binary_CarConcept.glb.png and b/tests/screenshots/CarConcept_glTF-Binary_CarConcept.glb.png differ diff --git a/tests/screenshots/CarConcept_glTF-JPG_CarConcept.gltf.png b/tests/screenshots/CarConcept_glTF-JPG_CarConcept.gltf.png index 36552ea..2b7176d 100644 Binary files a/tests/screenshots/CarConcept_glTF-JPG_CarConcept.gltf.png and b/tests/screenshots/CarConcept_glTF-JPG_CarConcept.gltf.png differ diff --git a/tests/screenshots/CarConcept_glTF_CarConcept.gltf.png b/tests/screenshots/CarConcept_glTF_CarConcept.gltf.png index 4265f38..128a6cd 100644 Binary files a/tests/screenshots/CarConcept_glTF_CarConcept.gltf.png and b/tests/screenshots/CarConcept_glTF_CarConcept.gltf.png differ diff --git a/tests/screenshots/ChairDamaskPurplegold_glTF-Binary_ChairDamaskPurplegold.glb.png b/tests/screenshots/ChairDamaskPurplegold_glTF-Binary_ChairDamaskPurplegold.glb.png index 953fb0b..2990253 100644 Binary files a/tests/screenshots/ChairDamaskPurplegold_glTF-Binary_ChairDamaskPurplegold.glb.png and b/tests/screenshots/ChairDamaskPurplegold_glTF-Binary_ChairDamaskPurplegold.glb.png differ diff --git a/tests/screenshots/ChairDamaskPurplegold_glTF_ChairDamaskPurplegold.gltf.png b/tests/screenshots/ChairDamaskPurplegold_glTF_ChairDamaskPurplegold.gltf.png index 953fb0b..2990253 100644 Binary files a/tests/screenshots/ChairDamaskPurplegold_glTF_ChairDamaskPurplegold.gltf.png and b/tests/screenshots/ChairDamaskPurplegold_glTF_ChairDamaskPurplegold.gltf.png differ diff --git a/tests/screenshots/ChronographWatch_glTF-Binary_ChronographWatch.glb.png b/tests/screenshots/ChronographWatch_glTF-Binary_ChronographWatch.glb.png index c6629e2..1d986fe 100644 Binary files a/tests/screenshots/ChronographWatch_glTF-Binary_ChronographWatch.glb.png and b/tests/screenshots/ChronographWatch_glTF-Binary_ChronographWatch.glb.png differ diff --git a/tests/screenshots/ChronographWatch_glTF_ChronographWatch.gltf.png b/tests/screenshots/ChronographWatch_glTF_ChronographWatch.gltf.png index c6629e2..1d986fe 100644 Binary files a/tests/screenshots/ChronographWatch_glTF_ChronographWatch.gltf.png and b/tests/screenshots/ChronographWatch_glTF_ChronographWatch.gltf.png differ diff --git a/tests/screenshots/ClearCoatTest_glTF-Binary_ClearCoatTest.glb.png b/tests/screenshots/ClearCoatTest_glTF-Binary_ClearCoatTest.glb.png index eda3b7a..faf8c6b 100644 Binary files a/tests/screenshots/ClearCoatTest_glTF-Binary_ClearCoatTest.glb.png and b/tests/screenshots/ClearCoatTest_glTF-Binary_ClearCoatTest.glb.png differ diff --git a/tests/screenshots/ClearCoatTest_glTF_ClearCoatTest.gltf.png b/tests/screenshots/ClearCoatTest_glTF_ClearCoatTest.gltf.png index eda3b7a..faf8c6b 100644 Binary files a/tests/screenshots/ClearCoatTest_glTF_ClearCoatTest.gltf.png and b/tests/screenshots/ClearCoatTest_glTF_ClearCoatTest.gltf.png differ diff --git a/tests/screenshots/CompareAlphaCoverage_glTF-Binary_CompareAlphaCoverage.glb.png b/tests/screenshots/CompareAlphaCoverage_glTF-Binary_CompareAlphaCoverage.glb.png index 2cbdf72..cb60886 100644 Binary files a/tests/screenshots/CompareAlphaCoverage_glTF-Binary_CompareAlphaCoverage.glb.png and b/tests/screenshots/CompareAlphaCoverage_glTF-Binary_CompareAlphaCoverage.glb.png differ diff --git a/tests/screenshots/CompareAlphaCoverage_glTF_CompareAlphaCoverage.gltf.png b/tests/screenshots/CompareAlphaCoverage_glTF_CompareAlphaCoverage.gltf.png index 2cbdf72..cb60886 100644 Binary files a/tests/screenshots/CompareAlphaCoverage_glTF_CompareAlphaCoverage.gltf.png and b/tests/screenshots/CompareAlphaCoverage_glTF_CompareAlphaCoverage.gltf.png differ diff --git a/tests/screenshots/CompareBaseColor_glTF-Binary_CompareBaseColor.glb.png b/tests/screenshots/CompareBaseColor_glTF-Binary_CompareBaseColor.glb.png index 310f1f6..e59221c 100644 Binary files a/tests/screenshots/CompareBaseColor_glTF-Binary_CompareBaseColor.glb.png and b/tests/screenshots/CompareBaseColor_glTF-Binary_CompareBaseColor.glb.png differ diff --git a/tests/screenshots/CompareBaseColor_glTF_CompareBaseColor.gltf.png b/tests/screenshots/CompareBaseColor_glTF_CompareBaseColor.gltf.png index 310f1f6..e59221c 100644 Binary files a/tests/screenshots/CompareBaseColor_glTF_CompareBaseColor.gltf.png and b/tests/screenshots/CompareBaseColor_glTF_CompareBaseColor.gltf.png differ diff --git a/tests/screenshots/CompareDispersion_glTF-Binary_CompareDispersion.glb.png b/tests/screenshots/CompareDispersion_glTF-Binary_CompareDispersion.glb.png index f87b5a2..7588459 100644 Binary files a/tests/screenshots/CompareDispersion_glTF-Binary_CompareDispersion.glb.png and b/tests/screenshots/CompareDispersion_glTF-Binary_CompareDispersion.glb.png differ diff --git a/tests/screenshots/CompareDispersion_glTF_CompareDispersion.gltf.png b/tests/screenshots/CompareDispersion_glTF_CompareDispersion.gltf.png index f87b5a2..7588459 100644 Binary files a/tests/screenshots/CompareDispersion_glTF_CompareDispersion.gltf.png and b/tests/screenshots/CompareDispersion_glTF_CompareDispersion.gltf.png differ diff --git a/tests/screenshots/CompareEmissiveStrength_glTF-Binary_CompareEmissiveStrength.glb.png b/tests/screenshots/CompareEmissiveStrength_glTF-Binary_CompareEmissiveStrength.glb.png index 51fb9b0..48f4d38 100644 Binary files a/tests/screenshots/CompareEmissiveStrength_glTF-Binary_CompareEmissiveStrength.glb.png and b/tests/screenshots/CompareEmissiveStrength_glTF-Binary_CompareEmissiveStrength.glb.png differ diff --git a/tests/screenshots/CompareEmissiveStrength_glTF_CompareEmissiveStrength.gltf.png b/tests/screenshots/CompareEmissiveStrength_glTF_CompareEmissiveStrength.gltf.png index 51fb9b0..48f4d38 100644 Binary files a/tests/screenshots/CompareEmissiveStrength_glTF_CompareEmissiveStrength.gltf.png and b/tests/screenshots/CompareEmissiveStrength_glTF_CompareEmissiveStrength.gltf.png differ diff --git a/tests/screenshots/CompareIridescence_glTF-Binary_CompareIridescence.glb.png b/tests/screenshots/CompareIridescence_glTF-Binary_CompareIridescence.glb.png index a17f2c7..b8ccd4c 100644 Binary files a/tests/screenshots/CompareIridescence_glTF-Binary_CompareIridescence.glb.png and b/tests/screenshots/CompareIridescence_glTF-Binary_CompareIridescence.glb.png differ diff --git a/tests/screenshots/CompareIridescence_glTF_CompareIridescence.gltf.png b/tests/screenshots/CompareIridescence_glTF_CompareIridescence.gltf.png index a17f2c7..b8ccd4c 100644 Binary files a/tests/screenshots/CompareIridescence_glTF_CompareIridescence.gltf.png and b/tests/screenshots/CompareIridescence_glTF_CompareIridescence.gltf.png differ diff --git a/tests/screenshots/CompareNormal_glTF-Binary_CompareNormal.glb.png b/tests/screenshots/CompareNormal_glTF-Binary_CompareNormal.glb.png index 0a74cd3..e45fde4 100644 Binary files a/tests/screenshots/CompareNormal_glTF-Binary_CompareNormal.glb.png and b/tests/screenshots/CompareNormal_glTF-Binary_CompareNormal.glb.png differ diff --git a/tests/screenshots/CompareNormal_glTF_CompareNormal.gltf.png b/tests/screenshots/CompareNormal_glTF_CompareNormal.gltf.png index 0a74cd3..e45fde4 100644 Binary files a/tests/screenshots/CompareNormal_glTF_CompareNormal.gltf.png and b/tests/screenshots/CompareNormal_glTF_CompareNormal.gltf.png differ diff --git a/tests/screenshots/CompareRoughness_glTF-Binary_CompareRoughness.glb.png b/tests/screenshots/CompareRoughness_glTF-Binary_CompareRoughness.glb.png index 8d83d11..880e8c2 100644 Binary files a/tests/screenshots/CompareRoughness_glTF-Binary_CompareRoughness.glb.png and b/tests/screenshots/CompareRoughness_glTF-Binary_CompareRoughness.glb.png differ diff --git a/tests/screenshots/CompareRoughness_glTF_CompareRoughness.gltf.png b/tests/screenshots/CompareRoughness_glTF_CompareRoughness.gltf.png index 8d83d11..880e8c2 100644 Binary files a/tests/screenshots/CompareRoughness_glTF_CompareRoughness.gltf.png and b/tests/screenshots/CompareRoughness_glTF_CompareRoughness.gltf.png differ diff --git a/tests/screenshots/CompareSpecular_glTF-Binary_CompareSpecular.glb.png b/tests/screenshots/CompareSpecular_glTF-Binary_CompareSpecular.glb.png index 7fb21cb..6b4aec7 100644 Binary files a/tests/screenshots/CompareSpecular_glTF-Binary_CompareSpecular.glb.png and b/tests/screenshots/CompareSpecular_glTF-Binary_CompareSpecular.glb.png differ diff --git a/tests/screenshots/CompareSpecular_glTF_CompareSpecular.gltf.png b/tests/screenshots/CompareSpecular_glTF_CompareSpecular.gltf.png index 7fb21cb..6b4aec7 100644 Binary files a/tests/screenshots/CompareSpecular_glTF_CompareSpecular.gltf.png and b/tests/screenshots/CompareSpecular_glTF_CompareSpecular.gltf.png differ diff --git a/tests/screenshots/CompareTransmission_glTF-Binary_CompareTransmission.glb.png b/tests/screenshots/CompareTransmission_glTF-Binary_CompareTransmission.glb.png index 0fc9794..2fe7b7f 100644 Binary files a/tests/screenshots/CompareTransmission_glTF-Binary_CompareTransmission.glb.png and b/tests/screenshots/CompareTransmission_glTF-Binary_CompareTransmission.glb.png differ diff --git a/tests/screenshots/CompareTransmission_glTF_CompareTransmission.gltf.png b/tests/screenshots/CompareTransmission_glTF_CompareTransmission.gltf.png index 7cb2e53..920eec6 100644 Binary files a/tests/screenshots/CompareTransmission_glTF_CompareTransmission.gltf.png and b/tests/screenshots/CompareTransmission_glTF_CompareTransmission.gltf.png differ diff --git a/tests/screenshots/CompareVolume_glTF-Binary_CompareVolume.glb.png b/tests/screenshots/CompareVolume_glTF-Binary_CompareVolume.glb.png index d89bd6e..19cc340 100644 Binary files a/tests/screenshots/CompareVolume_glTF-Binary_CompareVolume.glb.png and b/tests/screenshots/CompareVolume_glTF-Binary_CompareVolume.glb.png differ diff --git a/tests/screenshots/CompareVolume_glTF_CompareVolume.gltf.png b/tests/screenshots/CompareVolume_glTF_CompareVolume.gltf.png index 6207c52..37e73fa 100644 Binary files a/tests/screenshots/CompareVolume_glTF_CompareVolume.gltf.png and b/tests/screenshots/CompareVolume_glTF_CompareVolume.gltf.png differ diff --git a/tests/screenshots/Corset_glTF-Binary_Corset.glb.png b/tests/screenshots/Corset_glTF-Binary_Corset.glb.png index 754c197..bb5d77e 100644 Binary files a/tests/screenshots/Corset_glTF-Binary_Corset.glb.png and b/tests/screenshots/Corset_glTF-Binary_Corset.glb.png differ diff --git a/tests/screenshots/Corset_glTF_Corset.gltf.png b/tests/screenshots/Corset_glTF_Corset.gltf.png index 754c197..bb5d77e 100644 Binary files a/tests/screenshots/Corset_glTF_Corset.gltf.png and b/tests/screenshots/Corset_glTF_Corset.gltf.png differ diff --git a/tests/screenshots/CubeVisibility_glTF-Binary_CubeVisibility.glb.png b/tests/screenshots/CubeVisibility_glTF-Binary_CubeVisibility.glb.png new file mode 100644 index 0000000..4e33650 Binary files /dev/null and b/tests/screenshots/CubeVisibility_glTF-Binary_CubeVisibility.glb.png differ diff --git a/tests/screenshots/CubeVisibility_glTF_CubeVisibility.gltf.png b/tests/screenshots/CubeVisibility_glTF_CubeVisibility.gltf.png new file mode 100644 index 0000000..4e33650 Binary files /dev/null and b/tests/screenshots/CubeVisibility_glTF_CubeVisibility.gltf.png differ diff --git a/tests/screenshots/DiffuseTransmissionPlant_glTF-Binary_DiffuseTransmissionPlant.glb.png b/tests/screenshots/DiffuseTransmissionPlant_glTF-Binary_DiffuseTransmissionPlant.glb.png index 0dac4f2..32732d6 100644 Binary files a/tests/screenshots/DiffuseTransmissionPlant_glTF-Binary_DiffuseTransmissionPlant.glb.png and b/tests/screenshots/DiffuseTransmissionPlant_glTF-Binary_DiffuseTransmissionPlant.glb.png differ diff --git a/tests/screenshots/DiffuseTransmissionPlant_glTF_DiffuseTransmissionPlant.gltf.png b/tests/screenshots/DiffuseTransmissionPlant_glTF_DiffuseTransmissionPlant.gltf.png index 0dac4f2..32732d6 100644 Binary files a/tests/screenshots/DiffuseTransmissionPlant_glTF_DiffuseTransmissionPlant.gltf.png and b/tests/screenshots/DiffuseTransmissionPlant_glTF_DiffuseTransmissionPlant.gltf.png differ diff --git a/tests/screenshots/DiffuseTransmissionTeacup_glTF-Binary_DiffuseTransmissionTeacup.glb.png b/tests/screenshots/DiffuseTransmissionTeacup_glTF-Binary_DiffuseTransmissionTeacup.glb.png index c028ef1..6accd65 100644 Binary files a/tests/screenshots/DiffuseTransmissionTeacup_glTF-Binary_DiffuseTransmissionTeacup.glb.png and b/tests/screenshots/DiffuseTransmissionTeacup_glTF-Binary_DiffuseTransmissionTeacup.glb.png differ diff --git a/tests/screenshots/DiffuseTransmissionTeacup_glTF_DiffuseTransmissionTeacup.gltf.png b/tests/screenshots/DiffuseTransmissionTeacup_glTF_DiffuseTransmissionTeacup.gltf.png index c028ef1..6accd65 100644 Binary files a/tests/screenshots/DiffuseTransmissionTeacup_glTF_DiffuseTransmissionTeacup.gltf.png and b/tests/screenshots/DiffuseTransmissionTeacup_glTF_DiffuseTransmissionTeacup.gltf.png differ diff --git a/tests/screenshots/DispersionTest_glTF-Binary_DispersionTest.glb.png b/tests/screenshots/DispersionTest_glTF-Binary_DispersionTest.glb.png index ce4f5d0..3d49d81 100644 Binary files a/tests/screenshots/DispersionTest_glTF-Binary_DispersionTest.glb.png and b/tests/screenshots/DispersionTest_glTF-Binary_DispersionTest.glb.png differ diff --git a/tests/screenshots/DispersionTest_glTF_DispersionTest.gltf.png b/tests/screenshots/DispersionTest_glTF_DispersionTest.gltf.png index ce4f5d0..3d49d81 100644 Binary files a/tests/screenshots/DispersionTest_glTF_DispersionTest.gltf.png and b/tests/screenshots/DispersionTest_glTF_DispersionTest.gltf.png differ diff --git a/tests/screenshots/DragonAttenuation_glTF-Binary_DragonAttenuation.glb.png b/tests/screenshots/DragonAttenuation_glTF-Binary_DragonAttenuation.glb.png index f79e964..befc7bb 100644 Binary files a/tests/screenshots/DragonAttenuation_glTF-Binary_DragonAttenuation.glb.png and b/tests/screenshots/DragonAttenuation_glTF-Binary_DragonAttenuation.glb.png differ diff --git a/tests/screenshots/DragonAttenuation_glTF_DragonAttenuation.gltf.png b/tests/screenshots/DragonAttenuation_glTF_DragonAttenuation.gltf.png index f79e964..befc7bb 100644 Binary files a/tests/screenshots/DragonAttenuation_glTF_DragonAttenuation.gltf.png and b/tests/screenshots/DragonAttenuation_glTF_DragonAttenuation.gltf.png differ diff --git a/tests/screenshots/DragonDispersion_glTF-Binary_DragonDispersion.glb.png b/tests/screenshots/DragonDispersion_glTF-Binary_DragonDispersion.glb.png index 203f63b..b6e0716 100644 Binary files a/tests/screenshots/DragonDispersion_glTF-Binary_DragonDispersion.glb.png and b/tests/screenshots/DragonDispersion_glTF-Binary_DragonDispersion.glb.png differ diff --git a/tests/screenshots/DragonDispersion_glTF_DragonDispersion.gltf.png b/tests/screenshots/DragonDispersion_glTF_DragonDispersion.gltf.png index 203f63b..b6e0716 100644 Binary files a/tests/screenshots/DragonDispersion_glTF_DragonDispersion.gltf.png and b/tests/screenshots/DragonDispersion_glTF_DragonDispersion.gltf.png differ diff --git a/tests/screenshots/EnvironmentTest_glTF-IBL_EnvironmentTest.gltf.png b/tests/screenshots/EnvironmentTest_glTF-IBL_EnvironmentTest.gltf.png index 0f9cddd..81744b6 100644 Binary files a/tests/screenshots/EnvironmentTest_glTF-IBL_EnvironmentTest.gltf.png and b/tests/screenshots/EnvironmentTest_glTF-IBL_EnvironmentTest.gltf.png differ diff --git a/tests/screenshots/EnvironmentTest_glTF_EnvironmentTest.gltf.png b/tests/screenshots/EnvironmentTest_glTF_EnvironmentTest.gltf.png index 0f9cddd..81744b6 100644 Binary files a/tests/screenshots/EnvironmentTest_glTF_EnvironmentTest.gltf.png and b/tests/screenshots/EnvironmentTest_glTF_EnvironmentTest.gltf.png differ diff --git a/tests/screenshots/FlightHelmet_glTF_FlightHelmet.gltf.png b/tests/screenshots/FlightHelmet_glTF_FlightHelmet.gltf.png index d9bf0a8..0c02714 100644 Binary files a/tests/screenshots/FlightHelmet_glTF_FlightHelmet.gltf.png and b/tests/screenshots/FlightHelmet_glTF_FlightHelmet.gltf.png differ diff --git a/tests/screenshots/GlamVelvetSofa_glTF-Binary_GlamVelvetSofa.glb.png b/tests/screenshots/GlamVelvetSofa_glTF-Binary_GlamVelvetSofa.glb.png index 3f27077..d85388f 100644 Binary files a/tests/screenshots/GlamVelvetSofa_glTF-Binary_GlamVelvetSofa.glb.png and b/tests/screenshots/GlamVelvetSofa_glTF-Binary_GlamVelvetSofa.glb.png differ diff --git a/tests/screenshots/GlamVelvetSofa_glTF_GlamVelvetSofa.gltf.png b/tests/screenshots/GlamVelvetSofa_glTF_GlamVelvetSofa.gltf.png index 3f27077..d85388f 100644 Binary files a/tests/screenshots/GlamVelvetSofa_glTF_GlamVelvetSofa.gltf.png and b/tests/screenshots/GlamVelvetSofa_glTF_GlamVelvetSofa.gltf.png differ diff --git a/tests/screenshots/GlassVaseFlowers_glTF-Binary_GlassVaseFlowers.glb.png b/tests/screenshots/GlassVaseFlowers_glTF-Binary_GlassVaseFlowers.glb.png index 4efc4a1..b5a5cac 100644 Binary files a/tests/screenshots/GlassVaseFlowers_glTF-Binary_GlassVaseFlowers.glb.png and b/tests/screenshots/GlassVaseFlowers_glTF-Binary_GlassVaseFlowers.glb.png differ diff --git a/tests/screenshots/GlassVaseFlowers_glTF_GlassVaseFlowers.gltf.png b/tests/screenshots/GlassVaseFlowers_glTF_GlassVaseFlowers.gltf.png index e091d03..314e1bf 100644 Binary files a/tests/screenshots/GlassVaseFlowers_glTF_GlassVaseFlowers.gltf.png and b/tests/screenshots/GlassVaseFlowers_glTF_GlassVaseFlowers.gltf.png differ diff --git a/tests/screenshots/IridescenceAbalone_glTF-Binary_IridescenceAbalone.glb.png b/tests/screenshots/IridescenceAbalone_glTF-Binary_IridescenceAbalone.glb.png index c83b8fd..14ef20f 100644 Binary files a/tests/screenshots/IridescenceAbalone_glTF-Binary_IridescenceAbalone.glb.png and b/tests/screenshots/IridescenceAbalone_glTF-Binary_IridescenceAbalone.glb.png differ diff --git a/tests/screenshots/IridescenceAbalone_glTF_IridescenceAbalone.gltf.png b/tests/screenshots/IridescenceAbalone_glTF_IridescenceAbalone.gltf.png index c83b8fd..14ef20f 100644 Binary files a/tests/screenshots/IridescenceAbalone_glTF_IridescenceAbalone.gltf.png and b/tests/screenshots/IridescenceAbalone_glTF_IridescenceAbalone.gltf.png differ diff --git a/tests/screenshots/IridescenceDielectricSpheres_glTF_IridescenceDielectricSpheres.gltf.png b/tests/screenshots/IridescenceDielectricSpheres_glTF_IridescenceDielectricSpheres.gltf.png index 03aef69..e65fadd 100644 Binary files a/tests/screenshots/IridescenceDielectricSpheres_glTF_IridescenceDielectricSpheres.gltf.png and b/tests/screenshots/IridescenceDielectricSpheres_glTF_IridescenceDielectricSpheres.gltf.png differ diff --git a/tests/screenshots/IridescentDishWithOlives_glTF-Binary_IridescentDishWithOlives.glb.png b/tests/screenshots/IridescentDishWithOlives_glTF-Binary_IridescentDishWithOlives.glb.png index 701e020..c528a91 100644 Binary files a/tests/screenshots/IridescentDishWithOlives_glTF-Binary_IridescentDishWithOlives.glb.png and b/tests/screenshots/IridescentDishWithOlives_glTF-Binary_IridescentDishWithOlives.glb.png differ diff --git a/tests/screenshots/IridescentDishWithOlives_glTF_IridescentDishWithOlives.gltf.png b/tests/screenshots/IridescentDishWithOlives_glTF_IridescentDishWithOlives.gltf.png index 058beb3..3a93269 100644 Binary files a/tests/screenshots/IridescentDishWithOlives_glTF_IridescentDishWithOlives.gltf.png and b/tests/screenshots/IridescentDishWithOlives_glTF_IridescentDishWithOlives.gltf.png differ diff --git a/tests/screenshots/Lantern_glTF-Binary_Lantern.glb.png b/tests/screenshots/Lantern_glTF-Binary_Lantern.glb.png index a0c7f73..f2c1678 100644 Binary files a/tests/screenshots/Lantern_glTF-Binary_Lantern.glb.png and b/tests/screenshots/Lantern_glTF-Binary_Lantern.glb.png differ diff --git a/tests/screenshots/Lantern_glTF_Lantern.gltf.png b/tests/screenshots/Lantern_glTF_Lantern.gltf.png index a0c7f73..f2c1678 100644 Binary files a/tests/screenshots/Lantern_glTF_Lantern.gltf.png and b/tests/screenshots/Lantern_glTF_Lantern.gltf.png differ diff --git a/tests/screenshots/LightsPunctualLamp_glTF-Binary_LightsPunctualLamp.glb.png b/tests/screenshots/LightsPunctualLamp_glTF-Binary_LightsPunctualLamp.glb.png index 8bcd3f0..fcd6dde 100644 Binary files a/tests/screenshots/LightsPunctualLamp_glTF-Binary_LightsPunctualLamp.glb.png and b/tests/screenshots/LightsPunctualLamp_glTF-Binary_LightsPunctualLamp.glb.png differ diff --git a/tests/screenshots/LightsPunctualLamp_glTF_LightsPunctualLamp.gltf.png b/tests/screenshots/LightsPunctualLamp_glTF_LightsPunctualLamp.gltf.png index b94ed8b..3aa10ca 100644 Binary files a/tests/screenshots/LightsPunctualLamp_glTF_LightsPunctualLamp.gltf.png and b/tests/screenshots/LightsPunctualLamp_glTF_LightsPunctualLamp.gltf.png differ diff --git a/tests/screenshots/MandarinOrange_glTF_MandarinOrange.gltf.png b/tests/screenshots/MandarinOrange_glTF_MandarinOrange.gltf.png index fe526db..24fb092 100644 Binary files a/tests/screenshots/MandarinOrange_glTF_MandarinOrange.gltf.png and b/tests/screenshots/MandarinOrange_glTF_MandarinOrange.gltf.png differ diff --git a/tests/screenshots/MaterialsVariantsShoe_glTF-Binary_MaterialsVariantsShoe.glb.png b/tests/screenshots/MaterialsVariantsShoe_glTF-Binary_MaterialsVariantsShoe.glb.png index 6dd361a..8d97d3d 100644 Binary files a/tests/screenshots/MaterialsVariantsShoe_glTF-Binary_MaterialsVariantsShoe.glb.png and b/tests/screenshots/MaterialsVariantsShoe_glTF-Binary_MaterialsVariantsShoe.glb.png differ diff --git a/tests/screenshots/MaterialsVariantsShoe_glTF_MaterialsVariantsShoe.gltf.png b/tests/screenshots/MaterialsVariantsShoe_glTF_MaterialsVariantsShoe.gltf.png index 6dd361a..8d97d3d 100644 Binary files a/tests/screenshots/MaterialsVariantsShoe_glTF_MaterialsVariantsShoe.gltf.png and b/tests/screenshots/MaterialsVariantsShoe_glTF_MaterialsVariantsShoe.gltf.png differ diff --git a/tests/screenshots/MorphStressTest_glTF-Binary_MorphStressTest.glb.png b/tests/screenshots/MorphStressTest_glTF-Binary_MorphStressTest.glb.png index 9340f17..70be279 100644 Binary files a/tests/screenshots/MorphStressTest_glTF-Binary_MorphStressTest.glb.png and b/tests/screenshots/MorphStressTest_glTF-Binary_MorphStressTest.glb.png differ diff --git a/tests/screenshots/MorphStressTest_glTF_MorphStressTest.gltf.png b/tests/screenshots/MorphStressTest_glTF_MorphStressTest.gltf.png index 9340f17..70be279 100644 Binary files a/tests/screenshots/MorphStressTest_glTF_MorphStressTest.gltf.png and b/tests/screenshots/MorphStressTest_glTF_MorphStressTest.gltf.png differ diff --git a/tests/screenshots/MosquitoInAmber_glTF-Binary_MosquitoInAmber.glb.png b/tests/screenshots/MosquitoInAmber_glTF-Binary_MosquitoInAmber.glb.png index 628e52d..44900c5 100644 Binary files a/tests/screenshots/MosquitoInAmber_glTF-Binary_MosquitoInAmber.glb.png and b/tests/screenshots/MosquitoInAmber_glTF-Binary_MosquitoInAmber.glb.png differ diff --git a/tests/screenshots/MosquitoInAmber_glTF_MosquitoInAmber.gltf.png b/tests/screenshots/MosquitoInAmber_glTF_MosquitoInAmber.gltf.png index f1ef034..139e954 100644 Binary files a/tests/screenshots/MosquitoInAmber_glTF_MosquitoInAmber.gltf.png and b/tests/screenshots/MosquitoInAmber_glTF_MosquitoInAmber.gltf.png differ diff --git a/tests/screenshots/NegativeScaleTest_glTF-Binary_NegativeScaleTest.glb.png b/tests/screenshots/NegativeScaleTest_glTF-Binary_NegativeScaleTest.glb.png index 904f26a..cc8140a 100644 Binary files a/tests/screenshots/NegativeScaleTest_glTF-Binary_NegativeScaleTest.glb.png and b/tests/screenshots/NegativeScaleTest_glTF-Binary_NegativeScaleTest.glb.png differ diff --git a/tests/screenshots/NegativeScaleTest_glTF_NegativeScaleTest.gltf.png b/tests/screenshots/NegativeScaleTest_glTF_NegativeScaleTest.gltf.png index 904f26a..cc8140a 100644 Binary files a/tests/screenshots/NegativeScaleTest_glTF_NegativeScaleTest.gltf.png and b/tests/screenshots/NegativeScaleTest_glTF_NegativeScaleTest.gltf.png differ diff --git a/tests/screenshots/NormalTangentMirrorTest_glTF-Binary_NormalTangentMirrorTest.glb.png b/tests/screenshots/NormalTangentMirrorTest_glTF-Binary_NormalTangentMirrorTest.glb.png index 7fffabf..ee1ea2c 100644 Binary files a/tests/screenshots/NormalTangentMirrorTest_glTF-Binary_NormalTangentMirrorTest.glb.png and b/tests/screenshots/NormalTangentMirrorTest_glTF-Binary_NormalTangentMirrorTest.glb.png differ diff --git a/tests/screenshots/NormalTangentMirrorTest_glTF_NormalTangentMirrorTest.gltf.png b/tests/screenshots/NormalTangentMirrorTest_glTF_NormalTangentMirrorTest.gltf.png index 7fffabf..ee1ea2c 100644 Binary files a/tests/screenshots/NormalTangentMirrorTest_glTF_NormalTangentMirrorTest.gltf.png and b/tests/screenshots/NormalTangentMirrorTest_glTF_NormalTangentMirrorTest.gltf.png differ diff --git a/tests/screenshots/PointLightIntensityTest_glTF-Binary_PointLightIntensityTest.glb.png b/tests/screenshots/PointLightIntensityTest_glTF-Binary_PointLightIntensityTest.glb.png index 9a65e0c..06aa32c 100644 Binary files a/tests/screenshots/PointLightIntensityTest_glTF-Binary_PointLightIntensityTest.glb.png and b/tests/screenshots/PointLightIntensityTest_glTF-Binary_PointLightIntensityTest.glb.png differ diff --git a/tests/screenshots/PointLightIntensityTest_glTF_PointLightIntensityTest.gltf.png b/tests/screenshots/PointLightIntensityTest_glTF_PointLightIntensityTest.gltf.png index 9a65e0c..06aa32c 100644 Binary files a/tests/screenshots/PointLightIntensityTest_glTF_PointLightIntensityTest.gltf.png and b/tests/screenshots/PointLightIntensityTest_glTF_PointLightIntensityTest.gltf.png differ diff --git a/tests/screenshots/RecursiveSkeletons_glTF-Binary_RecursiveSkeletons.glb.png b/tests/screenshots/RecursiveSkeletons_glTF-Binary_RecursiveSkeletons.glb.png index 8a41720..5bc5c67 100644 Binary files a/tests/screenshots/RecursiveSkeletons_glTF-Binary_RecursiveSkeletons.glb.png and b/tests/screenshots/RecursiveSkeletons_glTF-Binary_RecursiveSkeletons.glb.png differ diff --git a/tests/screenshots/RecursiveSkeletons_glTF_RecursiveSkeletons.gltf.png b/tests/screenshots/RecursiveSkeletons_glTF_RecursiveSkeletons.gltf.png index 8a41720..5bc5c67 100644 Binary files a/tests/screenshots/RecursiveSkeletons_glTF_RecursiveSkeletons.gltf.png and b/tests/screenshots/RecursiveSkeletons_glTF_RecursiveSkeletons.gltf.png differ diff --git a/tests/screenshots/RiggedFigure_glTF-Binary_RiggedFigure.glb.png b/tests/screenshots/RiggedFigure_glTF-Binary_RiggedFigure.glb.png index 1e3159b..fd69763 100644 Binary files a/tests/screenshots/RiggedFigure_glTF-Binary_RiggedFigure.glb.png and b/tests/screenshots/RiggedFigure_glTF-Binary_RiggedFigure.glb.png differ diff --git a/tests/screenshots/RiggedFigure_glTF-Embedded_RiggedFigure.gltf.png b/tests/screenshots/RiggedFigure_glTF-Embedded_RiggedFigure.gltf.png index 1e3159b..fd69763 100644 Binary files a/tests/screenshots/RiggedFigure_glTF-Embedded_RiggedFigure.gltf.png and b/tests/screenshots/RiggedFigure_glTF-Embedded_RiggedFigure.gltf.png differ diff --git a/tests/screenshots/RiggedFigure_glTF_RiggedFigure.gltf.png b/tests/screenshots/RiggedFigure_glTF_RiggedFigure.gltf.png index 1e3159b..fd69763 100644 Binary files a/tests/screenshots/RiggedFigure_glTF_RiggedFigure.gltf.png and b/tests/screenshots/RiggedFigure_glTF_RiggedFigure.gltf.png differ diff --git a/tests/screenshots/RiggedSimple_glTF-Binary_RiggedSimple.glb.png b/tests/screenshots/RiggedSimple_glTF-Binary_RiggedSimple.glb.png index 2d20864..db3e912 100644 Binary files a/tests/screenshots/RiggedSimple_glTF-Binary_RiggedSimple.glb.png and b/tests/screenshots/RiggedSimple_glTF-Binary_RiggedSimple.glb.png differ diff --git a/tests/screenshots/RiggedSimple_glTF-Embedded_RiggedSimple.gltf.png b/tests/screenshots/RiggedSimple_glTF-Embedded_RiggedSimple.gltf.png index 2d20864..db3e912 100644 Binary files a/tests/screenshots/RiggedSimple_glTF-Embedded_RiggedSimple.gltf.png and b/tests/screenshots/RiggedSimple_glTF-Embedded_RiggedSimple.gltf.png differ diff --git a/tests/screenshots/RiggedSimple_glTF_RiggedSimple.gltf.png b/tests/screenshots/RiggedSimple_glTF_RiggedSimple.gltf.png index 2d20864..db3e912 100644 Binary files a/tests/screenshots/RiggedSimple_glTF_RiggedSimple.gltf.png and b/tests/screenshots/RiggedSimple_glTF_RiggedSimple.gltf.png differ diff --git a/tests/screenshots/SciFiHelmet_glTF_SciFiHelmet.gltf.png b/tests/screenshots/SciFiHelmet_glTF_SciFiHelmet.gltf.png index 56dc2d2..8674af0 100644 Binary files a/tests/screenshots/SciFiHelmet_glTF_SciFiHelmet.gltf.png and b/tests/screenshots/SciFiHelmet_glTF_SciFiHelmet.gltf.png differ diff --git a/tests/screenshots/SheenChair_glTF-Binary_SheenChair.glb.png b/tests/screenshots/SheenChair_glTF-Binary_SheenChair.glb.png index 560f779..9cfba17 100644 Binary files a/tests/screenshots/SheenChair_glTF-Binary_SheenChair.glb.png and b/tests/screenshots/SheenChair_glTF-Binary_SheenChair.glb.png differ diff --git a/tests/screenshots/SheenChair_glTF_SheenChair.gltf.png b/tests/screenshots/SheenChair_glTF_SheenChair.gltf.png index 560f779..9cfba17 100644 Binary files a/tests/screenshots/SheenChair_glTF_SheenChair.gltf.png and b/tests/screenshots/SheenChair_glTF_SheenChair.gltf.png differ diff --git a/tests/screenshots/SheenCloth_glTF_SheenCloth.gltf.png b/tests/screenshots/SheenCloth_glTF_SheenCloth.gltf.png index 810f738..c2425a7 100644 Binary files a/tests/screenshots/SheenCloth_glTF_SheenCloth.gltf.png and b/tests/screenshots/SheenCloth_glTF_SheenCloth.gltf.png differ diff --git a/tests/screenshots/SimpleMaterial_glTF_SimpleMaterial.gltf.png b/tests/screenshots/SimpleMaterial_glTF_SimpleMaterial.gltf.png new file mode 100644 index 0000000..d434cdf Binary files /dev/null and b/tests/screenshots/SimpleMaterial_glTF_SimpleMaterial.gltf.png differ diff --git a/tests/screenshots/SimpleMorph_glTF-Embedded_SimpleMorph.gltf.png b/tests/screenshots/SimpleMorph_glTF-Embedded_SimpleMorph.gltf.png index 52dc376..ee30137 100644 Binary files a/tests/screenshots/SimpleMorph_glTF-Embedded_SimpleMorph.gltf.png and b/tests/screenshots/SimpleMorph_glTF-Embedded_SimpleMorph.gltf.png differ diff --git a/tests/screenshots/SimpleMorph_glTF_SimpleMorph.gltf.png b/tests/screenshots/SimpleMorph_glTF_SimpleMorph.gltf.png index 52dc376..ee30137 100644 Binary files a/tests/screenshots/SimpleMorph_glTF_SimpleMorph.gltf.png and b/tests/screenshots/SimpleMorph_glTF_SimpleMorph.gltf.png differ diff --git a/tests/screenshots/SimpleSkin_glTF-Embedded_SimpleSkin.gltf.png b/tests/screenshots/SimpleSkin_glTF-Embedded_SimpleSkin.gltf.png index bac4952..0b12836 100644 Binary files a/tests/screenshots/SimpleSkin_glTF-Embedded_SimpleSkin.gltf.png and b/tests/screenshots/SimpleSkin_glTF-Embedded_SimpleSkin.gltf.png differ diff --git a/tests/screenshots/SimpleSkin_glTF_SimpleSkin.gltf.png b/tests/screenshots/SimpleSkin_glTF_SimpleSkin.gltf.png index bac4952..0b12836 100644 Binary files a/tests/screenshots/SimpleSkin_glTF_SimpleSkin.gltf.png and b/tests/screenshots/SimpleSkin_glTF_SimpleSkin.gltf.png differ diff --git a/tests/screenshots/SimpleTexture_glTF-Embedded_SimpleTexture.gltf.png b/tests/screenshots/SimpleTexture_glTF-Embedded_SimpleTexture.gltf.png index 7874605..a2e3ec6 100644 Binary files a/tests/screenshots/SimpleTexture_glTF-Embedded_SimpleTexture.gltf.png and b/tests/screenshots/SimpleTexture_glTF-Embedded_SimpleTexture.gltf.png differ diff --git a/tests/screenshots/SimpleTexture_glTF_SimpleTexture.gltf.png b/tests/screenshots/SimpleTexture_glTF_SimpleTexture.gltf.png index 7874605..a2e3ec6 100644 Binary files a/tests/screenshots/SimpleTexture_glTF_SimpleTexture.gltf.png and b/tests/screenshots/SimpleTexture_glTF_SimpleTexture.gltf.png differ diff --git a/tests/screenshots/SpecularTest_glTF-Binary_SpecularTest.glb.png b/tests/screenshots/SpecularTest_glTF-Binary_SpecularTest.glb.png index 63a2265..d36b946 100644 Binary files a/tests/screenshots/SpecularTest_glTF-Binary_SpecularTest.glb.png and b/tests/screenshots/SpecularTest_glTF-Binary_SpecularTest.glb.png differ diff --git a/tests/screenshots/SpecularTest_glTF_SpecularTest.gltf.png b/tests/screenshots/SpecularTest_glTF_SpecularTest.gltf.png index 63a2265..d36b946 100644 Binary files a/tests/screenshots/SpecularTest_glTF_SpecularTest.gltf.png and b/tests/screenshots/SpecularTest_glTF_SpecularTest.gltf.png differ diff --git a/tests/screenshots/Sponza_glTF_Sponza.gltf.png b/tests/screenshots/Sponza_glTF_Sponza.gltf.png index c94be42..4d01e46 100644 Binary files a/tests/screenshots/Sponza_glTF_Sponza.gltf.png and b/tests/screenshots/Sponza_glTF_Sponza.gltf.png differ diff --git a/tests/screenshots/StainedGlassLamp_glTF-JPG-PNG_StainedGlassLamp.gltf.png b/tests/screenshots/StainedGlassLamp_glTF-JPG-PNG_StainedGlassLamp.gltf.png index 30135ed..283c37d 100644 Binary files a/tests/screenshots/StainedGlassLamp_glTF-JPG-PNG_StainedGlassLamp.gltf.png and b/tests/screenshots/StainedGlassLamp_glTF-JPG-PNG_StainedGlassLamp.gltf.png differ diff --git a/tests/screenshots/StainedGlassLamp_glTF_StainedGlassLamp.gltf.png b/tests/screenshots/StainedGlassLamp_glTF_StainedGlassLamp.gltf.png index 742e244..9f151c9 100644 Binary files a/tests/screenshots/StainedGlassLamp_glTF_StainedGlassLamp.gltf.png and b/tests/screenshots/StainedGlassLamp_glTF_StainedGlassLamp.gltf.png differ diff --git a/tests/screenshots/SunglassesKhronos_glTF-Binary_SunglassesKhronos.glb.png b/tests/screenshots/SunglassesKhronos_glTF-Binary_SunglassesKhronos.glb.png index 399e137..e69ec59 100644 Binary files a/tests/screenshots/SunglassesKhronos_glTF-Binary_SunglassesKhronos.glb.png and b/tests/screenshots/SunglassesKhronos_glTF-Binary_SunglassesKhronos.glb.png differ diff --git a/tests/screenshots/SunglassesKhronos_glTF_SunglassesKhronos.gltf.png b/tests/screenshots/SunglassesKhronos_glTF_SunglassesKhronos.gltf.png index 399e137..48c221b 100644 Binary files a/tests/screenshots/SunglassesKhronos_glTF_SunglassesKhronos.gltf.png and b/tests/screenshots/SunglassesKhronos_glTF_SunglassesKhronos.gltf.png differ diff --git a/tests/screenshots/TextureEncodingTest_glTF-Binary_TextureEncodingTest.glb.png b/tests/screenshots/TextureEncodingTest_glTF-Binary_TextureEncodingTest.glb.png index 03c28d6..48c086b 100644 Binary files a/tests/screenshots/TextureEncodingTest_glTF-Binary_TextureEncodingTest.glb.png and b/tests/screenshots/TextureEncodingTest_glTF-Binary_TextureEncodingTest.glb.png differ diff --git a/tests/screenshots/TextureEncodingTest_glTF_TextureEncodingTest.gltf.png b/tests/screenshots/TextureEncodingTest_glTF_TextureEncodingTest.gltf.png index 03c28d6..48c086b 100644 Binary files a/tests/screenshots/TextureEncodingTest_glTF_TextureEncodingTest.gltf.png and b/tests/screenshots/TextureEncodingTest_glTF_TextureEncodingTest.gltf.png differ diff --git a/tests/screenshots/TextureLinearInterpolationTest_glTF-Binary_TextureLinearInterpolationTest.glb.png b/tests/screenshots/TextureLinearInterpolationTest_glTF-Binary_TextureLinearInterpolationTest.glb.png index a71f259..921ab77 100644 Binary files a/tests/screenshots/TextureLinearInterpolationTest_glTF-Binary_TextureLinearInterpolationTest.glb.png and b/tests/screenshots/TextureLinearInterpolationTest_glTF-Binary_TextureLinearInterpolationTest.glb.png differ diff --git a/tests/screenshots/TextureLinearInterpolationTest_glTF_TextureLinearInterpolationTest.gltf.png b/tests/screenshots/TextureLinearInterpolationTest_glTF_TextureLinearInterpolationTest.gltf.png index a71f259..921ab77 100644 Binary files a/tests/screenshots/TextureLinearInterpolationTest_glTF_TextureLinearInterpolationTest.gltf.png and b/tests/screenshots/TextureLinearInterpolationTest_glTF_TextureLinearInterpolationTest.gltf.png differ diff --git a/tests/screenshots/TextureTransformMultiTest_glTF-Binary_TextureTransformMultiTest.glb.png b/tests/screenshots/TextureTransformMultiTest_glTF-Binary_TextureTransformMultiTest.glb.png index 1b4d1c8..922c79c 100644 Binary files a/tests/screenshots/TextureTransformMultiTest_glTF-Binary_TextureTransformMultiTest.glb.png and b/tests/screenshots/TextureTransformMultiTest_glTF-Binary_TextureTransformMultiTest.glb.png differ diff --git a/tests/screenshots/TextureTransformMultiTest_glTF_TextureTransformMultiTest.gltf.png b/tests/screenshots/TextureTransformMultiTest_glTF_TextureTransformMultiTest.gltf.png index 1b4d1c8..922c79c 100644 Binary files a/tests/screenshots/TextureTransformMultiTest_glTF_TextureTransformMultiTest.gltf.png and b/tests/screenshots/TextureTransformMultiTest_glTF_TextureTransformMultiTest.gltf.png differ diff --git a/tests/screenshots/TextureTransformTest_glTF_TextureTransformTest.gltf.png b/tests/screenshots/TextureTransformTest_glTF_TextureTransformTest.gltf.png index a6ec0ce..be2fcd7 100644 Binary files a/tests/screenshots/TextureTransformTest_glTF_TextureTransformTest.gltf.png and b/tests/screenshots/TextureTransformTest_glTF_TextureTransformTest.gltf.png differ diff --git a/tests/screenshots/ToyCar_glTF-Binary_ToyCar.glb.png b/tests/screenshots/ToyCar_glTF-Binary_ToyCar.glb.png index fbca4f4..cecb041 100644 Binary files a/tests/screenshots/ToyCar_glTF-Binary_ToyCar.glb.png and b/tests/screenshots/ToyCar_glTF-Binary_ToyCar.glb.png differ diff --git a/tests/screenshots/ToyCar_glTF_ToyCar.gltf.png b/tests/screenshots/ToyCar_glTF_ToyCar.gltf.png index fbca4f4..cecb041 100644 Binary files a/tests/screenshots/ToyCar_glTF_ToyCar.gltf.png and b/tests/screenshots/ToyCar_glTF_ToyCar.gltf.png differ diff --git a/tests/screenshots/TransmissionOrderTest_glTF-Binary_TransmissionOrderTest.glb.png b/tests/screenshots/TransmissionOrderTest_glTF-Binary_TransmissionOrderTest.glb.png index 9851b95..27cada1 100644 Binary files a/tests/screenshots/TransmissionOrderTest_glTF-Binary_TransmissionOrderTest.glb.png and b/tests/screenshots/TransmissionOrderTest_glTF-Binary_TransmissionOrderTest.glb.png differ diff --git a/tests/screenshots/TransmissionOrderTest_glTF_TransmissionOrderTest.gltf.png b/tests/screenshots/TransmissionOrderTest_glTF_TransmissionOrderTest.gltf.png index 9851b95..27cada1 100644 Binary files a/tests/screenshots/TransmissionOrderTest_glTF_TransmissionOrderTest.gltf.png and b/tests/screenshots/TransmissionOrderTest_glTF_TransmissionOrderTest.gltf.png differ diff --git a/tests/screenshots/TransmissionRoughnessTest_glTF-Binary_TransmissionRoughnessTest.glb.png b/tests/screenshots/TransmissionRoughnessTest_glTF-Binary_TransmissionRoughnessTest.glb.png index 065c095..f640147 100644 Binary files a/tests/screenshots/TransmissionRoughnessTest_glTF-Binary_TransmissionRoughnessTest.glb.png and b/tests/screenshots/TransmissionRoughnessTest_glTF-Binary_TransmissionRoughnessTest.glb.png differ diff --git a/tests/screenshots/TransmissionRoughnessTest_glTF_TransmissionRoughnessTest.gltf.png b/tests/screenshots/TransmissionRoughnessTest_glTF_TransmissionRoughnessTest.gltf.png index 065c095..f640147 100644 Binary files a/tests/screenshots/TransmissionRoughnessTest_glTF_TransmissionRoughnessTest.gltf.png and b/tests/screenshots/TransmissionRoughnessTest_glTF_TransmissionRoughnessTest.gltf.png differ diff --git a/tests/screenshots/TransmissionTest_glTF-Binary_TransmissionTest.glb.png b/tests/screenshots/TransmissionTest_glTF-Binary_TransmissionTest.glb.png index 3a4123f..78994ed 100644 Binary files a/tests/screenshots/TransmissionTest_glTF-Binary_TransmissionTest.glb.png and b/tests/screenshots/TransmissionTest_glTF-Binary_TransmissionTest.glb.png differ diff --git a/tests/screenshots/TransmissionTest_glTF_TransmissionTest.gltf.png b/tests/screenshots/TransmissionTest_glTF_TransmissionTest.gltf.png index 3a4123f..78994ed 100644 Binary files a/tests/screenshots/TransmissionTest_glTF_TransmissionTest.gltf.png and b/tests/screenshots/TransmissionTest_glTF_TransmissionTest.gltf.png differ diff --git a/tests/screenshots/TransmissionThinwallTestGrid_glTF-Binary_TransmissionThinwallTestGrid.glb.png b/tests/screenshots/TransmissionThinwallTestGrid_glTF-Binary_TransmissionThinwallTestGrid.glb.png index be57a9b..0e05f84 100644 Binary files a/tests/screenshots/TransmissionThinwallTestGrid_glTF-Binary_TransmissionThinwallTestGrid.glb.png and b/tests/screenshots/TransmissionThinwallTestGrid_glTF-Binary_TransmissionThinwallTestGrid.glb.png differ diff --git a/tests/screenshots/TransmissionThinwallTestGrid_glTF_TransmissionThinwallTestGrid.gltf.png b/tests/screenshots/TransmissionThinwallTestGrid_glTF_TransmissionThinwallTestGrid.gltf.png index be57a9b..0e05f84 100644 Binary files a/tests/screenshots/TransmissionThinwallTestGrid_glTF_TransmissionThinwallTestGrid.gltf.png and b/tests/screenshots/TransmissionThinwallTestGrid_glTF_TransmissionThinwallTestGrid.gltf.png differ diff --git a/tests/screenshots/WaterBottle_glTF-Binary_WaterBottle.glb.png b/tests/screenshots/WaterBottle_glTF-Binary_WaterBottle.glb.png index b7d75ff..0400e21 100644 Binary files a/tests/screenshots/WaterBottle_glTF-Binary_WaterBottle.glb.png and b/tests/screenshots/WaterBottle_glTF-Binary_WaterBottle.glb.png differ diff --git a/tests/screenshots/WaterBottle_glTF_WaterBottle.gltf.png b/tests/screenshots/WaterBottle_glTF_WaterBottle.gltf.png index b7d75ff..0400e21 100644 Binary files a/tests/screenshots/WaterBottle_glTF_WaterBottle.gltf.png and b/tests/screenshots/WaterBottle_glTF_WaterBottle.gltf.png differ diff --git a/tests/tests.nim b/tests/tests.nim index f8b103d..a3bfce9 100644 --- a/tests/tests.nim +++ b/tests/tests.nim @@ -245,8 +245,8 @@ doAssert perspectiveNode != nil doAssert orthoNode != nil doAssert perspectiveNode.camera != nil doAssert orthoNode.camera != nil -doAssert perspectiveNode.camera.kind == ckPerspective -doAssert orthoNode.camera.kind == ckOrthographic +doAssert perspectiveNode.camera.kind == PerspectiveLens +doAssert orthoNode.camera.kind == OrthographicLens doAssert abs(perspectiveNode.camera.perspective.yfov - 0.7853982) < 0.0001 doAssert abs(orthoNode.camera.orthographic.xmag - 2.0) < 0.0001 @@ -703,8 +703,6 @@ doAssert basisNode.mesh != nil doAssert basisNode.mesh.primitives.len == 1 let basisMaterial = basisNode.mesh.primitives[0].material doAssert basisMaterial != nil -doAssert basisMaterial.baseColorId != 0 doAssert basisMaterial.baseColorName == "basisColor" doAssert basisMaterial.baseColor == nil -glDeleteTextures(1, basisMaterial.baseColorId.addr) -basisMaterial.baseColorId = 0 +doAssert basisMaterial.baseColorKtx2.len > 0 diff --git a/tools/gltf_viewer.nim b/tools/gltf_viewer.nim index 5ef60e2..2d65d2b 100644 --- a/tools/gltf_viewer.nim +++ b/tools/gltf_viewer.nim @@ -1,24 +1,34 @@ import - std/[algorithm, math, os, strformat, strutils, times], - opengl, windy, chroma, silky, silky/atlas, jsony, pixie, - pixie/fileformats/png, vmath, + std/[math, os, times], + windy, chroma, vmath, ../src/gltf/[animations, models, pbr, reader] +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + import opengl + +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + import + std/[algorithm, strformat, strutils], + silky, silky/atlas, jsony, pixie, + pixie/fileformats/png + const - AtlasPng = staticRead("dist/atlas.png") VerticalFov = 45'f32 FitPadding = 1.25'f32 FreeCameraName = "Free Camera" -let - debugViewOptions = @[ - "Lit", - "Unlit", - "Normals", - "AO Bake", - "Metallic", - "Specular" - ] +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + const AtlasPng = staticRead("dist/atlas.png") + + let + debugViewOptions = @[ + "Lit", + "Unlit", + "Normals", + "AO Bake", + "Metallic", + "Specular" + ] let params = commandLineParams() if params.len == 0 or not fileExists(params[0]): @@ -29,76 +39,95 @@ var window = newWindow( ivec2(1200, 700), msaa = msaa8x ) -makeContextCurrent(window) -loadExtensions() +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + makeContextCurrent(window) + loadExtensions() +elif defined(useDirectX) or defined(useVulkan): + loadExtensions() + +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + let + atlasImage = decodePng(AtlasPng).convertToImage() + atlasData = extractAtlasJsonFromPng(AtlasPng).fromJson(SilkyAtlas) + +var renderer = newRenderer(window) -let - atlasImage = decodePng(AtlasPng).convertToImage() - atlasData = extractAtlasJsonFromPng(AtlasPng).fromJson(SilkyAtlas) +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + var sk = newSilky(window, atlasImage, atlasData) var - sk = newSilky(window, atlasImage, atlasData) gltfFile: GltfFile model: Node modelBounds: Bounds loaded = false showControls = true useLighting = true - useShadows = true lightFollowCamera = true - debugViewName = "Lit" cameraOptions = @[FreeCameraName] cameraNodes: seq[Node] selectedCamera = FreeCameraName - skyboxOptions: seq[string] - skyboxPatterns: seq[(string, string)] - selectedSkybox = "" - lastSkybox = "" - skyboxLod: float32 = 7.0 modelPath = params[0] camCenter = vec3(0, 0, 0) camRotation = mat4() camDolly = 10'f32 backgroundColor = color(0.03, 0.035, 0.05, 1.0) - lastBackgroundColor = color(-1.0, -1.0, -1.0, 1.0) ambientLightColor = color(0.32, 0.36, 0.46, 0.18) sunLightDirection = normalize(vec3(1, 4, 2)) sunLightColor = color(0.95, 0.96, 1.0, 1.0) rimLightDirection = normalize(vec3(-1.0, 1.0, -2.0)) rimLightColor = color(0.95, 0.72, 0.46, 0.25) aspectRatio = window.size.x.float32 / window.size.y.float32 - proj = perspective(VerticalFov, aspectRatio, 0.1, 200) + proj = + when defined(useDirectX): + perspectiveDxRh(VerticalFov, aspectRatio, 0.1, 200) + elif defined(useVulkan): + perspectiveVkRh(VerticalFov, aspectRatio, 0.1, 200) + else: + perspective(VerticalFov, aspectRatio, 0.1, 200) lastTime = epochTime() -window.runeInputEnabled = true -window.onRune = proc(rune: Rune) = - sk.inputRunes.add(rune) - -proc applyTheme(sk: Silky) = - ## Applies the viewer theme colors and spacing. - sk.theme.padding = 10 - sk.theme.spacing = 8 - sk.theme.border = 10 - sk.theme.textPadding = 5 - sk.theme.headerHeight = 30 - sk.theme.defaultTextColor = rgbx(232, 240, 255, 255) - sk.theme.disabledTextColor = rgbx(136, 146, 168, 255) - sk.theme.errorTextColor = rgbx(255, 156, 172, 255) - sk.theme.buttonHoverColor = rgbx(255, 255, 255, 48) - sk.theme.buttonDownColor = rgbx(190, 216, 255, 76) - sk.theme.iconButtonHoverColor = rgbx(255, 255, 255, 40) - sk.theme.iconButtonDownColor = rgbx(190, 216, 255, 64) - sk.theme.dropdownHoverBgColor = rgbx(60, 74, 100, 255) - sk.theme.dropdownBgColor = rgbx(34, 42, 58, 255) - sk.theme.dropdownPopupBgColor = rgbx(28, 36, 50, 245) - sk.theme.textColor = rgbx(224, 234, 255, 255) - sk.theme.textH1Color = rgbx(250, 252, 255, 255) - sk.theme.headerBgColor = rgbx(38, 46, 62, 255) - sk.theme.menuRootHoverColor = rgbx(86, 102, 134, 160) - sk.theme.menuItemHoverColor = rgbx(74, 90, 120, 160) - sk.theme.menuItemBgColor = rgbx(40, 48, 66, 140) - sk.theme.menuPopupHoverColor = rgbx(84, 102, 134, 180) - sk.theme.menuPopupSelectedColor = rgbx(68, 84, 112, 140) +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + var + skyboxOptions: seq[string] + skyboxPatterns: seq[(string, string)] + selectedSkybox = "" + lastSkybox = "" + lastBackgroundColor = color(-1.0, -1.0, -1.0, 1.0) + useShadows = true + debugViewName = "Lit" + skyboxLod: float32 = 7.0 + +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + window.runeInputEnabled = true + window.onRune = proc(rune: Rune) = + sk.inputRunes.add(rune) + +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc applyTheme(sk: Silky) = + ## Applies the viewer theme colors and spacing. + sk.theme.padding = 10 + sk.theme.spacing = 8 + sk.theme.border = 10 + sk.theme.textPadding = 5 + sk.theme.headerHeight = 30 + sk.theme.defaultTextColor = rgbx(232, 240, 255, 255) + sk.theme.disabledTextColor = rgbx(136, 146, 168, 255) + sk.theme.errorTextColor = rgbx(255, 156, 172, 255) + sk.theme.buttonHoverColor = rgbx(255, 255, 255, 48) + sk.theme.buttonDownColor = rgbx(190, 216, 255, 76) + sk.theme.iconButtonHoverColor = rgbx(255, 255, 255, 40) + sk.theme.iconButtonDownColor = rgbx(190, 216, 255, 64) + sk.theme.dropdownHoverBgColor = rgbx(60, 74, 100, 255) + sk.theme.dropdownBgColor = rgbx(34, 42, 58, 255) + sk.theme.dropdownPopupBgColor = rgbx(28, 36, 50, 245) + sk.theme.textColor = rgbx(224, 234, 255, 255) + sk.theme.textH1Color = rgbx(250, 252, 255, 255) + sk.theme.headerBgColor = rgbx(38, 46, 62, 255) + sk.theme.menuRootHoverColor = rgbx(86, 102, 134, 160) + sk.theme.menuItemHoverColor = rgbx(74, 90, 120, 160) + sk.theme.menuItemBgColor = rgbx(40, 48, 66, 140) + sk.theme.menuPopupHoverColor = rgbx(84, 102, 134, 180) + sk.theme.menuPopupSelectedColor = rgbx(68, 84, 112, 140) proc safeNormalize(v, fallback: Vec3): Vec3 = ## Normalizes a vector and falls back when its length is too small. @@ -106,58 +135,60 @@ proc safeNormalize(v, fallback: Vec3): Vec3 = return fallback normalize(v) -proc drawColorControls(id, title: string, value: var Color) = - ## Draws four scrubbers for one RGBA color. - text(title) - scrubber(id & "_r", value.r, 0.0'f32, 1.0'f32, "R") - scrubber(id & "_g", value.g, 0.0'f32, 1.0'f32, "G") - scrubber(id & "_b", value.b, 0.0'f32, 1.0'f32, "B") - scrubber(id & "_a", value.a, 0.0'f32, 10.0'f32, "Strength") - -proc drawRgbControls(id, title: string, value: var Color) = - ## Draws rgb-only controls without a strength slider. - text(title) - scrubber(id & "_r", value.r, 0.0'f32, 1.0'f32, "R") - scrubber(id & "_g", value.g, 0.0'f32, 1.0'f32, "G") - scrubber(id & "_b", value.b, 0.0'f32, 1.0'f32, "B") - value.a = 1.0 - -proc drawDirectionControls(id, title: string, value: var Vec3) = - ## Draws three scrubbers for a direction vector. - text(title) - scrubber(id & "_x", value.x, -1.0'f32, 1.0'f32, "X") - scrubber(id & "_y", value.y, -1.0'f32, 1.0'f32, "Y") - scrubber(id & "_z", value.z, -1.0'f32, 1.0'f32, "Z") - -applyTheme(sk) - -proc skyboxPattern(path: string): string = - ## Converts one cubemap face path into a wildcard pattern. - for face in ["px", "nx", "py", "ny", "pz", "nz"]: - let token = "." & face & "." - if token in path: - return path.replace(token, ".*.") - path - -proc skyboxName(pattern: string): string = - ## Extracts a readable skybox name from a wildcard pattern. - pattern.splitFile.name.replace(".*", "") - -proc selectedSkyboxPattern(): string = - ## Looks up the selected skybox pattern by name. - for (name, pattern) in skyboxPatterns: - if name == selectedSkybox: - return pattern - "" - -proc defaultSkybox(): string = - ## Picks the first real skybox and falls back to solid color. - for name in skyboxOptions: - if name != "Solid Color": - return name - if skyboxOptions.len > 0: - return skyboxOptions[0] - "Solid Color" +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc drawColorControls(id, title: string, value: var Color) = + ## Draws four scrubbers for one RGBA color. + text(title) + scrubber(id & "_r", value.r, 0.0'f32, 1.0'f32, "R") + scrubber(id & "_g", value.g, 0.0'f32, 1.0'f32, "G") + scrubber(id & "_b", value.b, 0.0'f32, 1.0'f32, "B") + scrubber(id & "_a", value.a, 0.0'f32, 10.0'f32, "Strength") + + proc drawRgbControls(id, title: string, value: var Color) = + ## Draws rgb-only controls without a strength slider. + text(title) + scrubber(id & "_r", value.r, 0.0'f32, 1.0'f32, "R") + scrubber(id & "_g", value.g, 0.0'f32, 1.0'f32, "G") + scrubber(id & "_b", value.b, 0.0'f32, 1.0'f32, "B") + value.a = 1.0 + + proc drawDirectionControls(id, title: string, value: var Vec3) = + ## Draws three scrubbers for a direction vector. + text(title) + scrubber(id & "_x", value.x, -1.0'f32, 1.0'f32, "X") + scrubber(id & "_y", value.y, -1.0'f32, 1.0'f32, "Y") + scrubber(id & "_z", value.z, -1.0'f32, 1.0'f32, "Z") + + applyTheme(sk) + +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc skyboxPattern(path: string): string = + ## Converts one cubemap face path into a wildcard pattern. + for face in ["px", "nx", "py", "ny", "pz", "nz"]: + let token = "." & face & "." + if token in path: + return path.replace(token, ".*.") + path + + proc skyboxName(pattern: string): string = + ## Extracts a readable skybox name from a wildcard pattern. + pattern.splitFile.name.replace(".*", "") + + proc selectedSkyboxPattern(): string = + ## Looks up the selected skybox pattern by name. + for (name, pattern) in skyboxPatterns: + if name == selectedSkybox: + return pattern + "" + + proc defaultSkybox(): string = + ## Picks the first real skybox and falls back to solid color. + for name in skyboxOptions: + if name != "Solid Color": + return name + if skyboxOptions.len > 0: + return skyboxOptions[0] + "Solid Color" proc defaultCamera(): string = ## Picks the free camera by default. @@ -202,7 +233,7 @@ proc discoverCameras() = proc cameraProjection(camera: Camera, aspectRatio: float32): Mat4 = ## Builds a projection matrix for a glTF camera. case camera.kind - of ckPerspective: + of PerspectiveLens: let yfov = camera.perspective.yfov * 180.0'f32 / PI.float32 @@ -217,8 +248,13 @@ proc cameraProjection(camera: Camera, aspectRatio: float32): Mat4 = camera.perspective.zfar else: 2000.0'f32 - perspective(yfov, camAspect, znear, zfar) - of ckOrthographic: + when defined(useDirectX): + perspectiveDxRh(yfov, camAspect, znear, zfar) + elif defined(useVulkan): + perspectiveVkRh(yfov, camAspect, znear, zfar) + else: + perspective(yfov, camAspect, znear, zfar) + of OrthographicLens: ortho( -camera.orthographic.xmag, camera.orthographic.xmag, @@ -228,57 +264,58 @@ proc cameraProjection(camera: Camera, aspectRatio: float32): Mat4 = camera.orthographic.zfar ) -proc discoverSkyboxes() = - ## Scans the skybox folder for cubemap patterns. - skyboxOptions = @[] - skyboxPatterns.setLen(0) - if not dirExists("tools/skybox"): - skyboxOptions = @["Solid Color"] - return - - for path in walkDirRec("tools/skybox"): - let lower = path.toLowerAscii() - if not (lower.endsWith(".png") or - lower.endsWith(".jpg") or - lower.endsWith(".jpeg")): - continue - let pattern = skyboxPattern(path.replace("\\", "/")) - if pattern == path: - continue - let name = skyboxName(pattern) - var known = false - for (_, knownPattern) in skyboxPatterns: - if knownPattern == pattern: - known = true - break - if not known: - skyboxPatterns.add((name, pattern)) - skyboxOptions.add(name) - - skyboxOptions.sort() - skyboxOptions.add("Solid Color") - -proc updateEnvironmentMap(force = false) = - ## Reloads the environment map when the selection changes. - if selectedSkybox == "Solid Color": - if force or lastSkybox != selectedSkybox: - loadDefaultEnvironmentMap() - if force or - backgroundColor.r != lastBackgroundColor.r or - backgroundColor.g != lastBackgroundColor.g or - backgroundColor.b != lastBackgroundColor.b: - lastBackgroundColor = backgroundColor - lastSkybox = selectedSkybox - return +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc discoverSkyboxes() = + ## Scans the skybox folder for cubemap patterns. + skyboxOptions = @[] + skyboxPatterns.setLen(0) + if not dirExists("tools/skybox"): + skyboxOptions = @["Solid Color"] + return - if force or lastSkybox != selectedSkybox: - let pattern = selectedSkyboxPattern() - if pattern.len == 0: - selectedSkybox = "Solid Color" - updateEnvironmentMap(force = true) + for path in walkDirRec("tools/skybox"): + let lower = path.toLowerAscii() + if not (lower.endsWith(".png") or + lower.endsWith(".jpg") or + lower.endsWith(".jpeg")): + continue + let pattern = skyboxPattern(path.replace("\\", "/")) + if pattern == path: + continue + let name = skyboxName(pattern) + var known = false + for (_, knownPattern) in skyboxPatterns: + if knownPattern == pattern: + known = true + break + if not known: + skyboxPatterns.add((name, pattern)) + skyboxOptions.add(name) + + skyboxOptions.sort() + skyboxOptions.add("Solid Color") + + proc updateEnvironmentMap(force = false) = + ## Reloads the environment map when the selection changes. + if selectedSkybox == "Solid Color": + if force or lastSkybox != selectedSkybox: + loadDefaultEnvironmentMap() + if force or + backgroundColor.r != lastBackgroundColor.r or + backgroundColor.g != lastBackgroundColor.g or + backgroundColor.b != lastBackgroundColor.b: + lastBackgroundColor = backgroundColor + lastSkybox = selectedSkybox return - loadEnvironmentMap(pattern) - lastSkybox = selectedSkybox + + if force or lastSkybox != selectedSkybox: + let pattern = selectedSkyboxPattern() + if pattern.len == 0: + selectedSkybox = "Solid Color" + updateEnvironmentMap(force = true) + return + loadEnvironmentMap(pattern) + lastSkybox = selectedSkybox proc hasModel(node: Node): bool = ## Returns true when the node tree has geometry to draw. @@ -306,7 +343,7 @@ proc fitCameraDolly(bounds: Bounds, aspectRatio: float32): float32 = proc reloadFile(): bool = ## Reloads the current glTF file. if model != nil: - model.clearFromGpu() + renderer.release(model) if modelPath.len == 0: model = Node() @@ -346,11 +383,11 @@ proc reloadFile(): bool = proc loadAssets() = ## Loads the renderer assets and the requested model. - setupPbr() - discoverSkyboxes() - if selectedSkybox notin skyboxOptions: - selectedSkybox = defaultSkybox() - updateEnvironmentMap(force = true) + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + discoverSkyboxes() + if selectedSkybox notin skyboxOptions: + selectedSkybox = defaultSkybox() + updateEnvironmentMap(force = true) discard reloadFile() proc withStrengthDisabled(value: Color, enabled: bool): Color = @@ -359,103 +396,105 @@ proc withStrengthDisabled(value: Color, enabled: bool): Color = return value color(value.r, value.g, value.b, 0.0) -proc parseDebugView(name: string): DebugView = - ## Maps the UI label to the renderer debug mode. - case name - of "Unlit": - dvUnlit - of "Normals": - dvNormals - of "AO Bake": - dvAoBake - of "Metallic": - dvMetallic - of "Specular": - dvSpecular - else: - dvLit - -proc drawOverlay( - cameraPosition, - effectiveSunLightDirection, - effectiveRimLightDirection: Vec3 -) = - ## Draws the themed viewer controls window. - sk.beginUI(window, window.size) - subWindow("glTF Viewer", showControls, vec2(20, 20), vec2(430, 760)): - h1text("glTF Viewer") - text("Mouse middle or Cmd+left drag: orbit") - text("Scroll: dolly") - text("Key 1 toggles this window") - text("") - - if gltfFile != nil: - text("Current file:") - text(gltfFile.path) +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc parseDebugView(name: string): DebugView = + ## Maps the UI label to the renderer debug mode. + case name + of "Unlit": + dvUnlit + of "Normals": + dvNormals + of "AO Bake": + dvAoBake + of "Metallic": + dvMetallic + of "Specular": + dvSpecular else: - text("No glTF file loaded.") - - group(vec2(0, 0), LeftToRight): - button("Reload File"): - discard reloadFile() - button("Refit View"): - camCenter = modelBounds.center - camDolly = fitCameraDolly(modelBounds, aspectRatio) - - text("") - text("Scene") - checkBox("Lighting", useLighting) - checkBox("Shadows", useShadows) - checkBox("Light follow camera", lightFollowCamera) - text("Camera") - dropDown(selectedCamera, cameraOptions) - text("Debug View") - dropDown(debugViewName, debugViewOptions) - text("Skybox") - dropDown(selectedSkybox, skyboxOptions) - if selectedSkybox == "Solid Color": - drawRgbControls("background", "Background", backgroundColor) - else: - scrubber("skybox_lod", skyboxLod, 0.0'f32, 10.0'f32, "Skybox Blur") - - text("") - text("Ambient Light") - drawColorControls( - "ambient_light", - "Ambient Light Color", - ambientLightColor - ) + dvLit - text("") - text("Sun Light") - if lightFollowCamera: - text(&"Following camera dir: {effectiveSunLightDirection}") - else: +when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + proc drawOverlay( + cameraPosition, + effectiveSunLightDirection, + effectiveRimLightDirection: Vec3 + ) = + ## Draws the themed viewer controls window. + sk.beginUI(window, window.size) + subWindow("glTF Viewer", showControls, vec2(20, 20), vec2(430, 760)): + h1text("glTF Viewer") + text("Mouse middle or Cmd+left drag: orbit") + text("Scroll: dolly") + text("Key 1 toggles this window") + text("") + + if gltfFile != nil: + text("Current file:") + text(gltfFile.path) + else: + text("No glTF file loaded.") + + group(vec2(0, 0), LeftToRight): + button("Reload File"): + discard reloadFile() + button("Refit View"): + camCenter = modelBounds.center + camDolly = fitCameraDolly(modelBounds, aspectRatio) + + text("") + text("Scene") + checkBox("Lighting", useLighting) + checkBox("Shadows", useShadows) + checkBox("Light follow camera", lightFollowCamera) + text("Camera") + dropDown(selectedCamera, cameraOptions) + text("Debug View") + dropDown(debugViewName, debugViewOptions) + text("Skybox") + dropDown(selectedSkybox, skyboxOptions) + if selectedSkybox == "Solid Color": + drawRgbControls("background", "Background", backgroundColor) + else: + scrubber("skybox_lod", skyboxLod, 0.0'f32, 10.0'f32, "Skybox Blur") + + text("") + text("Ambient Light") + drawColorControls( + "ambient_light", + "Ambient Light Color", + ambientLightColor + ) + + text("") + text("Sun Light") + if lightFollowCamera: + text(&"Following camera dir: {effectiveSunLightDirection}") + else: + drawDirectionControls( + "sun_light_dir", + "Sun Light Direction", + sunLightDirection + ) + drawColorControls("sun_light", "Sun Light Color", sunLightColor) + + text("") + text("Rim Light") + text(&"Current rim dir: {effectiveRimLightDirection}") drawDirectionControls( - "sun_light_dir", - "Sun Light Direction", - sunLightDirection + "rim_light_dir", + "Rim Light Direction", + rimLightDirection ) - drawColorControls("sun_light", "Sun Light Color", sunLightColor) - - text("") - text("Rim Light") - text(&"Current rim dir: {effectiveRimLightDirection}") - drawDirectionControls( - "rim_light_dir", - "Rim Light Direction", - rimLightDirection - ) - drawColorControls("rim_light", "Rim Light Color", rimLightColor) + drawColorControls("rim_light", "Rim Light Color", rimLightColor) - text("") - text("View") - text(&"Active camera: {selectedCamera}") - text(&"Camera dolly: {camDolly:>7.2f}") - text(&"Camera center: {camCenter}") - text(&"Bounds radius: {modelBounds.radius:>7.4f}") - text(&"Camera position: {cameraPosition}") - sk.endUi() + text("") + text("View") + text(&"Active camera: {selectedCamera}") + text(&"Camera dolly: {camDolly:>7.2f}") + text(&"Camera center: {camCenter}") + text(&"Bounds radius: {modelBounds.radius:>7.4f}") + text(&"Camera position: {cameraPosition}") + sk.endUi() window.onFrame = proc() = let nowTime = epochTime() @@ -494,7 +533,8 @@ window.onFrame = proc() = lightFollowCamera = not lightFollowCamera aspectRatio = window.size.x.float32 / window.size.y.float32 - updateEnvironmentMap() + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + updateEnvironmentMap() if model != nil: model.updateAnimation(dt) @@ -503,7 +543,13 @@ window.onFrame = proc() = cameraMat: Mat4 cameraPosition: Vec3 if useFreeCamera: - proj = perspective(VerticalFov, aspectRatio, 0.001, 2000) + proj = + when defined(useDirectX): + perspectiveDxRh(VerticalFov, aspectRatio, 0.001, 2000) + elif defined(useVulkan): + perspectiveVkRh(VerticalFov, aspectRatio, 0.001, 2000) + else: + perspective(VerticalFov, aspectRatio, 0.001, 2000) cameraMat = translate(vec3(0, 0, -camDolly)) * camRotation * @@ -516,7 +562,13 @@ window.onFrame = proc() = cameraPosition = vec3(world.pos) proj = cameraProjection(activeCameraNode.camera, aspectRatio) else: - proj = perspective(VerticalFov, aspectRatio, 0.001, 2000) + proj = + when defined(useDirectX): + perspectiveDxRh(VerticalFov, aspectRatio, 0.001, 2000) + elif defined(useVulkan): + perspectiveVkRh(VerticalFov, aspectRatio, 0.001, 2000) + else: + perspective(VerticalFov, aspectRatio, 0.001, 2000) cameraMat = translate(vec3(0, 0, -camDolly)) * camRotation * @@ -532,78 +584,83 @@ window.onFrame = proc() = safeNormalize(sunLightDirection, vec3(1, 1, 1)) effectiveRimLightDirection = safeNormalize(rimLightDirection, vec3(-1, 1, -1)) + + let effectiveAmbientLightColor = withStrengthDisabled(ambientLightColor, useLighting) effectiveSunLightColor = withStrengthDisabled(sunLightColor, useLighting) effectiveRimLightColor = withStrengthDisabled(rimLightColor, useLighting) - selectedDebugView = parseDebugView(debugViewName) effectiveDebugView = - if useLighting: - selectedDebugView + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + let selectedDebugView = parseDebugView(debugViewName) + if useLighting: + selectedDebugView + else: + dvUnlit else: - dvUnlit - - glClearColor( - backgroundColor.r, - backgroundColor.g, - backgroundColor.b, - 1.0 - ) - glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT) - glEnable(GL_MULTISAMPLE) - glCullFace(GL_BACK) - glFrontFace(GL_CCW) - glEnable(GL_CULL_FACE) - glEnable(GL_DEPTH_TEST) - glDepthMask(GL_TRUE) - - if selectedSkybox != "Solid Color": - drawSkybox(cameraMat, proj, skyboxLod) + if useLighting: + dvLit + else: + dvUnlit + renderParams = RenderParams( + size: window.size, + clearColor: backgroundColor, + transform: mat4(), + view: cameraMat, + proj: proj, + tint: color(1, 1, 1, 1), + useTrs: true, + ambientLightColor: effectiveAmbientLightColor, + sunLightDirection: effectiveSunLightDirection, + sunLightColor: effectiveSunLightColor, + rimLightDirection: effectiveRimLightDirection, + rimLightColor: effectiveRimLightColor, + debugView: effectiveDebugView, + cameraPosition: cameraPosition, + useShadows: + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + useShadows + else: + false, + drawSkybox: + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + selectedSkybox != "Solid Color" + else: + false, + skyboxLod: + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + skyboxLod + else: + 0.0'f32, + vsync: + when compiles(window.vsync): + window.vsync + else: + false + ) + renderer.beginFrame(window, window.size) + renderer.clearScreen(backgroundColor) if model.hasModel(): - if useShadows and effectiveDebugView == dvLit: - model.drawPbrWithShadow( - mat4(), - cameraMat, - proj, - tint = color(1, 1, 1, 1), - sunLightDirection = effectiveSunLightDirection, - useTrs = true, - ambientLightColor = effectiveAmbientLightColor, - sunLightColor = effectiveSunLightColor, - rimLightDirection = effectiveRimLightDirection, - rimLightColor = effectiveRimLightColor, - debugView = effectiveDebugView, - cameraPosition = cameraPosition - ) - else: - model.drawPbr( - mat4(), - cameraMat, - proj, - tint = color(1, 1, 1, 1), - useTrs = true, - ambientLightColor = effectiveAmbientLightColor, - sunLightDirection = effectiveSunLightDirection, - sunLightColor = effectiveSunLightColor, - rimLightDirection = effectiveRimLightDirection, - rimLightColor = effectiveRimLightColor, - debugView = effectiveDebugView, - cameraPosition = cameraPosition - ) - - glDisable(GL_DEPTH_TEST) - glDisable(GL_CULL_FACE) - glDisable(GL_BLEND) - glDisable(GL_MULTISAMPLE) - drawOverlay( - cameraPosition, - effectiveSunLightDirection, - effectiveRimLightDirection - ) - window.swapBuffers() + renderer.render(model, renderParams) + renderer.endFrame() + + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + glDisable(GL_DEPTH_TEST) + glDisable(GL_CULL_FACE) + glDisable(GL_BLEND) + glDisable(GL_MULTISAMPLE) + when not defined(useDirectX) and not defined(useVulkan) and not defined(useMetal4): + drawOverlay( + cameraPosition, + effectiveSunLightDirection, + effectiveRimLightDirection + ) + window.swapBuffers() while not window.closeRequested: pollEvents() + +renderer.shutdown()