From 29b56d38e9ac543b95400c932c96faf131853e23 Mon Sep 17 00:00:00 2001 From: Momchil Atanasov Date: Sun, 29 Sep 2024 18:28:16 +0300 Subject: [PATCH 1/4] Fix loading of tangents from gltf --- game/graphics/shader.go | 4 ++++ util/gltfutil/util.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/game/graphics/shader.go b/game/graphics/shader.go index 51026819..7ffbdc3d 100644 --- a/game/graphics/shader.go +++ b/game/graphics/shader.go @@ -62,6 +62,9 @@ type GeometryConstraints struct { // HasNormals specifies whether the mesh has normals. HasNormals bool + // HasTangents specifies whether the mesh has tangents. + HasTangents bool + // HasTexCoords specifies whether the mesh has texture coordinates. HasTexCoords bool @@ -143,6 +146,7 @@ func (e *Engine) createGeometryProgramCode(shader *lsl.Shader, info internal.Sha return e.shaderBuilder.BuildGeometryCode(GeometryConstraints{ HasArmature: info.MeshHasArmature, HasNormals: info.MeshHasNormals, + HasTangents: info.MeshHasTangents, HasTexCoords: info.MeshHasTextureUVs, HasVertexColors: info.MeshHasVertexColors, }, shader) diff --git a/util/gltfutil/util.go b/util/gltfutil/util.go index fb77f63f..3b70e2ca 100644 --- a/util/gltfutil/util.go +++ b/util/gltfutil/util.go @@ -123,7 +123,7 @@ func Tangents(doc *gltf.Document, primitive *gltf.Primitive) ([]sprec.Vec3, erro if accessor.BufferView == nil { return nil, fmt.Errorf("accessor lacks a buffer view") } - if accessor.Type != gltf.AccessorVec3 { + if accessor.Type != gltf.AccessorVec4 { return nil, fmt.Errorf("unsupported accessor type %d", accessor.Type) } buffer := BufferViewData(doc, *accessor.BufferView) @@ -133,7 +133,8 @@ func Tangents(doc *gltf.Document, primitive *gltf.Primitive) ([]sprec.Vec3, erro switch accessor.ComponentType { case gltf.ComponentFloat: for i := range result { - result[i] = scanner.ScanSPVec3() + item := scanner.ScanSPVec4() + result[i] = sprec.Vec3Prod(sprec.NewVec3(item.X, item.Y, item.Z), item.W) } default: return nil, fmt.Errorf("unsupported accessor component type %d", accessor.ComponentType) From 70f7773b63cfd1f036431e4c3d3ea885123a1add Mon Sep 17 00:00:00 2001 From: Momchil Atanasov Date: Sun, 29 Sep 2024 18:28:33 +0300 Subject: [PATCH 2/4] Add support for normal mapping --- game/asset/dsl/provider_model.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/game/asset/dsl/provider_model.go b/game/asset/dsl/provider_model.go index 7416ae68..4eab41d8 100644 --- a/game/asset/dsl/provider_model.go +++ b/game/asset/dsl/provider_model.go @@ -227,6 +227,8 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model if texIndex, texScale := gltfutil.NormalTextureIndexScale(gltfDoc, gltfMaterial); texIndex != nil { normalTextureIndex = texIndex normalScale = texScale + sampler := samplersFromIndex[*texIndex] + sampler.Texture().SetLinear(true) } else { normalScale = 1.0 } @@ -782,8 +784,8 @@ func createPBRShader(cfg pbrShaderConfig) string { if cfg.hasMetallicRoughnessTexture { sourceCode += ` var metallicRoughness vec4 = sample(metallicRoughnessSampler, #vertexUV) - #metallic = metallicRoughness.b - #roughness = metallicRoughness.g + #metallic = metallicRoughness.b * metallic + #roughness = metallicRoughness.g * roughness ` } else { sourceCode += ` @@ -792,6 +794,13 @@ func createPBRShader(cfg pbrShaderConfig) string { ` } + if cfg.hasNormalTexture { + sourceCode += ` + var lsNormal vec4 = sample(normalSampler, #vertexUV) + #normal = mapNormal(lsNormal.xyz, normalScale) + ` + } + sourceCode += `}` return sourceCode From 6244d10d11d67762e7b6a62f978135baa4d4b610 Mon Sep 17 00:00:00 2001 From: Momchil Atanasov Date: Sun, 6 Oct 2024 17:33:33 +0300 Subject: [PATCH 3/4] Rework asset textures for manual mipmap support --- game/asset/dsl/provider_image.go | 58 ++++++++- game/asset/dsl/provider_model.go | 7 +- game/asset/dsl/provider_texture.go | 66 +++++++--- game/asset/mdl/converter.go | 15 ++- game/asset/mdl/image_cube.go | 10 ++ game/asset/mdl/image_util.go | 8 +- game/asset/mdl/texture.go | 162 ++++++++++++++++--------- game/asset/texture.go | 9 +- game/graphics/stage_bloom.go | 16 ++- game/graphics/stage_common.go | 12 ++ game/graphics/stage_forward_source.go | 8 +- game/graphics/stage_geometry_source.go | 16 ++- game/graphics/stage_lighting.go | 3 +- game/graphics/stage_probe.go | 8 +- game/resource_texture.go | 29 +++-- render/texture.go | 38 ++++-- ui/canvas_renderer.go | 11 +- ui/font_factory.go | 18 ++- ui/image_factory.go | 10 +- ui/std/viewport.go | 9 +- 20 files changed, 375 insertions(+), 138 deletions(-) diff --git a/game/asset/dsl/provider_image.go b/game/asset/dsl/provider_image.go index f272dd17..329f6761 100644 --- a/game/asset/dsl/provider_image.go +++ b/game/asset/dsl/provider_image.go @@ -2,6 +2,7 @@ package dsl import ( "fmt" + "math" "os" "github.com/mokiat/gog/opt" @@ -146,7 +147,7 @@ func IrradianceCubeImage(imageProvider Provider[*mdl.CubeImage], opts ...Operati } sampleCount := cfg.sampleCount.ValueOrDefault(20) - return mdl.BuildIrradianceCubeImage(image, sampleCount), nil + return mdl.BuildIrradianceCubeImage(image, sampleCount, 0.0), nil }, // digest function @@ -156,6 +157,61 @@ func IrradianceCubeImage(imageProvider Provider[*mdl.CubeImage], opts ...Operati )) } +// ReflectionCubeImages creates a reflection cube image mipmap set from the +// provided HDR skybox cube image. +func ReflectionCubeImages(imageProvider Provider[*mdl.CubeImage], opts ...Operation) Provider[[]*mdl.CubeImage] { + return OnceProvider(FuncProvider( + // get function + func() ([]*mdl.CubeImage, error) { + var cfg irradianceConfig + for _, opt := range opts { + if err := opt.Apply(&cfg); err != nil { + return nil, fmt.Errorf("failed to configure irradiance cube image: %w", err) + } + } + + image, err := imageProvider.Get() + if err != nil { + return nil, fmt.Errorf("error getting image: %w", err) + } + + sampleCount := cfg.sampleCount.ValueOrDefault(20) + + var dimensions []int + dimension := image.Side(mdl.CubeSideFront).Width() + for dimension > 0 { + dimensions = append(dimensions, dimension) + dimension /= 2 + } + + mipmapCount := len(dimensions) + result := make([]*mdl.CubeImage, mipmapCount) + for i := range mipmapCount { + if i == 0 { + result[i] = image.MapTexels(func(texel mdl.Color) mdl.Color { + return mdl.Color{ + R: texel.R * math.Pi * 2.0, + G: texel.G * math.Pi * 2.0, + B: texel.B * math.Pi * 2.0, + A: 1.0, + } + }) + } else { + minDot := 1.0 - (float64(i) / float64(mipmapCount-1)) + dimension := dimensions[i] + result[i] = mdl.BuildIrradianceCubeImage(image.Scale(dimension), sampleCount, minDot) + } + } + return result, nil + }, + + // digest function + func() ([]byte, error) { + return CreateDigest("reflection-cube-images", imageProvider, opts) + }, + )) +} + type irradianceConfig struct { sampleCount opt.T[int] } diff --git a/game/asset/dsl/provider_model.go b/game/asset/dsl/provider_model.go index 4eab41d8..ef072786 100644 --- a/game/asset/dsl/provider_model.go +++ b/game/asset/dsl/provider_model.go @@ -128,13 +128,10 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model // build textures texturesFromIndex := make(map[int]*mdl.Texture) for i, img := range imagesFromIndex { - texture := &mdl.Texture{} + texture := mdl.Create2DTexture(img.Width(), img.Height(), 1, mdl.TextureFormatRGBA8) texture.SetName(img.Name()) - texture.SetKind(mdl.TextureKind2D) - texture.SetFormat(mdl.TextureFormatRGBA8) texture.SetGenerateMipmaps(true) - texture.Resize(img.Width(), img.Height()) - texture.SetLayerImage(0, img) + texture.SetLayerImage(0, 0, img) texturesFromIndex[i] = texture } diff --git a/game/asset/dsl/provider_texture.go b/game/asset/dsl/provider_texture.go index a9c86a91..37b96337 100644 --- a/game/asset/dsl/provider_texture.go +++ b/game/asset/dsl/provider_texture.go @@ -25,14 +25,11 @@ func Create2DTexture(imageProvider Provider[*mdl.Image], opts ...Operation) Prov return nil, fmt.Errorf("failed to get image: %w", err) } - var texture mdl.Texture + texture := mdl.Create2DTexture(image.Width(), image.Height(), 1, cfg.format.ValueOrDefault(mdl.TextureFormatRGBA8)) texture.SetName(image.Name()) - texture.SetKind(mdl.TextureKind2D) - texture.SetFormat(cfg.format.ValueOrDefault(mdl.TextureFormatRGBA8)) texture.SetGenerateMipmaps(cfg.mipmapping) - texture.Resize(image.Width(), image.Height()) - texture.SetLayerImage(0, image) - return &texture, nil + texture.SetLayerImage(0, 0, image) + return texture, nil }, // digest function @@ -67,18 +64,15 @@ func CreateCubeTexture(cubeImageProvider Provider[*mdl.CubeImage], opts ...Opera topImage := cubeImage.Side(mdl.CubeSideTop) bottomImage := cubeImage.Side(mdl.CubeSideBottom) - var texture mdl.Texture - texture.SetKind(mdl.TextureKindCube) - texture.SetFormat(cfg.format.ValueOrDefault(mdl.TextureFormatRGBA16F)) + texture := mdl.CreateCubeTexture(frontImage.Width(), 1, cfg.format.ValueOrDefault(mdl.TextureFormatRGBA16F)) texture.SetGenerateMipmaps(cfg.mipmapping) - texture.Resize(frontImage.Width(), frontImage.Height()) - texture.SetLayerImage(0, frontImage) - texture.SetLayerImage(1, rearImage) - texture.SetLayerImage(2, leftImage) - texture.SetLayerImage(3, rightImage) - texture.SetLayerImage(4, topImage) - texture.SetLayerImage(5, bottomImage) - return &texture, nil + texture.SetLayerImage(0, 0, frontImage) + texture.SetLayerImage(0, 1, rearImage) + texture.SetLayerImage(0, 2, leftImage) + texture.SetLayerImage(0, 3, rightImage) + texture.SetLayerImage(0, 4, topImage) + texture.SetLayerImage(0, 5, bottomImage) + return texture, nil }, // digest function @@ -88,6 +82,44 @@ func CreateCubeTexture(cubeImageProvider Provider[*mdl.CubeImage], opts ...Opera )) } +// CreateCubeMipmapTexture creates a new cube texture with the specified format +// and source mipmap images. +func CreateCubeMipmapTexture(cubeImagesProvider Provider[[]*mdl.CubeImage], opts ...Operation) Provider[*mdl.Texture] { + return OnceProvider(FuncProvider( + // get function + func() (*mdl.Texture, error) { + var cfg textureConfig + for _, opt := range opts { + if err := opt.Apply(&cfg); err != nil { + return nil, fmt.Errorf("failed to configure cube texture: %w", err) + } + } + + cubeImages, err := cubeImagesProvider.Get() + if err != nil { + return nil, fmt.Errorf("failed to get cube image: %w", err) + } + + dimension := cubeImages[0].Side(mdl.CubeSideFront).Width() + texture := mdl.CreateCubeTexture(dimension, len(cubeImages), cfg.format.ValueOrDefault(mdl.TextureFormatRGBA16F)) + for i, cubeImage := range cubeImages { + texture.SetLayerImage(i, 0, cubeImage.Side(mdl.CubeSideFront)) + texture.SetLayerImage(i, 1, cubeImage.Side(mdl.CubeSideRear)) + texture.SetLayerImage(i, 2, cubeImage.Side(mdl.CubeSideLeft)) + texture.SetLayerImage(i, 3, cubeImage.Side(mdl.CubeSideRight)) + texture.SetLayerImage(i, 4, cubeImage.Side(mdl.CubeSideTop)) + texture.SetLayerImage(i, 5, cubeImage.Side(mdl.CubeSideBottom)) + } + return texture, nil + }, + + // digest function + func() ([]byte, error) { + return CreateDigest("create-cube-mipmap-texture", cubeImagesProvider, opts) + }, + )) +} + type textureConfig struct { format opt.T[mdl.TextureFormat] mipmapping bool diff --git a/game/asset/mdl/converter.go b/game/asset/mdl/converter.go index e2700e43..59fb29de 100644 --- a/game/asset/mdl/converter.go +++ b/game/asset/mdl/converter.go @@ -844,13 +844,18 @@ func (c *Converter) convertTexture(texture *Texture) (uint32, error) { flags |= asset.TextureFlagMipmapping } assetTexture := asset.Texture{ - Width: uint32(texture.Width()), - Height: uint32(texture.Height()), Format: texture.Format(), Flags: flags, - Layers: gog.Map(texture.layers, func(layer TextureLayer) asset.TextureLayer { - return asset.TextureLayer{ - Data: layer.Data(), + MipmapLayers: gog.Map(texture.mipmapLayers, func(mipLayer MipmapLayer) asset.MipmapLayer { + return asset.MipmapLayer{ + Width: uint32(mipLayer.Width()), + Height: uint32(mipLayer.Height()), + Depth: uint32(mipLayer.Depth()), + Layers: gog.Map(mipLayer.Layers(), func(layer TextureLayer) asset.TextureLayer { + return asset.TextureLayer{ + Data: layer.Data(), + } + }), } }), } diff --git a/game/asset/mdl/image_cube.go b/game/asset/mdl/image_cube.go index 31f1f92f..124a176e 100644 --- a/game/asset/mdl/image_cube.go +++ b/game/asset/mdl/image_cube.go @@ -61,3 +61,13 @@ func (i *CubeImage) Scale(newSize int) *CubeImage { } return dstImage } + +func (i *CubeImage) MapTexels(fn func(texel Color) Color) *CubeImage { + dstImage := i.Scale(i.size) // copy + for _, sideImage := range dstImage.sides { + for i := range sideImage.texels { + sideImage.texels[i] = fn(sideImage.texels[i]) + } + } + return dstImage +} diff --git a/game/asset/mdl/image_util.go b/game/asset/mdl/image_util.go index 15488d75..f4d33500 100644 --- a/game/asset/mdl/image_util.go +++ b/game/asset/mdl/image_util.go @@ -141,21 +141,21 @@ func BuildCubeSideFromEquirectangular(side CubeSide, srcImage *Image) *Image { return dstImage } -func BuildIrradianceCubeImage(srcImage *CubeImage, sampleCount int) *CubeImage { +func BuildIrradianceCubeImage(srcImage *CubeImage, sampleCount int, minDot float64) *CubeImage { dstImage := NewCubeImage(srcImage.size) var group sync.WaitGroup for i := range srcImage.sides { group.Add(1) go func() { defer group.Done() - projectIrradianceCubeImageSide(srcImage, dstImage, CubeSide(i), sampleCount) + projectIrradianceCubeImageSide(srcImage, dstImage, CubeSide(i), sampleCount, minDot) }() } group.Wait() return dstImage } -func projectIrradianceCubeImageSide(srcImage, dstImage *CubeImage, side CubeSide, sampleCount int) { +func projectIrradianceCubeImageSide(srcImage, dstImage *CubeImage, side CubeSide, sampleCount int, minDot float64) { dimension := srcImage.size startLat := dprec.Degrees(-90.0) @@ -195,7 +195,7 @@ func projectIrradianceCubeImageSide(srcImage, dstImage *CubeImage, side CubeSide longitudeCS*latitudeCS, latitudeSN, ) - if dot := dprec.Vec3Dot(uvw, direction); dot > 0.0 { + if dot := dprec.Vec3Dot(uvw, direction); dot > minDot { positiveSamples++ srcColor := srcImage.TexelUVW(direction) color.R += srcColor.R * dot diff --git a/game/asset/mdl/texture.go b/game/asset/mdl/texture.go index 55b80a96..e07b12e4 100644 --- a/game/asset/mdl/texture.go +++ b/game/asset/mdl/texture.go @@ -38,15 +38,75 @@ const ( type TextureKind uint8 +func Create2DTexture(width, height, mipmaps int, format TextureFormat) *Texture { + mipmapLayers := make([]MipmapLayer, mipmaps) + for i := range mipmapLayers { + mipWidth := max(1, width>>i) + mipHeight := max(1, height>>i) + mipTexelSize := textureFormatSize(format) + mipmapLayers[i] = MipmapLayer{ + width: mipWidth, + height: mipHeight, + depth: 1, + layers: []TextureLayer{ + { + data: make([]byte, mipWidth*mipHeight*mipTexelSize), + }, + }, + } + } + return &Texture{ + kind: TextureKind2D, + format: format, + mipmapLayers: mipmapLayers, + } +} + +func CreateCubeTexture(dimension, mipmaps int, format TextureFormat) *Texture { + mipmapLayers := make([]MipmapLayer, mipmaps) + for i := range mipmapLayers { + mipDimension := max(1, dimension>>i) + mipTexelSize := textureFormatSize(format) + mipmapLayers[i] = MipmapLayer{ + width: mipDimension, + height: mipDimension, + depth: 1, + layers: []TextureLayer{ + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + { + data: make([]byte, mipDimension*mipDimension*mipTexelSize), + }, + }, + } + } + return &Texture{ + kind: TextureKindCube, + format: format, + mipmapLayers: mipmapLayers, + } +} + type Texture struct { name string kind TextureKind - width int - height int format TextureFormat generateMipmaps bool isLinear bool - layers []TextureLayer + mipmapLayers []MipmapLayer } func (t *Texture) Name() string { @@ -61,29 +121,10 @@ func (t *Texture) Kind() TextureKind { return t.kind } -func (t *Texture) SetKind(kind TextureKind) { - t.kind = kind -} - -func (t *Texture) Width() int { - return t.width -} - -func (t *Texture) Height() int { - return t.height -} - func (t *Texture) Format() TextureFormat { return t.format } -func (t *Texture) SetFormat(format TextureFormat) { - t.format = format - if len(t.layers) > 0 { - panic("setting texture format with layers is not supported yet") - } -} - func (t *Texture) Linear() bool { return t.isLinear } @@ -100,53 +141,41 @@ func (t *Texture) SetGenerateMipmaps(generateMipmaps bool) { t.generateMipmaps = generateMipmaps } -func (t *Texture) Resize(width, height int) { - t.width = width - t.height = height - if len(t.layers) > 0 { - panic("resizing texture with layers is not supported yet") - } -} - -func (t *Texture) EnsureLayer(index int) { - for index >= len(t.layers) { - t.layers = append(t.layers, createTextureLayer(t.width, t.height, t.format)) - } -} - -func (t *Texture) SetLayerImage(index int, image *Image) { - t.EnsureLayer(index) - - if image.width != t.width || image.height != t.height { - image = image.Scale(t.width, t.height) - } +func (t *Texture) SetLayerImage(mipmap, index int, image *Image) { + mipmapLayer := t.mipmapLayers[mipmap] switch t.format { case TextureFormatRGBA8: - copy(t.layers[index].data, image.DataRGBA8()) + copy(mipmapLayer.layers[index].data, image.DataRGBA8()) case TextureFormatRGBA16F: - copy(t.layers[index].data, image.DataRGBA16F()) + copy(mipmapLayer.layers[index].data, image.DataRGBA16F()) case TextureFormatRGBA32F: - copy(t.layers[index].data, image.DataRGBA32F()) + copy(mipmapLayer.layers[index].data, image.DataRGBA32F()) default: panic(fmt.Errorf("unsupported texture format: %v", t.format)) } } -func createTextureLayer(width, height int, format TextureFormat) TextureLayer { - var texelSize int - switch format { - case TextureFormatRGBA8: - texelSize = 4 - case TextureFormatRGBA16F: - texelSize = 8 - case TextureFormatRGBA32F: - texelSize = 16 - default: - panic(fmt.Errorf("unsupported texture format: %v", format)) - } - return TextureLayer{ - data: make([]byte, width*height*texelSize), - } +type MipmapLayer struct { + width int + height int + depth int + layers []TextureLayer +} + +func (l *MipmapLayer) Width() int { + return l.width +} + +func (l *MipmapLayer) Height() int { + return l.height +} + +func (l *MipmapLayer) Depth() int { + return l.depth +} + +func (l *MipmapLayer) Layers() []TextureLayer { + return l.layers } type TextureLayer struct { @@ -156,3 +185,16 @@ type TextureLayer struct { func (l *TextureLayer) Data() []byte { return l.data } + +func textureFormatSize(format TextureFormat) int { + switch format { + case TextureFormatRGBA8: + return 4 + case TextureFormatRGBA16F: + return 8 + case TextureFormatRGBA32F: + return 16 + default: + panic(fmt.Errorf("unsupported texture format: %v", format)) + } +} diff --git a/game/asset/texture.go b/game/asset/texture.go index aa65054d..d04f61f2 100644 --- a/game/asset/texture.go +++ b/game/asset/texture.go @@ -40,10 +40,15 @@ func (f TextureFlag) Has(flag TextureFlag) bool { } type Texture struct { + Format TexelFormat + Flags TextureFlag + MipmapLayers []MipmapLayer +} + +type MipmapLayer struct { Width uint32 Height uint32 - Format TexelFormat - Flags TextureFlag + Depth uint32 Layers []TextureLayer } diff --git a/game/graphics/stage_bloom.go b/game/graphics/stage_bloom.go index ad132d5c..8c3efebc 100644 --- a/game/graphics/stage_bloom.go +++ b/game/graphics/stage_bloom.go @@ -241,11 +241,15 @@ func (s *BloomStage) allocateTextures(width, height uint32) { s.pingTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Bloom Ping Texture", - Width: width, - Height: height, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA16F, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: width, + Height: height, + }, + }, }) s.pingFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ Label: "Bloom Ping Framebuffer", @@ -256,11 +260,15 @@ func (s *BloomStage) allocateTextures(width, height uint32) { s.pongTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Bloom Pong Texture", - Width: width, - Height: height, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA16F, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: width, + Height: height, + }, + }, }) s.pongFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ Label: "Bloom Pong Framebuffer", diff --git a/game/graphics/stage_common.go b/game/graphics/stage_common.go index ea155e4a..7302b3cf 100644 --- a/game/graphics/stage_common.go +++ b/game/graphics/stage_common.go @@ -40,6 +40,7 @@ type commonStageData struct { nearestSampler render.Sampler linearSampler render.Sampler + mipmapSampler render.Sampler depthSampler render.Sampler commandBuffer render.CommandBuffer @@ -80,6 +81,12 @@ func (d *commonStageData) Allocate() { Filtering: render.FilterModeLinear, Mipmapping: false, }) + d.mipmapSampler = d.api.CreateSampler(render.SamplerInfo{ + Label: "Mipmap Sampler", + Wrapping: render.WrapModeClamp, + Filtering: render.FilterModeLinear, + Mipmapping: true, + }) d.depthSampler = d.api.CreateSampler(render.SamplerInfo{ Label: "Depth Sampler", Wrapping: render.WrapModeClamp, @@ -152,6 +159,7 @@ func (d *commonStageData) Release() { defer d.nearestSampler.Release() defer d.linearSampler.Release() + defer d.mipmapSampler.Release() defer d.depthSampler.Release() defer d.uniformBuffer.Release() @@ -208,6 +216,10 @@ func (d *commonStageData) LinearSampler() render.Sampler { return d.linearSampler } +func (d *commonStageData) MipmapSampler() render.Sampler { + return d.mipmapSampler +} + func (d *commonStageData) DepthSampler() render.Sampler { return d.depthSampler } diff --git a/game/graphics/stage_forward_source.go b/game/graphics/stage_forward_source.go index 51a44ca0..0f62c37d 100644 --- a/game/graphics/stage_forward_source.go +++ b/game/graphics/stage_forward_source.go @@ -57,11 +57,15 @@ func (s *ForwardSourceStage) PostRender() { func (s *ForwardSourceStage) allocateTextures() { s.hdrTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Forward Source Texture", - Width: s.framebufferWidth, - Height: s.framebufferHeight, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA16F, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: s.framebufferWidth, + Height: s.framebufferHeight, + }, + }, }) } diff --git a/game/graphics/stage_geometry_source.go b/game/graphics/stage_geometry_source.go index 16456413..428a8b4b 100644 --- a/game/graphics/stage_geometry_source.go +++ b/game/graphics/stage_geometry_source.go @@ -64,19 +64,27 @@ func (s *GeometrySourceStage) PostRender() { func (s *GeometrySourceStage) allocateTextures() { s.albedoMetallicTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Geometry Source Albedo Texture", - Width: s.framebufferWidth, - Height: s.framebufferHeight, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA8, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: s.framebufferWidth, + Height: s.framebufferHeight, + }, + }, }) s.normalRoughnessTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Geometry Source Normal Texture", - Width: s.framebufferWidth, - Height: s.framebufferHeight, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA16F, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: s.framebufferWidth, + Height: s.framebufferHeight, + }, + }, }) } diff --git a/game/graphics/stage_lighting.go b/game/graphics/stage_lighting.go index f83ee64b..6572db54 100644 --- a/game/graphics/stage_lighting.go +++ b/game/graphics/stage_lighting.go @@ -313,6 +313,7 @@ func (s *LightingStage) renderAmbientLight(ctx StageContext, light *AmbientLight nearestSampler := s.data.NearestSampler() linearSampler := s.data.LinearSampler() + mipmapSampler := s.data.MipmapSampler() albedoMetallicTexture := s.input.AlbedoMetallicTexture() normalRoughnessTexture := s.input.NormalRoughnessTexture() @@ -328,7 +329,7 @@ func (s *LightingStage) renderAmbientLight(ctx StageContext, light *AmbientLight commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, depthTexture) commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, nearestSampler) commandBuffer.TextureUnit(internal.TextureBindingLightingReflectionTexture, light.reflectionTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingReflectionTexture, linearSampler) + commandBuffer.SamplerUnit(internal.TextureBindingLightingReflectionTexture, mipmapSampler) commandBuffer.TextureUnit(internal.TextureBindingLightingRefractionTexture, light.refractionTexture) commandBuffer.SamplerUnit(internal.TextureBindingLightingRefractionTexture, linearSampler) commandBuffer.UniformBufferUnit( diff --git a/game/graphics/stage_probe.go b/game/graphics/stage_probe.go index e26706e9..7dfd1e78 100644 --- a/game/graphics/stage_probe.go +++ b/game/graphics/stage_probe.go @@ -59,11 +59,15 @@ func (s *ExposureProbeStage) Allocate() { s.exposureAlbedoTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ Label: "Exposure Probe Texture", - Width: 1, - Height: 1, GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA16F, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: 1, + Height: 1, + }, + }, }) s.exposureFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ Label: "Exposure Probe Framebuffer", diff --git a/game/resource_texture.go b/game/resource_texture.go index 2c34a16d..ba87c3b3 100644 --- a/game/resource_texture.go +++ b/game/resource_texture.go @@ -3,6 +3,7 @@ package game import ( "fmt" + "github.com/mokiat/gog" "github.com/mokiat/lacking/game/asset" "github.com/mokiat/lacking/render" "github.com/mokiat/lacking/util/async" @@ -24,12 +25,16 @@ func (s *ResourceSet) allocateTexture2D(assetTexture asset.Texture) async.Promis promise := async.NewPromise[render.Texture]() s.gfxWorker.Schedule(func() { texture := s.renderAPI.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: assetTexture.Width, - Height: assetTexture.Height, GenerateMipmaps: assetTexture.Flags.Has(asset.TextureFlagMipmapping), GammaCorrection: !assetTexture.Flags.Has(asset.TextureFlagLinearSpace), Format: s.resolveDataFormat(assetTexture.Format), - Data: assetTexture.Layers[0].Data, + MipmapLayers: gog.Map(assetTexture.MipmapLayers, func(layer asset.MipmapLayer) render.Mipmap2DLayer { + return render.Mipmap2DLayer{ + Width: layer.Width, + Height: layer.Height, + Data: layer.Layers[0].Data, + } + }), }) promise.Deliver(texture) }) @@ -40,16 +45,20 @@ func (s *ResourceSet) allocateTextureCube(assetTexture asset.Texture) async.Prom promise := async.NewPromise[render.Texture]() s.gfxWorker.Schedule(func() { texture := s.renderAPI.CreateColorTextureCube(render.ColorTextureCubeInfo{ - Dimension: assetTexture.Width, GenerateMipmaps: assetTexture.Flags.Has(asset.TextureFlagMipmapping), GammaCorrection: !assetTexture.Flags.Has(asset.TextureFlagLinearSpace), Format: s.resolveDataFormat(assetTexture.Format), - FrontSideData: assetTexture.Layers[0].Data, - BackSideData: assetTexture.Layers[1].Data, - LeftSideData: assetTexture.Layers[2].Data, - RightSideData: assetTexture.Layers[3].Data, - TopSideData: assetTexture.Layers[4].Data, - BottomSideData: assetTexture.Layers[5].Data, + MipmapLayers: gog.Map(assetTexture.MipmapLayers, func(layer asset.MipmapLayer) render.MipmapCubeLayer { + return render.MipmapCubeLayer{ + Dimension: layer.Width, + FrontSideData: layer.Layers[0].Data, + BackSideData: layer.Layers[1].Data, + LeftSideData: layer.Layers[2].Data, + RightSideData: layer.Layers[3].Data, + TopSideData: layer.Layers[4].Data, + BottomSideData: layer.Layers[5].Data, + } + }), }) promise.Deliver(texture) }) diff --git a/render/texture.go b/render/texture.go index 66ac73b0..2ab692b1 100644 --- a/render/texture.go +++ b/render/texture.go @@ -66,12 +66,6 @@ type ColorTexture2DInfo struct { // debugging and logging purposes only. Label string - // Width specifies the width of the texture. - Width uint32 - - // Height specifies the height of the texture. - Height uint32 - // GenerateMipmaps specifies whether mipmaps should be generated. GenerateMipmaps bool @@ -82,6 +76,22 @@ type ColorTexture2DInfo struct { // Format specifies the format of the data. Format DataFormat + // MipmapLayers specifies the data that should be uploaded to the texture. + // + // If GenerateMipmaps is set to true, the first layer should contain the + // base image data and subsequent layers are not required. + MipmapLayers []Mipmap2DLayer +} + +// Mipmap2DLayer represents a single layer of a 2D texture. +type Mipmap2DLayer struct { + + // Width specifies the width of the texture. + Width uint32 + + // Height specifies the height of the texture. + Height uint32 + // Data specifies the data that should be uploaded to the texture. Data []byte } @@ -94,9 +104,6 @@ type ColorTextureCubeInfo struct { // debugging and logging purposes only. Label string - // Dimension specifies the width, height and length of the texture. - Dimension uint32 - // GenerateMipmaps specifies whether mipmaps should be generated. GenerateMipmaps bool @@ -107,6 +114,19 @@ type ColorTextureCubeInfo struct { // Format specifies the format of the data. Format DataFormat + // MipmapLayers specifies the data that should be uploaded to the texture. + // + // If GenerateMipmaps is set to true, the first layer should contain the + // base image data and subsequent layers are not required. + MipmapLayers []MipmapCubeLayer +} + +// MipmapCubeLayer represents a single layer of a cube texture. +type MipmapCubeLayer struct { + + // Dimension specifies the width, height and length of the texture. + Dimension uint32 + // FrontSideData specifies the data that should be uploaded to the front // side of the texture. FrontSideData []byte diff --git a/ui/canvas_renderer.go b/ui/canvas_renderer.go index e8bb9d72..f94c618a 100644 --- a/ui/canvas_renderer.go +++ b/ui/canvas_renderer.go @@ -87,12 +87,17 @@ func (c *canvasRenderer) onCreate() { c.uniforms = ubo.NewUniformBlockBuffer(c.api, uniformBufferSize) c.whiteMask = c.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: 1, - Height: 1, + Label: "White Mask", GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA8, - Data: []byte{0xFF, 0xFF, 0xFF, 0xFF}, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: 1, + Height: 1, + Data: []byte{0xFF, 0xFF, 0xFF, 0xFF}, + }, + }, }) c.shapeMesh.Allocate(c.api) diff --git a/ui/font_factory.go b/ui/font_factory.go index 6f501cd5..ea467784 100644 --- a/ui/font_factory.go +++ b/ui/font_factory.go @@ -68,11 +68,16 @@ func (f *fontFactory) Init() { } f.colorTexture = f.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: f.fontImageSize, - Height: f.fontImageSize, + Label: "Font Color Texture", GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA8, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: f.fontImageSize, + Height: f.fontImageSize, + }, + }, }) f.stencilTexture = f.api.CreateStencilTexture2D(render.StencilTexture2DInfo{ @@ -273,11 +278,16 @@ func (f *fontFactory) CreateFont(font *opentype.Font) (*Font, error) { f.renderer.onEnd() resultTexture := f.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: f.fontImageSize, - Height: f.fontImageSize, + Label: "Font Output Texture", GenerateMipmaps: true, GammaCorrection: false, Format: render.DataFormatRGBA8, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: f.fontImageSize, + Height: f.fontImageSize, + }, + }, }) commandBuffer.CopyFramebufferToTexture(render.CopyFramebufferToTextureInfo{ Texture: resultTexture, diff --git a/ui/image_factory.go b/ui/image_factory.go index 643c8935..22c9d570 100644 --- a/ui/image_factory.go +++ b/ui/image_factory.go @@ -20,12 +20,16 @@ func (f *imageFactory) CreateImage(img image.Image) *Image { bounds := img.Bounds() size := NewSize(bounds.Dx(), bounds.Dy()) texture := f.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: uint32(size.Width), - Height: uint32(size.Height), GenerateMipmaps: true, GammaCorrection: false, Format: render.DataFormatRGBA8, - Data: imgToRGBA8(img), + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: uint32(size.Width), + Height: uint32(size.Height), + Data: imgToRGBA8(img), + }, + }, }) return newImage(texture, size) } diff --git a/ui/std/viewport.go b/ui/std/viewport.go index d51c1f64..d4b4ffc9 100644 --- a/ui/std/viewport.go +++ b/ui/std/viewport.go @@ -154,11 +154,16 @@ func (c *viewportSurface) createFramebuffer(width, height uint32) { c.fbWidth = width c.fbHeight = height c.colorTexture = c.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: width, - Height: height, + Label: "Viewport Color Texture", GenerateMipmaps: false, GammaCorrection: false, Format: render.DataFormatRGBA8, + MipmapLayers: []render.Mipmap2DLayer{ + { + Width: width, + Height: height, + }, + }, }) c.framebuffer = c.api.CreateFramebuffer(render.FramebufferInfo{ ColorAttachments: [4]opt.T[render.TextureAttachment]{ From b30c9f4603d02400cd81450a139d33a3b22046d7 Mon Sep 17 00:00:00 2001 From: Momchil Atanasov Date: Sun, 6 Oct 2024 21:09:18 +0300 Subject: [PATCH 4/4] Bump dependencies --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index bc1c3733..38f80346 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/onsi/gomega v1.34.2 github.com/qmuntal/gltf v0.27.0 github.com/x448/float16 v0.8.4 - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - golang.org/x/image v0.20.0 + golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 + golang.org/x/image v0.21.0 golang.org/x/sync v0.8.0 ) @@ -22,14 +22,14 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.25.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9ecf682b..6f8843c9 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d h1:Jaz2JzpQaQXyET0AjLBXShrthbpqMkhGiEfkcQAiAUs= +github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -42,26 +42,26 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/qmuntal/gltf v0.27.0 h1:A6E5F9Efg/G8ke4OfzJtSDfaTKwXRx4OYCoXfslF534= github.com/qmuntal/gltf v0.27.0/go.mod h1:YoXZOt0Nc0kIfSKOLZIRoV4FycdC+GzE+3JgiAGYoMs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= -golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=