go.mobile: move glimage to glutil package and export
Drawing now supports defining both the source and destination bounds, which is equivalent to clipping and scaling an image. LGTM=nigeltao R=nigeltao, crawshaw CC=adg, golang-codereviews https://golang.org/cl/144480043
This commit is contained in:
Родитель
c2d8ff3468
Коммит
8a5f40b7b3
|
@ -17,6 +17,7 @@ import (
|
|||
|
||||
"code.google.com/p/freetype-go/freetype"
|
||||
"code.google.com/p/go.mobile/geom"
|
||||
"code.google.com/p/go.mobile/gl/glutil"
|
||||
)
|
||||
|
||||
var lastDraw = time.Now()
|
||||
|
@ -25,7 +26,7 @@ var monofont = freetype.NewContext()
|
|||
|
||||
var fps struct {
|
||||
sync.Once
|
||||
*rgba
|
||||
*glutil.Image
|
||||
}
|
||||
|
||||
// TODO(crawshaw): It looks like we need a gl.RegisterInit feature.
|
||||
|
@ -56,9 +57,9 @@ func fpsInit() {
|
|||
monofont.SetSrc(image.Black)
|
||||
monofont.SetHinting(freetype.FullHinting)
|
||||
|
||||
fps.rgba = newRGBA(geom.Point{50, 12})
|
||||
monofont.SetDst(fps.Image)
|
||||
monofont.SetClip(fps.Image.Bounds())
|
||||
fps.Image = glutil.NewImage(geom.Point{50, 12})
|
||||
monofont.SetDst(fps.Image.RGBA)
|
||||
monofont.SetClip(fps.Bounds())
|
||||
monofont.SetDPI(72 * float64(geom.Scale))
|
||||
monofont.SetFontSize(12)
|
||||
}
|
||||
|
@ -79,7 +80,10 @@ func DrawFPS() {
|
|||
}
|
||||
|
||||
fps.Upload()
|
||||
fps.Draw(geom.Point{0, geom.Height - 12})
|
||||
fps.Draw(
|
||||
geom.Rectangle{geom.Point{0, geom.Height - 12}, geom.Point{50, geom.Height}},
|
||||
fps.Bounds(),
|
||||
)
|
||||
|
||||
lastDraw = now
|
||||
}
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(crawshaw): A GL texture backed by an *image.RGBA is a common
|
||||
// concept we will want to export. Find a generally useful interface.
|
||||
// Think about the tricky things: mipmaps, a varying Rect, just backed
|
||||
// by texture, not 2D drawer, etc. Maybe skip all of these issues and
|
||||
// just solve the smaller problem: an unscaled sprite.
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"code.google.com/p/go.mobile/f32"
|
||||
"code.google.com/p/go.mobile/geom"
|
||||
"code.google.com/p/go.mobile/gl"
|
||||
"code.google.com/p/go.mobile/gl/glutil"
|
||||
)
|
||||
|
||||
var glimage struct {
|
||||
sync.Once
|
||||
square gl.Buffer
|
||||
squareUV gl.Buffer
|
||||
program gl.Program
|
||||
pos gl.Attrib
|
||||
mvp gl.Uniform
|
||||
inUV gl.Attrib
|
||||
textureSample gl.Uniform
|
||||
}
|
||||
|
||||
func glInit() {
|
||||
var err error
|
||||
glimage.program, err = glutil.CreateProgram(vertexShader, fragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
glimage.square = gl.GenBuffer()
|
||||
glimage.squareUV = gl.GenBuffer()
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareCoords)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareUVCoords)
|
||||
|
||||
glimage.pos = gl.GetAttribLocation(glimage.program, "pos")
|
||||
glimage.mvp = gl.GetUniformLocation(glimage.program, "mvp")
|
||||
glimage.inUV = gl.GetAttribLocation(glimage.program, "inUV")
|
||||
glimage.textureSample = gl.GetUniformLocation(glimage.program, "textureSample")
|
||||
}
|
||||
|
||||
type rgba struct {
|
||||
Image *image.RGBA
|
||||
Texture gl.Texture
|
||||
sizeX, sizeY float32
|
||||
}
|
||||
|
||||
func roundToPower2(x int) int {
|
||||
x2 := 1
|
||||
for x2 < x {
|
||||
x2 *= 2
|
||||
}
|
||||
return x2
|
||||
}
|
||||
|
||||
func newRGBA(size geom.Point) *rgba {
|
||||
dx := roundToPower2(int(size.X.Px()))
|
||||
dy := roundToPower2(int(size.Y.Px()))
|
||||
|
||||
imgRGBA := image.NewRGBA(image.Rect(0, 0, dx, dy))
|
||||
|
||||
glimage.Do(glInit)
|
||||
|
||||
w, h := imgRGBA.Rect.Dx(), imgRGBA.Rect.Dy()
|
||||
|
||||
img := &rgba{
|
||||
Image: imgRGBA, // TODO: embed?
|
||||
Texture: gl.GenTexture(),
|
||||
}
|
||||
runtime.SetFinalizer(img, func(img *rgba) {
|
||||
gl.DeleteTexture(img.Texture)
|
||||
})
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
img.sizeX = float32(size.X / geom.Width)
|
||||
img.sizeY = float32(size.Y / geom.Height)
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
func (img *rgba) Upload() {
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
w, h := img.Image.Rect.Dx(), img.Image.Rect.Dy()
|
||||
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, img.Image.Pix)
|
||||
}
|
||||
|
||||
// TODO(crawshaw): Clip/Scale options. Introduce geom.Rectangle?
|
||||
// TODO(crawshaw): Adjust viewport for the top bar on android?
|
||||
func (img *rgba) Draw(at geom.Point) {
|
||||
gl.UseProgram(glimage.program)
|
||||
|
||||
// Screen plane
|
||||
// (-1, 1) ( 1, 1)
|
||||
// (-1,-1) ( 1,-1)
|
||||
// Unscaled image corners
|
||||
// (1,0) (1,1)
|
||||
// (0,0) (0,1)
|
||||
// Orig size.X / geom.Width is a [0,1] fraction of screen.
|
||||
// The at position is referenced from the top of the image.
|
||||
var m f32.Mat4
|
||||
m.Identity()
|
||||
m.Translate(m, &f32.Vec3{
|
||||
-1 + float32(2*at.X/geom.Width),
|
||||
+1 - 2*img.sizeY - float32(2*at.Y/geom.Height),
|
||||
0,
|
||||
})
|
||||
m.Scale(&m, &f32.Vec3{
|
||||
2 * img.sizeX,
|
||||
2 * img.sizeY,
|
||||
1,
|
||||
})
|
||||
glimage.mvp.WriteMat4(&m)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.Uniform1i(glimage.textureSample, 0)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square)
|
||||
gl.EnableVertexAttribArray(glimage.pos)
|
||||
gl.VertexAttribPointer(glimage.pos, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV)
|
||||
gl.EnableVertexAttribArray(glimage.inUV)
|
||||
gl.VertexAttribPointer(glimage.inUV, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
gl.DrawArrays(gl.TRIANGLES, 0, 6)
|
||||
|
||||
gl.DisableVertexAttribArray(glimage.pos)
|
||||
gl.DisableVertexAttribArray(glimage.inUV)
|
||||
}
|
||||
|
||||
// Vertices of two triangles.
|
||||
var squareCoords = toBytes([]float32{
|
||||
0, 0,
|
||||
1, 0,
|
||||
1, 1,
|
||||
|
||||
0, 0,
|
||||
1, 1,
|
||||
0, 1,
|
||||
})
|
||||
|
||||
var squareUVCoords = toBytes([]float32{
|
||||
0, 1,
|
||||
1, 1,
|
||||
1, 0,
|
||||
|
||||
0, 1,
|
||||
1, 0,
|
||||
0, 0,
|
||||
})
|
||||
|
||||
func toBytes(v []float32) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.LittleEndian, v); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package glutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"log"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"code.google.com/p/go.mobile/f32"
|
||||
"code.google.com/p/go.mobile/geom"
|
||||
"code.google.com/p/go.mobile/gl"
|
||||
)
|
||||
|
||||
var glimage struct {
|
||||
sync.Once
|
||||
square gl.Buffer
|
||||
squareUV gl.Buffer
|
||||
program gl.Program
|
||||
pos gl.Attrib
|
||||
mvp gl.Uniform
|
||||
uvp gl.Uniform
|
||||
inUV gl.Attrib
|
||||
textureSample gl.Uniform
|
||||
}
|
||||
|
||||
func glInit() {
|
||||
var err error
|
||||
glimage.program, err = CreateProgram(vertexShader, fragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
glimage.square = gl.GenBuffer()
|
||||
glimage.squareUV = gl.GenBuffer()
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareCoords)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareUVCoords)
|
||||
|
||||
glimage.pos = gl.GetAttribLocation(glimage.program, "pos")
|
||||
glimage.mvp = gl.GetUniformLocation(glimage.program, "mvp")
|
||||
glimage.uvp = gl.GetUniformLocation(glimage.program, "uvp")
|
||||
glimage.inUV = gl.GetAttribLocation(glimage.program, "inUV")
|
||||
glimage.textureSample = gl.GetUniformLocation(glimage.program, "textureSample")
|
||||
}
|
||||
|
||||
// Image bridges between an *image.RGBA and an OpenGL texture.
|
||||
//
|
||||
// The contents of the embedded *image.RGBA can be uploaded as a
|
||||
// texture and drawn as a 2D quad.
|
||||
//
|
||||
// The number of active Images must fit in the system's OpenGL texture
|
||||
// limit. The typical use of an Image is as a texture atlas.
|
||||
type Image struct {
|
||||
*image.RGBA
|
||||
|
||||
Texture gl.Texture
|
||||
texWidth int
|
||||
texHeight int
|
||||
}
|
||||
|
||||
// NewImage creates an Image of the given size.
|
||||
//
|
||||
// Both a host-memory *image.RGBA and a GL texture are created.
|
||||
func NewImage(size geom.Point) *Image {
|
||||
realx := int(math.Ceil(float64(size.X.Px())))
|
||||
realy := int(math.Ceil(float64(size.Y.Px())))
|
||||
dx := roundToPower2(realx)
|
||||
dy := roundToPower2(realy)
|
||||
|
||||
// TODO(crawshaw): Using VertexAttribPointer we can pass texture
|
||||
// data with a stride, which would let us use the exact number of
|
||||
// pixels on the host instead of the rounded up power 2 size.
|
||||
m := image.NewRGBA(image.Rect(0, 0, dx, dy))
|
||||
|
||||
glimage.Do(glInit)
|
||||
|
||||
img := &Image{
|
||||
RGBA: m.SubImage(image.Rect(0, 0, realx, realy)).(*image.RGBA),
|
||||
Texture: gl.GenTexture(),
|
||||
texWidth: dx,
|
||||
texHeight: dy,
|
||||
}
|
||||
// TODO(crawshaw): We don't have the context on a finalizer. Find a way.
|
||||
// runtime.SetFinalizer(img, func(img *Image) { gl.DeleteTexture(img.Texture) })
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, dx, dy, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
func roundToPower2(x int) int {
|
||||
x2 := 1
|
||||
for x2 < x {
|
||||
x2 *= 2
|
||||
}
|
||||
return x2
|
||||
}
|
||||
|
||||
// Upload copies the host image data to the GL device.
|
||||
func (img *Image) Upload() {
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, img.texWidth, img.texHeight, gl.RGBA, gl.UNSIGNED_BYTE, img.Pix)
|
||||
}
|
||||
|
||||
// Draw draws the image onto the current GL framebuffer.
|
||||
func (img *Image) Draw(dstBounds geom.Rectangle, srcBounds image.Rectangle) {
|
||||
// TODO(crawshaw): Adjust viewport for the top bar on android?
|
||||
gl.UseProgram(glimage.program)
|
||||
|
||||
// We are drawing a sub-image of dst, defined by dstBounds. Let ABCD
|
||||
// be the image, and PQRS be the sub-image. The two images may actually
|
||||
// be equal, but in the general case, PQRS can be smaller:
|
||||
//
|
||||
// M
|
||||
// A +----+----------+ B
|
||||
// | |
|
||||
// N + P +-----+ Q |
|
||||
// | | | |
|
||||
// | S +-----+ R |
|
||||
// | |
|
||||
// D +---------------+ C
|
||||
//
|
||||
// There are two co-ordinate spaces: geom space and framebuffer space.
|
||||
// In geom space, the ABCD rectangle is:
|
||||
//
|
||||
// (0, 0) (geom.Width, 0)
|
||||
// (0, geom.Height) (geom.Width, geom.Height)
|
||||
//
|
||||
// and the PQRS rectangle is:
|
||||
//
|
||||
// (dstBounds.Min.X, dstBounds.Min.Y) (dstBounds.Max.X, dstBounds.Min.Y)
|
||||
// (dstBounds.Min.X, dstBounds.Max.Y) (dstBounds.Max.X, dstBounds.Max.Y)
|
||||
//
|
||||
// In framebuffer space, the ABCD rectangle is:
|
||||
//
|
||||
// (-1, +1) (+1, +1)
|
||||
// (-1, -1) (+1, -1)
|
||||
//
|
||||
// We need to solve for PQRS' co-ordinates in framebuffer space, and
|
||||
// calculate the MVP matrix that transforms the -1/+1 ABCD co-ordinates
|
||||
// to PQRS co-ordinates.
|
||||
//
|
||||
// To solve for PQRS, note that PQ / AB must match in both spaces. Call
|
||||
// this ratio fracX, and likewise for fracY.
|
||||
//
|
||||
// [EQ1] fracX = (dstBounds.Max.X - dstBounds.Min.X) / geom.Width
|
||||
//
|
||||
// Similarly, the AM / AB ratio must match:
|
||||
//
|
||||
// (P.x - -1) / (1 - -1) = dstBounds.Min.X / geom.Width
|
||||
//
|
||||
// where the LHS is in framebuffer space and the RHS in geom space. This
|
||||
// equation is equivalent to:
|
||||
//
|
||||
// [EQ2] P.x = -1 + 2 * dstBounds.Min.X / geom.Width
|
||||
//
|
||||
// This MVP matrix is a scale followed by a translate. The scale is by
|
||||
// (fracX, fracY). After this, our corners have been transformed to:
|
||||
//
|
||||
// (-fracX, +fracY) (+fracX, +fracY)
|
||||
// (-fracX, -fracY) (+fracX, -fracY)
|
||||
//
|
||||
// so the translate is by (P.x + fracX) in the X direction, and
|
||||
// likewise for Y. Combining equations EQ1 and EQ2 simplifies the
|
||||
// translate to be:
|
||||
//
|
||||
// -1 + (dstBounds.Max.X + dstBounds.Min.X) / geom.Width
|
||||
// +1 - (dstBounds.Max.Y + dstBounds.Min.Y) / geom.Height
|
||||
var a f32.Affine
|
||||
a.Identity()
|
||||
a.Translate(
|
||||
&a,
|
||||
-1+float32((dstBounds.Max.X+dstBounds.Min.X)/geom.Width),
|
||||
+1-float32((dstBounds.Max.Y+dstBounds.Min.Y)/geom.Height),
|
||||
)
|
||||
a.Scale(
|
||||
&a,
|
||||
float32((dstBounds.Max.X-dstBounds.Min.X)/geom.Width),
|
||||
float32((dstBounds.Max.Y-dstBounds.Min.Y)/geom.Height),
|
||||
)
|
||||
glimage.mvp.WriteAffine(&a)
|
||||
|
||||
// Texture UV co-ordinates start out as:
|
||||
//
|
||||
// (0,0) (1,0)
|
||||
// (0,1) (1,1)
|
||||
//
|
||||
// These co-ordinates need to be scaled to texWidth/Height,
|
||||
// which may be less than 1 as the source image may not have
|
||||
// power-of-2 dimensions. Then it is scaled and translated
|
||||
// to represent the srcBounds rectangle of the source texture.
|
||||
//
|
||||
// The math is simpler here because in both co-ordinate spaces,
|
||||
// the top-left corner is (0, 0).
|
||||
a.Identity()
|
||||
a.Translate(
|
||||
&a,
|
||||
float32(srcBounds.Min.X)/float32(img.Rect.Dx()),
|
||||
float32(srcBounds.Min.Y)/float32(img.Rect.Dy()),
|
||||
)
|
||||
a.Scale(
|
||||
&a,
|
||||
float32(srcBounds.Dx())/float32(img.texWidth),
|
||||
float32(srcBounds.Dy())/float32(img.texHeight),
|
||||
)
|
||||
glimage.uvp.WriteAffine(&a)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, img.Texture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.Uniform1i(glimage.textureSample, 0)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square)
|
||||
gl.EnableVertexAttribArray(glimage.pos)
|
||||
gl.VertexAttribPointer(glimage.pos, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV)
|
||||
gl.EnableVertexAttribArray(glimage.inUV)
|
||||
gl.VertexAttribPointer(glimage.inUV, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
gl.DrawArrays(gl.TRIANGLES, 0, 6)
|
||||
|
||||
gl.DisableVertexAttribArray(glimage.pos)
|
||||
gl.DisableVertexAttribArray(glimage.inUV)
|
||||
}
|
||||
|
||||
// Vertices of two triangles.
|
||||
var squareCoords = toBytes([]float32{
|
||||
-1, -1,
|
||||
+1, -1,
|
||||
+1, +1,
|
||||
|
||||
-1, -1,
|
||||
+1, +1,
|
||||
-1, +1,
|
||||
})
|
||||
|
||||
var squareUVCoords = toBytes([]float32{
|
||||
0, 1,
|
||||
1, 1,
|
||||
1, 0,
|
||||
|
||||
0, 1,
|
||||
1, 0,
|
||||
0, 0,
|
||||
})
|
||||
|
||||
func toBytes(v []float32) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.LittleEndian, v); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
|
@ -2,22 +2,23 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build android
|
||||
package glutil
|
||||
|
||||
package debug
|
||||
|
||||
const vertexShader = `
|
||||
uniform mat4 mvp;
|
||||
attribute vec4 pos;
|
||||
const vertexShader = `#version 100
|
||||
uniform mat3 mvp;
|
||||
uniform mat3 uvp;
|
||||
attribute vec3 pos;
|
||||
attribute vec2 inUV;
|
||||
varying vec2 UV;
|
||||
void main() {
|
||||
gl_Position = mvp * pos;
|
||||
UV = inUV;
|
||||
vec3 p = pos;
|
||||
p.z = 1.0;
|
||||
gl_Position = vec4(mvp * p, 1);
|
||||
UV = (uvp * vec3(inUV, 1)).xy;
|
||||
}
|
||||
`
|
||||
|
||||
const fragmentShader = `
|
||||
const fragmentShader = `#version 100
|
||||
precision mediump float;
|
||||
varying vec2 UV;
|
||||
uniform sampler2D textureSample;
|
Загрузка…
Ссылка в новой задаче