【Android】RuntimeShader 应用
1 简介
RuntimeShader 是 Android 13(T)中新增的特性,用于逐像素渲染界面,它使用 AGSL(Android Graphics Shading Language)编写着色器代码,底层基于 Skia 图形渲染引擎。官方介绍详见 → RuntimeShader。
相较于 OpenGL ES,RuntimeShader 具有以下特点。
- RuntimeShader 中只有片元着色器,没有顶点着色器。
- RuntimeShader 中用户不用输入顶点数据,简化了输入操作。
- RuntimeShader 基于 AGSL 语言,OpenGL ES 基于 GLSL 语言。
- AGSL 中纹理坐标值域与 View 的宽高对应,GLSL 中纹理坐标一般归一化了。
2 对 View 进行二次渲染
MainActivity.java
package com.zhyan8.shaderdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.RenderEffect;
import android.graphics.RuntimeShader;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import com.zhyan8.shaderdemo.utils.ScreenUtils;
import com.zhyan8.shaderdemo.utils.StringUtils;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LinearLayout parentView = findViewById(R.id.parent);
float[] resolution = ScreenUtils.getScreenSizeF(this);
applyRuntimeShader(parentView, resolution);
}
private void applyRuntimeShader(View view, float[] resolution) {
String shaderCode = StringUtils.loadString(this, "shaders/dazzling.agsl");
RuntimeShader shader = new RuntimeShader(shaderCode);
shader.setFloatUniform("u_resolution", resolution);
RenderEffect effect = RenderEffect.createRuntimeShaderEffect(shader, "u_texture");
view.setRenderEffect(effect);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:gravity="center"
android:background="#FFFFFF"
android:id="@+id/parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:textSize="60sp"
android:textColor="#669933"/>
</LinearLayout>
dazzling.agsl
uniform shader u_texture;
uniform vec2 u_resolution;
vec4 main(vec2 coords) {
vec4 tex = u_texture.eval(coords);
vec2 normUV = coords / u_resolution;
vec3 color = tex.rgb * vec3(normUV.x, normUV.y, 0.5);
return vec4(color, 1.0);
}
说明:coords 的值域与 View 的宽高对应,并不是归一化的坐标。
运行效果如下。
3 通过 Canvas 进行渲染
3.1 简单应用
MainActivity.java
package com.zhyan8.shaderdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.LinearLayout;
import com.zhyan8.shaderdemo.graphics.ShaderView;
public class MainActivity extends AppCompatActivity {
private ShaderView mShaderView;
private LinearLayout mParentView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mParentView = findViewById(R.id.parent);
addView();
MyRenderer renderer = new MyRenderer(this);
mShaderView.setRenderer(renderer);
mShaderView.requestRender(true);
}
private void addView() {
mShaderView = new ShaderView(this);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
mParentView.addView(mShaderView, lp);
}
}
ShaderView.java
package com.zhyan8.shaderdemo.graphics;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RuntimeShader;
import android.os.Handler;
import android.os.Looper;
import android.view.Choreographer;
import android.view.View;
/**
* 自定义view, 承载渲染环境作用, 类比GLSurfaceView
* @author little fat sheep
*/
public class ShaderView extends View {
private Paint mPaint = new Paint();
private Renderer mRenderer;
private float[] mResolution;
private long mStartTime = 0L;
private long mRunTime = 0L;
private Choreographer mChoreographer;
private Handler mHandler;
public ShaderView(Context context) {
super(context);
mChoreographer = Choreographer.getInstance();
mHandler = new Handler(Looper.getMainLooper());
}
public void setRenderer(Renderer renderer) {
this.mRenderer = renderer;
RuntimeShader shader = renderer.onSurfaceCreated();
mPaint.setShader(shader);
mStartTime = System.currentTimeMillis();
}
public void requestRender() {
invalidate();
}
public void requestRender(long duration) {
mHandler.post(() -> {
mChoreographer.postFrameCallback(mFrameCallback);
});
mHandler.postDelayed(() -> {
mChoreographer.removeFrameCallback(mFrameCallback);
}, duration);
}
public void requestRender(boolean continuous) {
if (continuous) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
invalidate();
}
}
public void stopRenderer() {
mChoreographer.removeFrameCallback(mFrameCallback);
}
public void stopRenderer(long delay) {
mHandler.postDelayed(() -> {
mChoreographer.removeFrameCallback(mFrameCallback);
}, delay);
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mResolution = new float[] { w, h };
mRenderer.onSurfaceChanged(w, h);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
mRunTime = System.currentTimeMillis() - mStartTime;
mRenderer.onDrawFrame(mRunTime);
canvas.drawRect(0f, 0f, mResolution[0], mResolution[1], mPaint);
}
private Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mChoreographer.postFrameCallback(this);
ShaderView.this.invalidate();
}
};
/**
* 渲染器接口, 类比GLSurfaceView.Renderer
* @author little fat sheep
*/
public interface Renderer {
RuntimeShader onSurfaceCreated();
void onSurfaceChanged(int width, int height);
void onDrawFrame(long runTime);
}
}
BitmapTexture.java
package com.zhyan8.shaderdemo.graphics;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.RuntimeShader;
import android.graphics.Shader;
import com.zhyan8.shaderdemo.utils.BitmapUtils;
/**
* Bitmap纹理
* @author little fat sheep
*/
public class BitmapTexture {
private BitmapShader mBitmapShader;
private float[] mSize;
private BitmapTexture(BitmapShader bitmapShader, float[] size) {
this.mBitmapShader = bitmapShader;
this.mSize = size;
}
public static BitmapTexture create(Context context, String assetPath) {
Bitmap bitmap = BitmapUtils.loadBitmapFromAsset(context, assetPath);
return create(bitmap);
}
public static BitmapTexture create(Context context, int rawId) {
Bitmap bitmap = BitmapUtils.loadBitmapFromRaw(context, rawId);
return create(bitmap);
}
public static BitmapTexture create(Bitmap bitmap) {
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
float[] size = new float[] { bitmap.getWidth(), bitmap.getHeight() };
return new BitmapTexture(bitmapShader, size);
}
public void bind(RuntimeShader shader, String textureName, String sizeName) {
shader.setInputShader(textureName, mBitmapShader);
shader.setFloatUniform(sizeName, mSize);
}
}
MyRenderer.java
package com.zhyan8.shaderdemo;
import android.content.Context;
import android.graphics.RuntimeShader;
import com.zhyan8.shaderdemo.graphics.BitmapTexture;
import com.zhyan8.shaderdemo.graphics.ShaderView;
import com.zhyan8.shaderdemo.utils.StringUtils;
public class MyRenderer implements ShaderView.Renderer {
private Context mContext;
private RuntimeShader mShader;
private float[] mResolution;
private BitmapTexture mBitmapTexture;
public MyRenderer(Context context) {
this.mContext = context;
}
@Override
public RuntimeShader onSurfaceCreated() {
String shaderCode = StringUtils.loadString(mContext, "shaders/jelly.agsl");
mShader = new RuntimeShader(shaderCode);
mBitmapTexture = BitmapTexture.create(mContext, "textures/photo.png");
return mShader;
}
@Override
public void onSurfaceChanged(int width, int height) {
mResolution = new float[] { width, height };
}
@Override
public void onDrawFrame(long runTime) {
mBitmapTexture.bind(mShader, "u_texture", "u_textureSize");
mShader.setFloatUniform("u_resolution", mResolution);
mShader.setFloatUniform("u_time", runTime / 1000f);
}
}
jelly.agsl
uniform shader u_texture;
uniform vec2 u_textureSize;
uniform vec2 u_resolution;
uniform float u_time;
vec4 texture(vec2 normUV) { // 纹理采样
vec2 uv = normUV * u_textureSize;
return u_texture.eval(uv);
}
vec2 fun(vec2 uv, float aspect) { // 畸变函数
vec2 center = vec2(0.5, 0.5 / aspect);
vec2 dire = normalize(uv - center);
float dist = distance(uv, center);
vec2 uv1 = uv + sin(dist * 2.2 + u_time * 3.5) * 0.025;
return uv1;
}
vec4 main(vec2 coords) {
vec2 normUV = coords / u_resolution;
float aspect = u_resolution.x / u_resolution.y;
normUV.y /= aspect;
vec2 uv = fun(normUV, aspect);
uv.y *= aspect;
return texture(uv);
}
运行效果如下。
3.2 二次渲染
本节将对 3.1 节中 MyRenderer 进行修改,使用两个 RuntimeShader 实现二次渲染。
MyRenderer.java
package com.zhyan8.shaderdemo;
import android.content.Context;
import android.graphics.RuntimeShader;
import com.zhyan8.shaderdemo.graphics.BitmapTexture;
import com.zhyan8.shaderdemo.graphics.ShaderView;
import com.zhyan8.shaderdemo.utils.StringUtils;
public class MyRenderer implements ShaderView.Renderer {
private Context mContext;
private RuntimeShader mShader1;
private RuntimeShader mShader2;
private float[] mResolution;
private BitmapTexture mBitmapTexture;
public MyRenderer(Context context) {
this.mContext = context;
}
@Override
public RuntimeShader onSurfaceCreated() {
String shaderCode1 = StringUtils.loadString(mContext, "shaders/dispersion.agsl");
mShader1 = new RuntimeShader(shaderCode1);
String shaderCode2 = StringUtils.loadString(mContext, "shaders/jelly.agsl");
mShader2 = new RuntimeShader(shaderCode2);
mBitmapTexture = BitmapTexture.create(mContext, "textures/photo.jpg");
return mShader2;
}
@Override
public void onSurfaceChanged(int width, int height) {
mResolution = new float[] { width, height };
}
@Override
public void onDrawFrame(long runTime) {
mBitmapTexture.bind(mShader1, "u_texture", "u_textureSize");
mShader1.setFloatUniform("u_resolution", mResolution);
mShader1.setFloatUniform("u_time", runTime / 1000f);
mShader2.setInputShader("u_texture", mShader1);
mShader2.setFloatUniform("u_textureSize", mResolution);
mShader2.setFloatUniform("u_resolution", mResolution);
mShader2.setFloatUniform("u_time", runTime / 1000f);
}
}
dispersion.agsl
uniform shader u_texture;
uniform vec2 u_textureSize;
uniform vec2 u_resolution;
uniform float u_time;
vec4 texture(vec2 normUV) { // 纹理采样
vec2 uv = normUV * u_textureSize;
return u_texture.eval(uv);
}
vec2 getOffset() { // 偏移函数
float time = u_time * 1.5;
vec2 dire = vec2(sin(time), cos(time));
float strength = sin(u_time * 2.0) * 0.01;
return dire * strength;
}
vec4 main(vec2 coords) {
vec2 normUV = coords / u_resolution;
vec4 color = texture(normUV);
vec2 offset = getOffset();
color.r = texture(normUV + offset).r;
color.b = texture(normUV - offset).b;
return color;
}
运行效果如下,可以看到叠加了果冻畸变和 RGB 色散效果。