EngoEngine/engo

Zoom-out causes random disappearance of texture

inkeliz opened this issue · 8 comments

I'm unable to re-open issue #679, the bug is very similar, but the causes seem to be not related.

So, I have that scene:

image

There's two layers of image, both created by common.NewTextureSingle. The first image is the background, the green grass (which is 100x100). Then, on top, has a grid which is 200x200, which is just a square border.

Everything works, until zoom-out. If I zoom-out too much the texture desapears:

image

I'm trying to find what is causing it. I don't know if it's a bug or it's expected behaviour.

It’s definitely unwanted behavior. Let me know if you come up with anything. I’ll try looking into it later tonight to see I can replicate it. Thanks for tracking this down!

I can't find anything, probably next year. :P

I write one small piece of code to make it easier to reproduce the problem. Since it needs the assets, you can download the code and the assets here.


package main

import (
	"github.com/EngoEngine/ecs"
	"github.com/EngoEngine/engo"
	"github.com/EngoEngine/engo/common"
)

// Game
type Game struct {
	common.RenderSystem
	common.MouseSystem
	common.EdgeScroller
	common.KeyboardScroller
	common.MouseZoomer
}

func (m *Game) Preload() {
	m.EdgeScroller.EdgeMargin = 20
	m.EdgeScroller.ScrollSpeed = 400

	m.KeyboardScroller.ScrollSpeed = 400
	m.KeyboardScroller.BindKeyboard(engo.DefaultHorizontalAxis, engo.DefaultVerticalAxis)

	m.MouseZoomer.ZoomSpeed = -0.1

	engo.Files.Load("grid.png")
	engo.Files.Load("grid-full.png")
}

func (m *Game) Setup(u engo.Updater) {
	world, _ := u.(*ecs.World)
	world.AddSystem(&m.RenderSystem)
	world.AddSystem(&m.MouseSystem)
	world.AddSystem(&m.EdgeScroller)
	world.AddSystem(&m.KeyboardScroller)
	world.AddSystem(&m.MouseZoomer)

	// Using same texture rotating:
	for i := float32(0); i < 100*100; i += 200 {
		for j := float32(0); j < 100*100; j += 200 {
			m.RenderSystem.AddByInterface(NewGrid(engo.Point{X: i, Y: j}, 0))
			m.RenderSystem.AddByInterface(NewGrid(engo.Point{X: i, Y: j + 200}, 270))
			m.RenderSystem.AddByInterface(NewGrid(engo.Point{X: i + 200, Y: j}, 90))
			m.RenderSystem.AddByInterface(NewGrid(engo.Point{X: i + 200, Y: j + 200}, 180))
		}
	}

	// Using single image (bug as well):
	/**
	for i := float32(0); i < 100*100; i += 200 {
		for j := float32(0); j < 100*100; j += 200 {
			m.RenderSystem.AddByInterface(NewSingleImageGrid(engo.Point{X: i, Y: j}))
		}
	}
	*/

	common.MaxZoom = 1000
	common.CameraBounds = engo.AABB{Min: engo.Point{X: 0, Y: 0}, Max: engo.Point{X: 10000, Y: 10000}}
}

func (m *Game) Type() string {
	return "game"
}

func main() {
	engo.Run(engo.RunOptions{
		Title:          "Test",
		Width:          800,
		Height:         800,
		ScaleOnResize:  true,
		NotResizable:   true,
		StandardInputs: true,
		MSAA:           8,
	}, new(Game))
}

// Entity

type BasicElement struct {
	ecs.BasicEntity
	*common.RenderComponent
	*common.SpaceComponent
}

var TextureGrid *common.Texture = nil

func NewGrid(pos engo.Point, rotate float32) *BasicElement {
	if TextureGrid == nil {
		TextureGrid, _ = common.LoadedSprite("grid.png")
	}

	return &BasicElement{
		BasicEntity:     ecs.NewBasic(),
		RenderComponent: &common.RenderComponent{Drawable: TextureGrid, StartZIndex: 2},
		SpaceComponent:  &common.SpaceComponent{Position: pos, Width: 100, Height: 100, Rotation: rotate},
	}
}

