`

OpenGL ES与libgdx学习笔记一:二维坐标系方向变换

 
阅读更多

二维坐标系变换为原点在左上角(测试用)

 

* GLES

* JOGL

* LWJGL

* libgdx(使用g2d与pixmap)

 

 

package com.iteye.weimingtom.testgl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class Testgl3Activity extends Activity implements GLSurfaceView.Renderer {
	private boolean USE_ORIGIN_TOPLEFT = true;
	
	private IntBuffer vertices;
	private int vertexBytes;
	private int vertexNum;
	
	private float[] arrVertices;
	private int[] tmpBuffer;
	
	private GLSurfaceView contentView;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                             WindowManager.LayoutParams.FLAG_FULLSCREEN);
        contentView = new GLSurfaceView(this);
        contentView.setRenderer(this);
        setContentView(contentView);
	}

	@Override
	protected void onPause() {
		super.onPause();
		contentView.onPause();
	}

	@Override
	protected void onResume() {
		super.onResume();
		contentView.onResume();
	}

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		loadData();
        gl.glDisable(GL10.GL_DITHER);
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
        gl.glClearColor(1, 1, 1, 1);
        gl.glDisable(GL10.GL_CULL_FACE);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glEnable(GL10.GL_DEPTH_TEST);
	}
	
	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
	    gl.glViewport(0, 0, width, height);
	    //gl.glViewport(0, height, width, height);
	    if (USE_ORIGIN_TOPLEFT) {
	    	gl.glOrthof(0, width, -height, 0, 0, 1);
	    } else {
	    	gl.glOrthof(0, width, 0, height, 0, 1);
	    }
        updateData(width, height);
	}

	@Override
	public void onDrawFrame(GL10 gl) {
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        if (USE_ORIGIN_TOPLEFT) {
        	gl.glScalef(1f, -1f, 1f);
        }
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        
        vertices.position(0);
        gl.glVertexPointer(3, GL10.GL_FLOAT, vertexBytes, vertices);
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
        vertices.position(3);
        gl.glColorPointer(4, GL10.GL_FLOAT, vertexBytes, vertices);
        gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, vertexNum);
		
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	}
	
	private void loadData() {
		final float factor = 200f / 320f * 100;
        this.arrVertices = new float[] { 
        		0f * factor, 0f * factor, 0, 0, 0, 1, 1,
        		1f * factor, 0f * factor,   0, 0, 0, 1, 1,
        		1f * factor, 1f * factor, 0, 0, 0, 1, 1,
                0f * factor,  1f * factor,  0, 0, 0, 1, 1,
        };
        this.vertexBytes = (3 + 4) * (Integer.SIZE / 8);
        this.vertexNum = arrVertices.length / (this.vertexBytes / (Integer.SIZE / 8));
        this.tmpBuffer = new int[vertexNum * vertexBytes / (Integer.SIZE / 8)];
        ByteBuffer buffer = ByteBuffer.allocateDirect(vertexNum * vertexBytes);
        buffer.order(ByteOrder.nativeOrder());
        vertices = buffer.asIntBuffer();
		this.vertices.clear();
        for (int i = 0, j = 0; i < arrVertices.length; i++, j++) {
            tmpBuffer[j] = Float.floatToRawIntBits(arrVertices[i]);
        }
        this.vertices.put(tmpBuffer, 0, tmpBuffer.length);
        this.vertices.flip();
	}
	
	private void updateData(int width, int height) {
		arrVertices[0] = 100f;
		arrVertices[1] = 100f;
		arrVertices[0 + 7] = width - 10;
		arrVertices[1 + 7] = 0;
		arrVertices[0 + 14] = width - 10;
		arrVertices[1 + 14] = height - 10;		
		arrVertices[0 + 21] = 0;
		arrVertices[1 + 21] = height - 10;	
		this.vertices.clear();
        for (int i = 0, j = 0; i < arrVertices.length; i++, j++) {
            tmpBuffer[j] = Float.floatToRawIntBits(arrVertices[i]);
        }
        this.vertices.put(tmpBuffer, 0, tmpBuffer.length);
        this.vertices.flip();
	}
}

 

 

 

JOGL:

 

 

 

 

import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.media.opengl.GL2ES1;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;

import com.jogamp.opengl.util.FPSAnimator;

/**
 * gluegen-rt-natives-windows-i586.jar
 * gluegen-rt.jar
 * gluegen.jar
 * jogl-all-natives-windows-i586.jar
 * jogl.all.jar
 * 
 * @see sites.google.com/site/justinscsstuff/jogl-tutorial-3
 * @author Administrator
 *
 */
public class TestGLES1 implements GLEventListener {
	private boolean USE_ORIGIN_TOPLEFT = true;
	
	private IntBuffer vertices;
	private int vertexBytes;
	private int vertexNum;
	
	private float[] arrVertices;
	private int[] tmpBuffer;
	
	@Override
	public void init(GLAutoDrawable drawable) {
		GL2ES1 gl = drawable.getGL().getGL2ES1();
		loadData();
        gl.glDisable(GL2ES1.GL_DITHER);
        gl.glHint(GL2ES1.GL_PERSPECTIVE_CORRECTION_HINT, GL2ES1.GL_FASTEST);
        gl.glClearColor(1, 1, 1, 1);
        gl.glDisable(GL2ES1.GL_CULL_FACE);
        gl.glShadeModel(GL2ES1.GL_SMOOTH);
        gl.glEnable(GL2ES1.GL_DEPTH_TEST);
	}

	@Override
	public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
		GL2ES1 gl = drawable.getGL().getGL2ES1();
		gl.glMatrixMode(GL2ES1.GL_PROJECTION);
        gl.glLoadIdentity();
	    gl.glViewport(0, 0, w, h);
	    //gl.glViewport(0, height, width, height);
	    if (USE_ORIGIN_TOPLEFT) {
	    	gl.glOrthof(0, w, -h, 0, 0, 1);
	    } else {
	    	gl.glOrthof(0, w, 0, h, 0, 1);
	    }
	    updateData(w, h);
	}
	
	@Override
	public void display(GLAutoDrawable drawable) {
		GL2ES1 gl = drawable.getGL().getGL2ES1();
        gl.glClear(GL2ES1.GL_COLOR_BUFFER_BIT | GL2ES1.GL_DEPTH_BUFFER_BIT);
        gl.glMatrixMode(GL2ES1.GL_MODELVIEW);
        gl.glLoadIdentity();
        if (USE_ORIGIN_TOPLEFT) {
        	gl.glScalef(1f, -1f, 1f);
        }
        gl.glEnableClientState(GL2ES1.GL_VERTEX_ARRAY);
        
        vertices.position(0);
        gl.glVertexPointer(3, GL2ES1.GL_FLOAT, vertexBytes, vertices);
        gl.glEnableClientState(GL2ES1.GL_COLOR_ARRAY);
        vertices.position(3);
        gl.glColorPointer(4, GL2ES1.GL_FLOAT, vertexBytes, vertices);
        gl.glDrawArrays(GL2ES1.GL_TRIANGLE_FAN, 0, vertexNum);
		
        gl.glDisableClientState(GL2ES1.GL_COLOR_ARRAY);
        gl.glDisableClientState(GL2ES1.GL_VERTEX_ARRAY);
	}

	@Override
	public void dispose(GLAutoDrawable drawable) {
		
	}
		
	private void loadData() {
		final float factor = 200f / 320f * 100;
        this.arrVertices = new float[] { 
        		0f * factor, 0f * factor, 0, 0, 0, 1, 1,
        		1f * factor, 0f * factor,   0, 0, 0, 1, 1,
        		1f * factor, 1f * factor, 0, 0, 0, 1, 1,
                0f * factor,  1f * factor,  0, 0, 0, 1, 1,
        };
        this.vertexBytes = (3 + 4) * (Integer.SIZE / 8);
        this.vertexNum = arrVertices.length / (this.vertexBytes / (Integer.SIZE / 8));
        this.tmpBuffer = new int[vertexNum * vertexBytes / (Integer.SIZE / 8)];
        ByteBuffer buffer = ByteBuffer.allocateDirect(vertexNum * vertexBytes);
        buffer.order(ByteOrder.nativeOrder());
        vertices = buffer.asIntBuffer();
		this.vertices.clear();
        for (int i = 0, j = 0; i < arrVertices.length; i++, j++) {
            tmpBuffer[j] = Float.floatToRawIntBits(arrVertices[i]);
        }
        this.vertices.put(tmpBuffer, 0, tmpBuffer.length);
        this.vertices.flip();
	}
	
	private void updateData(int width, int height) {
		arrVertices[0] = 100f;
		arrVertices[1] = 100f;
		arrVertices[0 + 7] = width - 10;
		arrVertices[1 + 7] = 0;
		arrVertices[0 + 14] = width - 10;
		arrVertices[1 + 14] = height - 10;		
		arrVertices[0 + 21] = 0;
		arrVertices[1 + 21] = height - 10;	
		this.vertices.clear();
        for (int i = 0, j = 0; i < arrVertices.length; i++, j++) {
            tmpBuffer[j] = Float.floatToRawIntBits(arrVertices[i]);
        }
        this.vertices.put(tmpBuffer, 0, tmpBuffer.length);
        this.vertices.flip();
	}
	
	public static void main(String[] args) {
        GLProfile glp = GLProfile.getDefault();
        GLCapabilities caps = new GLCapabilities(glp);
        GLCanvas canvas = new GLCanvas(caps);

        Frame frame = new Frame("AWT Window Test");
        frame.setSize(240, 320);
		frame.setResizable(false);
		frame.setLocationRelativeTo(null);
        frame.add(canvas);
        frame.setVisible(true);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        canvas.addGLEventListener(new TestGLES1());
        
	    FPSAnimator animator = new FPSAnimator(canvas, 60);
	    animator.add(canvas);
	    animator.start();
	}
}

 

LWJGL:

 

 

 

import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;

/**
 * lwjgl.jar : native library location -> testlwjgl/native/windows
 * 
 * @see http://lwjgl.org/wiki/index.php?title=LWJGL_Basics_1_(The_Display)
 * @see http://lwjgl.org/wiki/index.php?title=LWJGL_Basics_3_(The_Quad)
 * @author Administrator
 *
 */
public class Testlwjgl1 {
	private boolean USE_ORIGIN_TOPLEFT = true;
	
	private final static int SCREEN_WIDTH = 240;
	private final static int SCREEN_HEIGHT = 320;
	
	public void start() {
		try {
			Display.setDisplayMode(new DisplayMode(
					SCREEN_WIDTH, SCREEN_HEIGHT));
			Display.create();
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(0);
		}
		
		init();
		
		while (!Display.isCloseRequested()) {
			render();
			Display.update();
		}
		Display.destroy();
	}
	
	private void init() {
		GL11.glMatrixMode(GL11.GL_PROJECTION);
		GL11.glLoadIdentity();
		if (USE_ORIGIN_TOPLEFT) {
			GL11.glOrtho(0, SCREEN_WIDTH, 
					-SCREEN_HEIGHT, 0, 
					1, -1);			
		} else {
			GL11.glOrtho(0, SCREEN_WIDTH, 
					0, SCREEN_HEIGHT, 
					1, -1);
		}
	}
	
	private void render() {
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		GL11.glLoadIdentity();
		if (USE_ORIGIN_TOPLEFT) {
			GL11.glScalef(1f, -1f, 1f);
		}
		
		GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | 
	    		GL11.GL_DEPTH_BUFFER_BIT);	
		GL11.glClearColor(1, 1, 1, 1);
		
		GL11.glColor3f(0.0f, 0.0f, 1.0f);
	    GL11.glBegin(GL11.GL_TRIANGLE_FAN);
	    {
		    GL11.glVertex2f(100, 100);
			GL11.glVertex2f(SCREEN_WIDTH - 10, 0);
			GL11.glVertex2f(SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10);
			GL11.glVertex2f(0, SCREEN_HEIGHT - 10);
	    }
	    GL11.glEnd();
	}
	
	public static void main(String[] args) {
		new Testlwjgl1().start();
	}
}

 

 

libgdx (使用g2d与pixmap,而非使用GLES绘画)

 

package com.iteye.weimingtom.libgdxtest;

import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;

public class Test001Screen implements Screen {
	private final static boolean DIRECTION_DOWN = true;
	private final static int BOX_W = 50;
	private final static int BOX_H = 50;
	
	private int LOG_LEVEL = Application.LOG_DEBUG;
	private final static String TAG = "Test001Screen"; 
	private FPSLogger logger;
	
	private SpriteBatch sb;
	private Pixmap pixmap;
	private Texture texture;
	private TextureRegion textureRegion;
	private Vector2 pos;
	private Vector2 dir;

	public Test001Screen() {
		Gdx.app.setLogLevel(LOG_LEVEL);
		logger = new FPSLogger();
		init();
	}
	
	private void init() {
		log("init");
		sb = new SpriteBatch();
		int w = Gdx.graphics.getWidth();
		int h = Gdx.graphics.getHeight();
		pixmap = new Pixmap(w, h, Pixmap.Format.RGBA8888);
		final int potW = MathUtils.nextPowerOfTwo(w);
		final int potH = MathUtils.nextPowerOfTwo(h);
		texture = new Texture(potW, potH, Pixmap.Format.RGBA8888);
		if (DIRECTION_DOWN) {
			textureRegion = new TextureRegion(texture, 0, 0, w, h);
		} else {
			textureRegion = new TextureRegion(texture, 0, h, w, -h);
		}
		pos = new Vector2(w / 2, h / 2);
		dir = new Vector2(1, 1);
	}
	
	@Override
	public void dispose() {
		log("dispose");
		texture.dispose();
		pixmap.dispose();
		sb.dispose();
	}
	
	@Override
	public void render(float delta) {
		onUpdate(delta);
		GL10 gl = Gdx.gl10;
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		gl.glClearColor(0, 0, 0, 0);
		//gl.glClearColor(1, 1, 1, 1);
		onRender();
		sb.begin();
		texture.draw(pixmap, 0, 0);
		sb.draw(textureRegion, 0, 0);
		sb.end();
		logger.log();
	}
	
	@Override
	public void resize(int width, int height) {
		sb.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
	}

	@Override
	public void show() {
		
	}

	@Override
	public void hide() {
		
	}

	@Override
	public void pause() {
		
	}

	@Override
	public void resume() {
		
	}
	
	private void onUpdate(float delta) {
		int w = pixmap.getWidth();
		int h = pixmap.getHeight();
		
		pos.x += dir.x * delta * 60;
		pos.y += dir.y * delta * 60;
		if (pos.x < 0) {
			dir.x = -dir.x;
			pos.x = 0;
		}
		if (pos.x > w) {
			dir.x = -dir.x;
			pos.x = w;
		}
		if (pos.y < 0) {
			dir.y = -dir.y;
			pos.y = 0;
		}
		if (pos.y > h) {
			dir.y = -dir.y;
			pos.y = h;
		}
	}
	
	private void onRender() {
		pixmap.setColor(0, 0, 0, 0);
		pixmap.fill();
		pixmap.setColor(Color.BLUE);
		pixmap.fillRectangle(
			(int)pos.x - BOX_W / 2, (int)pos.y - BOX_H / 2, 
			BOX_W, BOX_H);
	}
	
	private void log(String message) {
		Gdx.app.log(TAG, message);
	}
}

 

 

 

 

 

 

 

X. 参考资料:

1. Beginning Android 4 Games Development

随书代码Source Code/Downloads

http://www.apress.com/9781430239871

 

 

(TODO)

 

 

 

 

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

    Libgdx入门-代码1

    Libgdx是一款基于OpenGL ES技术开发的Android游戏引擎,支持Android平台下的2D和3D游戏开发,2D物理引擎采用Box2D实现。单就性能角度来说,堪称是一款非常强大的Android游戏引擎,但缺陷在于精灵类等相关组件在使用...

    Libgdx入门-code

    Libgdx是一款基于OpenGL ES技术开发的Android游戏引擎,支持Android平台下的2D和3D游戏开发,2D物理引擎采用Box2D实现。单就性能角度来说,堪称是一款非常强大的Android游戏引擎,但缺陷在于精灵类等相关组件在使用...

    Libgdx入门-代码2

    Libgdx是一款基于OpenGL ES技术开发的Android游戏引擎,支持Android平台下的2D和3D游戏开发,2D物理引擎采用Box2D实现。单就性能角度来说,堪称是一款非常强大的Android游戏引擎,但缺陷在于精灵类等相关组件在使用...

    libgdx学习资料

    LibGDX是一个强大的开源Java框架,专为跨平台游戏开发设计。它允许开发者使用单一代码库创建游戏,这些游戏可以在Android、iOS、HTML5、Windows、Linux和Mac等多个平台上运行。这个“libgdx学习资料”压缩包包含了...

    libgdx学习文档 DOC格式

    Libgdx是一款强大的开源游戏开发框架,它支持2D和3D游戏的开发,并且跨平台,可以在JavaSE环境(包括Mac、Linux、Windows等操作系统)以及Android平台上运行。其核心功能由audio、files、graphics、math、physics、...

    libGDX学习记录(一)源码

    libGDX学习记录(一)源码,搭建一个桌面端和android端的libGDX框架,展示图片。 详细地址:https://blog.csdn.net/weixin_47450795/article/details/110003413

    libgdx学习文档(比较全面)

    ### libgdx学习文档知识点详解 #### 一、Libgdx概述 Libgdx是一款功能强大的跨平台游戏开发框架,支持2D与3D游戏的开发。它旨在为开发者提供一套简洁高效的API来构建高性能的游戏应用。Libgdx不仅可以在Windows、...

    libgdx学习文档

    ### libgdx学习文档知识点详解 #### 一、libgdx概述 libgdx是一款功能强大的跨平台游戏开发框架,支持2D与3D游戏的创建。它旨在为开发者提供一套全面的API,覆盖从图形渲染到物理模拟的广泛领域。libgdx的亮点在于...

    LibGDX Lua Tutorial工程

    LibGDX是一个强大的开源游戏开发框架,它支持跨平台的游戏开发,包括Android、iOS、桌面系统(Windows、MacOS、Linux)以及Web浏览器。在这个"LibGDX Lua Tutorial工程"中,开发者可以学习如何利用LibGDX框架结合Lua...

    Libgdx专题系列第一篇 第一节

    Libgdx是一个强大的开源游戏开发框架,专为创建跨平台的游戏而设计。它支持Windows、Linux、MacOS、Android以及HTML5等平台,让开发者能够用Java语言编写一次代码,到处运行。本专题系列将深入探讨Libgdx的使用方法...

    libGDX学习记录(二)阶段源码

    libGDX学习记录(二)阶段源码 展示TexturePacker合成的图片,详细地址: https://blog.csdn.net/weixin_47450795/article/details/110037945

    Libgdx开发丛书之 Learning LibGDX Game Development, 2nd Edition

    1. **图形渲染**:LibGDX提供了一个基于OpenGL ES 2.0的图形后端,允许开发者创建高性能的2D和3D游戏。它包括纹理处理、着色器编程以及模型加载等功能。 2. **音频处理**:LibGDX提供了对音频文件的全面支持,包括...

    Libgdx开源游戏 【蚂蚁回家】libgdx实现

    3. **图形渲染**:Libgdx使用LWJGL( Lightweight Java Game Library )作为底层图形API,支持OpenGL ES 2.0,提供了纹理、精灵、批次渲染等功能,使开发者能高效地绘制2D和3D图形。 4. **音频处理**:Libgdx提供了...

    Libgdx-Box2D:方块2D

    Libgdx是一个强大的开源游戏开发框架,主要用于创建跨平台的游戏。Box2D是它的一个重要组件,是一个物理模拟库,专用于2D物理效果。在本文中,我们将深入探讨Libgdx结合Box2D如何帮助开发者构建具有真实感的2D物理...

    Libgdx专题系列 第一篇 第七节

    Libgdx是一个强大的开源游戏开发框架,专为创建跨平台的游戏而设计。它支持Windows、Linux、MacOS、Android以及HTML5等平台,使开发者能够编写一次代码,到处运行。在"Libgdx专题系列 第一篇 第七节"中,我们将深入...

    libgdx游戏引擎开发指南

    不过,该书可能是学习libgdx的最佳资源之一,特别是对于那些希望深入探索该框架的开发者。 9. 注意事项: 在实践中使用本书提供的信息和代码时,开发者应当留意书中明确提到的免责声明,即书中信息及代码使用风险...

    libgdx AndroidApplicationConfiguration

    2. **useGL30**:这是一个布尔值,用于决定是否使用OpenGL ES 3.0。如果设备支持且你需要利用高级图形特性,可以设置为`true`。否则,将使用更兼容的OpenGL ES 2.0。 3. **renderingBackend**:允许你选择渲染后端...

    Libgdx专题系列 UI篇

    Libgdx是一个强大的开源游戏开发框架,用于创建跨平台的游戏。在这个"Libgdx专题系列 UI篇"中,我们将深入探讨如何使用Libgdx构建用户界面(UI),特别是TWL库和TableLayout的运用。 首先,让我们理解TWL库。TWL...

    libgdx1.6.1.rar

    2. **图形渲染**:LibGDX提供了一个强大的图形API,基于OpenGL ES,支持2D和3D图形的绘制。开发者可以利用这个功能创建复杂的游戏场景和动画。 3. **音频处理**:框架内置了音频管理,支持播放、暂停、停止和循环...

Global site tag (gtag.js) - Google Analytics