func NewSingleImageGrid(pos engo.Point) *BasicElement {
	if TextureGrid == nil {
		TextureGrid, _ = common.LoadedSprite("grid-full.png")
	}

	return &BasicElement{
		BasicEntity:     ecs.NewBasic(),
		RenderComponent: &common.RenderComponent{Drawable: TextureGrid, StartZIndex: 2},
		SpaceComponent:  &common.SpaceComponent{Position: pos, Width: 200, Height: 200},
	}
}

The assets is grid.png and grid-full.png.


The bug can be trigged by zoom-out. Also, I notice that:

  1. While you scroll the scene in diagonal, even if "zoom-outed", the grid/texture appears fine (or at least it's visible).
  2. While in Fullscreen, the issue is less noticeable.

If the grid becomes smaller than 1px seems reasonable that they will disappear, that is why I thought that it's expected behaviour. But, it doesn't seem the case.

Well, I have a guess. I'm trying to make it works, but my knowledge around OpenGL stuff is minimal.

The problem seems related to TEXTURE_MIN_FILTER. The TEXTURE_MIN_FILTER seems to define how the OpenGL handles when the image must be shrunk.

However, the OpenGL has no clue about what is the most critical content of the image. It makes the white border disappear.

Playing around TEXTURE_MIN_FILTER I found that question on StackOverflow. So, there's a value like GL_NEAREST_MIPMAP_*, however, it needs a MIPMAP. Researching about MIPMAP, I discovered that MIPMAP could "Instead of sampling a single texture, the application can be set up to switch between any of the lower resolution mipmaps in the chain depending on the distance from the camera.".

I don't know if it makes sense, but I'm trying to make it work.

Yes, I think it's the right direction.

image

Now the image has a diferent color, but never disappear. I change the TEXTURE_MIN_FILTER to GL_NEAREST_MIPMAP_NEAREST and change the BindTexture.

func (c *Context) BindTexture(target int, texture *Texture) {
	if texture == nil {
		gl.BindTexture(uint32(target), 0)
		return
	}
	gl.BindTexture(uint32(target), texture.uint32)
	gl.GenerateMipmap(uint32(target)) // << Here
}

I need to understand why it changes the color, seems to be related to the alpha, but I don't know how fix.

The last change, mentioned previously, didn't fix the issue. Instead, it makes the usage of GPU significant higher than before, from 4% to 21%, of GPU usage, using an AMD RX 5700XT.

Using gl.GenerateMipmap(uint32(target)) even without using the *_MIPMAP_* makes the usage higher.

Yeah, mipmaps are for when you have different quality textures based on distance, usually in a 3D settings. You can have detailed model textures when you're up close, and a flat png as a far away background so you don't have to, say, render a full mountain even though it's miles away.

I believe I can replicate the behavor, but I can't seem to get it to repeat like yours, where it stretches to fill the viewport, that looks like the render component's Repeat is set to ClampToEdge.

I would say that it's related to point number 5 here. It looks like if we switch between non-mipmapped filters to ones that are, we have to actually setup a mipmap (OpenGL can do this for us, just have to set it up so that it does).

The workaround that I found is using an "power of 2". The texture must be 128x128 (instead of 100x100). The zoom must be also power of 2, which means that the zoom is: 1, 2, 4, 8, 16, 32...

It's not a proper fix, but mitigates the issue for now. The current MouseZoomer didn't have any settings for that, like a Steps []float32, I create my own version of MouseZoomer with that option.

Futhermore, the moviment of the camera (about X and Y) must be "kinda power of 2". If you are in the position of: {X: 6, Y: 6, Z: 8} some lines will be smaller than others. However, if you are at {X: 4, Y: 4, Z: 8} it's good.

Saving textures as a power of two is how OpenGL does it anyway, if you save a 100x100 texture it'll take up the same amount of graphics memory as a 128x128 texture. We could simply put this into the stuff we use for UploadTexture; check the size of the texture and if it's not a power of two then we can create a new texture that is and copy the texture to the new one. Doing this would probably fix a lot of bugs like this.

Adding 'Steps []float32' to the all the camera systems in common sounds like a great idea to me. We could have a default value, say 2, for when it's not set. Limiting to a power of two sounds like a useful restriction all around, actually. OpenGL is built on 'next power of two' for a ton of things it does, so restricting camera movement to powers of two may actually make things run a lot smoother.