如果说 NDK 是在安卓上获得高性能的最佳工具之一。它提供了对机器的低级访问,为您提供了对内存分配的控制,提供了对高级 CPU 指令集的访问,等等。
这种能力是有代价的:为了在一段关键代码上获得最大性能,人们需要为世界上许多设备和平台优化代码。有时,使用中央处理器 SIMD 指令更合适,有时,在图形处理器上执行计算更合适。你最好有经验,有足够的设备和时间在你面前!这就是谷歌在安卓上推出 RenderScript 的原因。
RenderScript 是一种专门针对安卓的编程语言,编写时只考虑一个目标:性能。让我们明确一点,应用不能完全用 RenderScript 编写。然而,需要密集计算的关键部分应该是!渲染脚本可以从 Java 或 C/C++ 中执行。
在本章中,我们将讨论这些基础知识,并集中精力研究其 NDK 绑定。我们将创建一个新项目,通过过滤图像来演示 RenderScript 功能。更准确地说,我们将看到如何:
- 执行预定义的内联
- 创建您自己的自定义内核
- 将内核和内核结合在一起
到本章结束时,您应该能够创建自己的 RenderScript 程序,并将它们绑定到本机代码中。
RenderScript 于 2011 年在《蜂巢》中推出,它非常注重图形功能,因此得名。然而,自安卓 4.1 JellyBean 以来,RenderScript 的图形引擎部分已被弃用。尽管它保留了它的名字,RenderScript 已经深入发展以强调它的“计算引擎”。它类似于 OpenCL 和 CUDA 等技术,强调可移植性和可用性。
更具体地说,RenderScript 试图从程序员那里抽象出硬件特性,并从中提取最大的原始功率。它不是采用最小公分母,而是根据运行时执行的平台来优化代码。最终的代码可以在中央处理器或图形处理器上运行,具有由渲染脚本管理的自动并行化的优势。
渲染脚本框架由几个元素组成:
- 基于 C99 的类 C 语言,提供变量、函数、结构等
- 一个低级虚拟机 ( LLVM )基于编译器的开发者机器产生中间代码
- RenderScript 库和运行时,仅当最终程序在设备上运行时,将中间代码转换为机器代码
- 用于执行和链接计算任务的 Java 和 NDK 绑定应用编程接口
计算任务显然是渲染脚本的中心。有两种任务:
- 内核,是用户创建的脚本,使用 RenderScript 语言执行计算任务
- 内核,是内置的内核,用于执行一些常见的任务,如模糊像素
内核和内核可以组合在一起,一个程序的输出可以链接到另一个程序的输入。从复杂的计算任务图中,出现了快速而强大的程序。
然而,现在,让我们看看什么是内禀函数,以及它们是如何工作的。
RenderScript 提供了几个内置函数,主要用于图像处理,称为 Intrinsics。有了这些,混合图像,例如在 Photoshop 中,模糊它们,甚至解码来自相机的原始 YUV 图像,(参见第 4 章、从本机代码中调用 Java】,了解更慢的替代方案)变得简单而高效。事实上,Intrinsics 是高度优化的,可以被认为是其领域中最好的实现之一。
为了了解 Intrinsics 是如何工作的,让我们创建一个新的项目,获取一个输入图像并对其应用模糊效果。
由此产生的项目以RenderScript_Part1
的名称提供本书。
让我们用 JNI 模块创建一个新的 Java 项目。
-
创建新的混合 Java/C++ 项目,如第 2 章、启动原生安卓项目所示:
- 命名为
RenderScript
。 - 主要包装是
com.packtpub.renderscript
。 minSdkVersion
是 9,targetSdkVersion
是 19。- 在
AndroidManifest.xml
文件中定义android.permission.WRITE_EXTERNAL_STORAGE
权限。 - 如前所述,将项目转换为原生项目。
- 删除由 ADT 创建的本机源文件和头文件。
- 命名主活动
RenderScriptActivity
及其布局activity_renderscript.xml
。
- 命名为
-
如下定义
project.properties
文件。这些行激活RenderScript
支持库,该库允许将代码移植到旧设备,直到 API 8:target=android-20 renderscript.target=20 renderscript.support.mode=true sdk.buildtools=20
-
修改
res/activity_renderscript.xml
使其看起来如下。我们需要:-
A
SeekBar
至定义模糊半径 -
应用模糊效果的
Button
-
应用效果前后显示图像的两个
ImageView
元素。<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" a:layout_width="fill_parent" a:layout_height="fill_parent" a:layout_weight="1" a:orientation="vertical" > <LinearLayout a:orientation="horizontal" a:layout_width="fill_parent" a:layout_height="wrap_content" > <SeekBar a:id="@+id/radiusBar" a:max="250" a:layout_gravity="center_vertical" a:layout_width="128dp" a:layout_height="wrap_content" /> <Button a:id="@+id/blurButton" a:text="Blur" a:layout_width="wrap_content" a:layout_height="wrap_content"/> </LinearLayout> <LinearLayout a:baselineAligned="true" a:orientation="horizontal" a:layout_width="fill_parent" a:layout_height="fill_parent" > <ImageView a:id="@+id/srcImageView" a:layout_weight="1" a:layout_width="fill_parent" a:layout_height="fill_parent" /> <ImageView a:id="@+id/dstImageView" a:layout_weight="1" a:layout_width="fill_parent" a:layout_height="fill_parent" /> </LinearLayout> </LinearLayout>
-
-
Implement
RenderScriptActivity
as shown below.加载
RSSupport
模块,这是RenderScript
支持库,以及renderscript
模块,这是我们将要在静态块中创建的。然后,在
onCreate()
中,从放置在drawable
资源(此处命名为picture
)中的图像加载 32 位位图,并创建第二个相同大小的空位图。将这些位图分配给各自的ImageView
组件。另外,在模糊按钮上定义OnClickListener
:package com.packtpub.renderscript; ... public class RenderScriptActivity extends Activity implements OnClickListener { static { System.loadLibrary("renderscript"); } private Button mBlurButton; private SeekBar mBlurRadiusBar, mThresholdBar; private ImageView mSrcImageView, mDstImageView; private Bitmap mSrcImage, mDstImage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_renderscript); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; mSrcImage = BitmapFactory.decodeResource(getResources(), R.drawable.picture, options); mDstImage = Bitmap.createBitmap(mSrcImage.getWidth(), mSrcImage.getHeight(), Bitmap.Config.ARGB_8888); mBlurButton = (Button) findViewById(R.id.blurButton); mBlurButton.setOnClickListener(this); mBlurRadiusBar = (SeekBar) findViewById(R.id.radiusBar); mSrcImageView = (ImageView) findViewById(R.id.srcImageView); mDstImageView = (ImageView) findViewById(R.id.dstImageView); mSrcImageView.setImageBitmap(mSrcImage); mDstImageView.setImageBitmap(mDstImage); } ...
-
Create a native function,
blur
, which takes in the parameter:RenderScript
运行时的应用缓存目录- 源和目标位图
- 用于确定模糊强度的模糊效果半径
使用搜索栏值从
onClick()
处理程序调用此方法,以确定模糊半径。半径必须在[0
,25
]范围内。... private native void blur(String pCacheDir, Bitmap pSrcImage, Bitmap pDstImage, float pRadius); @Override public void onClick(View pView) { float progressRadius = (float) mBlurRadiusBar.getProgress(); float radius = Math.max(progressRadius * 0.1f, 0.1f); switch(pView.getId()) { case R.id.blurButton: blur(getCacheDir().toString(), mSrcImage, mDstImage, radius); break; } mDstImageView.invalidate(); } }
让我们创建将生成我们的新效果的原生模块。
-
创建新文件
jni/ RenderScript.cpp
。我们将需要以下内容:-
android/bitmap.h
操纵位图的标题。 -
jni.h
为 JNI 弦。 -
RenderScript.h
,这是主RenderScript
头文件。这是你唯一需要的。RenderScript 是用 C++ 编写的,在android::RSC
命名空间中定义。#include <android/bitmap.h> #include <jni.h> #include <RenderScript.h> using namespace android::RSC; ...
-
-
编写两个实用工具方法来锁定和解锁安卓位图,如第 4 章、从本机代码调用 Java:
... void lockBitmap(JNIEnv* pEnv, jobject pImage, AndroidBitmapInfo* pInfo, uint32_t** pContent) { if (AndroidBitmap_getInfo(pEnv, pImage, pInfo) < 0) abort(); if (pInfo->format != ANDROID_BITMAP_FORMAT_RGBA_8888) abort(); if (AndroidBitmap_lockPixels(pEnv, pImage, (void**)pContent) < 0) abort(); } void unlockBitmap(JNIEnv* pEnv, jobject pImage) { if (AndroidBitmap_unlockPixels(pEnv, pImage) < 0) abort(); } ...
-
Implement the native method
blur()
using the JNI convention.然后,实例化 RS 类。这个类是主界面,它控制 RenderScript 初始化、资源管理和对象创建。用 RenderScript 提供的
sp
助手类包装它,它代表一个智能指针。用参数中给定的缓存目录初始化它,用 JNI 适当地转换字符串:
... extern "C" { JNIEXPORT void JNICALL Java_com_packtpub_renderscript_RenderScriptActivity_blur (JNIEnv* pEnv, jobject pClass, jstring pCacheDir, jobject pSrcImage, jobject pDstImage, jfloat pRadius) { const char * cacheDir = pEnv->GetStringUTFChars(pCacheDir, NULL); sp<RS> rs = new RS(); rs->init(cacheDir); pEnv->ReleaseStringUTFChars(pCacheDir, cacheDir); ...
-
使用我们刚刚编写的实用方法锁定我们正在处理的位图:
... AndroidBitmapInfo srcInfo; uint32_t* srcContent; AndroidBitmapInfo dstInfo; uint32_t* dstContent; lockBitmap(pEnv, pSrcImage, &srcInfo, &srcContent); lockBitmap(pEnv, pDstImage, &dstInfo, &dstContent); ...
-
Now comes the interesting part. Create a RenderScript Allocation from the source bitmap. This
ALLOCATION
represents the whole input memory area whose dimensions are defined byType
. The Allocation is composed of "individual" Elements; in our case, 32-bit RGBA pixels are defined asElement::RGBA_8888
. Since the bitmap is not used as a texture, we have no need for Mipmaps (see Chaper 6, Rendering Graphics with OpenGL ES, about OpenGL ES for more information).对从输出位图创建的输出
ALLOCATION
重复相同的操作:... sp<const Type> srcType = Type::create(rs, Element::RGBA_8888(rs), srcInfo.width, srcInfo.height, 0); sp<Allocation> srcAlloc = Allocation::createTyped(rs, srcType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, srcContent); sp<const Type> dstType = Type::create(rs, Element::RGBA_8888(rs), dstInfo.width, dstInfo.height, 0); sp<Allocation> dstAlloc = Allocation::createTyped(rs, dstType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, dstContent); ...
-
Create a
ScriptIntrinsicBlur
instance and the kind of elements it works on, which is again RGBA pixels. An Intrinsic is a predefined RenderScript function, which implements a common operation, such as a blur effect in our case. The Blur Intrinsic takes a radius as an input parameter. Set it withsetRadius()
.然后,指定模糊内在输入,即带有
setInput()
的源分配。用
forEach()
对其每个元素应用内在,并将其保存到输出分配中。最后用
copy2DRangeTo()
将结果复制到目的位图。... sp<ScriptIntrinsicBlur> blurIntrinsic = ScriptIntrinsicBlur::create(rs, Element::RGBA_8888(rs)); blurIntrinsic->setRadius(pRadius); blurIntrinsic->setInput(srcAlloc); blurIntrinsic->forEach(dstAlloc); dstAlloc->copy2DRangeTo(0, 0, dstInfo.width, dstInfo.height, dstContent); ...
-
应用效果后别忘了解锁位图!
... unlockBitmap(pEnv, pSrcImage); unlockBitmap(pEnv, pDstImage); } }
-
创建一个针对
ArmEABI V7
和X86
平台的jni/Application.mk
文件。事实上,RenderScript 目前不支持较旧的ArmEABI V5
。STLPort
,也是 RenderScript 原生库所需要的。APP_PLATFORM := android-19 APP_ABI := armeabi-v7a x86 APP_STL := stlport_static
-
Create a
jni/Android.mk
file defining ourrenderscript
module and listingRenderScript.cpp
for compilation.使
LOCAL_C_INCLUDES
指向适当的 RenderScript,包括 NDK 平台目录中的文件目录。另外,将 RenderScript 预编译库目录追加到LOCAL_LDFLAG
。最后,链接到渲染脚本所需的
dl
、log
和RScpp_static
:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := renderscript LOCAL_C_INCLUDES += $(TARGET_C_INCLUDES)/rs/cpp \ $(TARGET_C_INCLUDES)/rs LOCAL_SRC_FILES := RenderScript.cpp LOCAL_LDFLAGS += -L$(call host-path,$(TARGET_C_INCLUDES)/../lib/rs) LOCAL_LDLIBS := -ljnigraphics -ldl -llog -lRScpp_static include $(BUILD_SHARED_LIBRARY)
运行项目,增加SeekBar
值,点击模糊按钮。输出ImageView
应显示过滤后的图片,如下所示:
我们在项目中嵌入了 RenderScript 兼容性库,使我们可以访问 RenderScript,一直到 API 8 Froyo。在旧设备上,渲染脚本是在中央处理器上“模拟”的。
如果您决定使用 NDK 的 RenderScript,但不想使用兼容性库,则需要手动嵌入 RenderScript 运行时。为此,删除我们在步骤 2 中添加到project.properties
文件中的所有内容,并在您的Android.mk
文件末尾包含以下代码:
...
include $(CLEAR_VARS)
LOCAL_MODULE := RSSupport
LOCAL_SRC_FILES := $(SYSROOT_LINK)/usr/lib/rs/lib$(LOCAL_MODULE)$(TARGET_ SONAME_EXTENSION)
include $(PREBUILT_SHARED_LIBRARY)
然后,我们执行了第一个尽可能高效地应用模糊效果的内部渲染脚本。内在执行遵循一个简单且重复的模式,您将会反复看到:
- 确保输入和输出内存区域是独占可用的,例如,通过锁定位图。
- 创建或重用适当的输入和输出分配。
- 创建并设置内在参数。
- 设置输入分配,并将固有分配应用于输出分配。
- 将输出分配的结果复制到目标内存区域。
为了更好地理解这个过程,让我们深入了解一下 RenderScript 的工作方式。渲染脚本遵循一个简单的模型。它将一些数据作为输入,并将其处理到输出存储区:
作为一种计算解决方案,RenderScript 可以处理存储在内存中的任何类型的数据。这是一个分配。分配由单个元素组成。对于指向位图的分配,元素通常是一个像素(它本身是一组 4 uchar
值)。在大量可用的元素中,我们可以引用:
可能的分配要素
|
| --- |
| U8
、U8_2
、U8_3
、U8_4
| I8
、I8_2
、I8_3
、I8_4
| RGBA_8888
|
| U16
、U16_2
、U16_3
、U16_4
| I16
、I16_2``I16_3
、I16_4
| RGB_565
|
| U32
、U32_2
、U32_3
、U32_4
| I32
、I32_2``I32_3``I32_4
| RGB_888
|
| U64
、U64_2
、U64_3
、U64_4
| I64
、I64_2
、I64_3
、I64_4
| A_8
|
| F32
、F32_2
、F32_3
、F32_4
| F64``F64_2``F64_3``F64_4
| YUV
|
| MATRIX_2X2
| MATRIX_3X3
| MATRIX_4X4
|
U
=无符号整数,I
=有符号整数,F
=浮点
8
、16
、32
、64
=字节数。例如I8
= 8 位带符号int
(即带符号字符)
_2
、_3
、_4
=向量的元素数(I8_3
表示 3 个有符号整数的向量)
A_8
表示 Alpha 通道(每个像素表示为一个无符号字符)。
在内部,元素是用数据类型(如无符号字符的UNSIGNED_8
)和数据类型(如像素的PIXEL_RGBA
)描述的。对于在 GPU 上解释的图形数据,DataKind 与称为采样器 的东西一起使用(参见第 6 章、用 OpenGL ES 渲染图形,关于 OpenGL ES 更好地理解什么是采样器)。数据类型和数据种类是为了更高级的使用,并且应该在大部分时间对您透明。您可以在http://developer . Android . com/reference/Android/render script/element . html查看完整的元素列表。
知道输入/输出的类型元素是不够的。它们的数量也是必不可少的,因为这个决定了整个分配的大小。这是Type
的作用,可以设置为一维、二维(一般为位图)或三维。还支持其他一些信息,比如 YUV 格式(如第 4 章、从原生代码调用 Java 中所见,NV21 是安卓系统中的默认值)。所以,换句话说,Type
描述的是多维数组。
分配有一个特定的标志来控制如何生成 Mipmaps。默认情况下,大多数分配将不需要一个(RS_ALLOCATION_MIPMAP_NONE
)。但是,当用作图形纹理的输入时,会在脚本内存(RS_ALLOCATION_MIPMAP_FULL
)或上传到图形处理器(RS_ALLOCATION_MIPMAP_ON_SYNC_TO_TEXTURE
)中创建纹理贴图。
一旦我们从一个类型和一个元素创建了分配,我们就可以负责创建和设置内部函数。RenderScript 提供了其中的几个,数量不多,但主要集中在图像处理上:
|固有的
|
描述
| | --- | --- | |
ScriptIntrinsicBlend
| 为了将两个分配混合在一起,例如,两个图像(我们将在本章的最后部分看到加法混合)。 | |
ScriptIntrinsicBlur
| 在位图上应用模糊效果。 | |
ScriptIntrinsicColorMatrix
| 将颜色矩阵应用于分配(例如,调整图像色调、更改颜色等)。 | |
ScriptIntrinsicConvolve3x3
| 将大小为 3 的卷积矩阵应用于分配(许多图像过滤器可以用卷积矩阵实现,包括模糊)。 | |
ScriptIntrinsicConvolve5x5
| 这与ScriptIntrinsicConvolve3x3
相同,但矩阵大小为 5。 |
|
ScriptIntrinsicHistogram
| 这用于应用直方图过滤器(例如,提高图像对比度)。 | |
ScriptIntrinsicLUT
| 这用于为每个通道应用“查找表”(例如,将像素中的给定红色值转换为表中的另一个预定义值)。 | |
ScriptIntrinsicResize
| 这用于调整 2D 分配的大小(例如,缩放图像)。 | |
ScriptIntrinsicYuvToRGB
| 例如,要将来自相机的 YUV 图像翻译成 RGB 图像(就像我们在第 4 章、从本机代码中调用 Java 一样)。这本书在 NDK 的装订被窃听,因此,在本书撰写时无法使用。如果真的需要,可以从 Java 应用。 |
这些内禀中的每一个都需要它自己的特定参数(例如,模糊效果的半径)。完整的 Intrinsics 文档可在http://developer . Android . com/reference/Android/render script/package-summary . html上找到。
内部需要一个输入和输出分配。如果应用的功能类型合适,在技术上可以将输入用作输出。情况并非如此,例如,ScriptIntrinsicBlur
因为模糊的像素可以在被读取的同时被写入,以模糊其他像素。
一旦设置了分配,就会应用一个固有的并执行它的工作。之后,必须使用其中一种copy***To()
方法将结果复制到输出存储区域(copy2DRangeTo()
表示位图,它有两个维度,如果目标区域有间隙,则为copy2DStridedTo()
)。数据复制是利用计算结果的先决步骤。
当映像分配的大小不是 4 的倍数时,某些设备上报告了一些问题。这可能会让你想起 OpenGL 纹理,它们有相同的要求。所以,尽量坚持 4 的倍数。
虽然 RenderScript 提供的内联功能非常有用,但是您可能需要更大的灵活性。也许你需要自己定制的图像过滤器,或者超过 25 像素的模糊效果,或者也许你根本不想处理图像。那么,RenderScript 内核可能是您的正确答案。
RenderScript 让能够开发小的定制“脚本”,而不是内置的 Intrinsics。这些程序被称为内核,是用类似 C 语言编写的。它们在构建时由基于 RenderScript LLVM 的编译器编译成中间语言。最后,它们在运行时被翻译成机器代码。RenderScript 负责平台相关的优化。
现在让我们看看如何通过实现一个自定义的图像效果来创建这样一个内核,该效果根据像素的亮度来过滤像素。
由此产生的项目以RenderScript_Part2
的名称提供本书。
让我们在用户界面中添加一个新的组件,并实现新的图像过滤器。
-
在
res/activity_renderscript.xml
中新增门槛SeekBar
和Button
:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" a:layout_width="fill_parent" a:layout_height="fill_parent" a:layout_weight="1" a:orientation="vertical" > <LinearLayout a:orientation="horizontal" a:layout_width="fill_parent" a:layout_height="wrap_content" > ... <SeekBar a:id="@+id/thresholdBar" a:max="100" a:layout_gravity="center_vertical" a:layout_width="128dp" a:layout_height="wrap_content" /> <Button a:id="@+id/thresholdButton" a:text="Threshold" a:layout_width="wrap_content" a:layout_height="wrap_content"/> </LinearLayout> <LinearLayout a:baselineAligned="true" a:orientation="horizontal" a:layout_width="fill_parent" a:layout_height="fill_parent" > ... </LinearLayout> </LinearLayout>
-
编辑
RenderScriptActivity
并将阈值SeekBar
和Button
绑定到新的原生方法threshold()
。这种方法类似于blur()
,不同的是它采用的阈值浮动参数在[0
,100
]范围内。... public class RenderScriptActivity extends Activity implements OnClickListener { ... private Button mBlurButton, mThresholdButton; private SeekBar mBlurRadiusBar, mThresholdBar; private ImageView mSrcImageView, mDstImageView; private Bitmap mSrcImage, mDstImage; @Override protected void onCreate(Bundle savedInstanceState) { ... mBlurButton = (Button) findViewById(R.id.blurButton); mBlurButton.setOnClickListener(this); mThresholdButton = (Button)findViewById(R.id.thresholdButton); mThresholdButton.setOnClickListener(this); mBlurRadiusBar = (SeekBar) findViewById(R.id.radiusBar); mThresholdBar = (SeekBar) findViewById(R.id.thresholdBar); ... } @Override public void onClick(View pView) { float progressRadius = (float) mBlurRadiusBar.getProgress(); float radius = Math.max(progressRadius * 0.1f, 0.1f); float threshold = ((float) mThresholdBar.getProgress()) / 100.0f; switch(pView.getId()) { ... case R.id.thresholdButton: threshold(getCacheDir().toString(), mSrcImage, mDstImage, threshold); break; } mDstImageView.invalidate(); } ... private native void threshold(String pCacheDir, Bitmap pSrcImage, Bitmap pDstImage, float pThreshold); }
-
现在,让我们使用 RenderScript 语言来编写我们自己的
jni/threshold.rs
过滤器。首先,使用 pragma 指令声明:-
脚本语言版本(目前只有
1
可以) -
脚本关联的 Java 包名
#pragma version(1) #pragma rs java_package_name(com.packtpub.renderscript) ...
-
-
Then, declare an input parameter
thresholdValue
of typefloat
.我们还需要两个 3 个浮点数的常量向量(
float3
):-
第一个值代表一种
BLACK
颜色 -
第二个值 a 预定义
LUMINANCE_VECTOR
... float thresholdValue; static const float3 BLACK = { 0.0, 0.0, 0.0 }; static const float3 LUMINANCE_VECTOR = { 0.2125, 0.7154, 0.0721 }; ...
-
-
创建名为
threshold()
的脚本的根函数。它接受一个 4 个无符号字符的向量,即输入中的一个 RGBA 像素,并在输出中返回一个新的。前置__attribute__((kernel))
表示这个函数是主脚本函数,也就是“内核的根”。该函数的工作原理如下:-
它将输入像素从每个颜色分量都在[
0
,255
]范围内的字符矢量转换为每个分量都在[0.0
,1.0
]范围内的浮点值矢量。这就是rsUnpackColor8888()
功能的作用。 -
现在我们有了一个浮点向量,RenderScript 提供的许多数学函数中的一些可以被应用。这里,RGBA 颜色空间的预定义亮度向量的点积返回像素的相对亮度。
-
有了这些信息,该函数根据给定的阈值检查像素的亮度是否足够。如果没有,像素设置为黑色。
-
最后,它用
rsPackColor8888()
将像素的颜色从浮点向量转换为无符号字符向量。这个值将被渲染脚本复制到最终的位图中,如我们所见。... uchar4 __attribute__((kernel)) threshold(uchar4 in) { float4 pixel = rsUnpackColor8888(in); float luminance = dot(LUMINANCE_VECTOR, pixel.rgb); if (luminance < thresholdValue) { pixel.rgb = BLACK; } return rsPackColorTo8888(pixel); }
-
-
To compile our new
threshold.rs
script, list it in theAndroid.mk
file.在编译过程中,
ScriptC_threshold.h
和ScriptC_threshold.cpp
在obj/local/armeabi-v7a/objs-debug/renderscript
生成。这些文件包含将我们的代码与 RenderScript 执行的阈值内核绑定的代码。所以,我们还需要将目录追加到LOCAL_C_INCLUDES
目录中:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := renderscript LOCAL_C_INCLUDES += $(TARGET_C_INCLUDES)/rs/cpp \ $(TARGET_C_INCLUDES)/rs \ $(TARGET_OBJS)/$(LOCAL_MODULE) LOCAL_SRC_FILES := RenderScript.cpp threshold.rs LOCAL_LDFLAGS += -L$(call host-path,$(TARGET_C_INCLUDES)/../lib/rs) LOCAL_LDLIBS := -ljnigraphics -ldl -llog -lRScpp_static include $(BUILD_SHARED_LIBRARY)
-
将生成的表头包含在
jni/RenderScript.cpp
中。#include <android/bitmap.h> #include <jni.h> #include <RenderScript.h> #include "ScriptC_threshold.h" using namespace android::RSC; ...
-
Then, implement the new method
threshold()
, respecting the JNI naming convention. This method is similar toblur()
.然而,我们没有实例化一个预定义的内部对象,而是实例化了一个由 RenderScript 生成的内核。根据我们的渲染脚本文件名,这个内核被命名为
ScriptC_threshold
。我们脚本中定义的输入参数
thresholdValue
可以用 RenderScript 生成的set_thresholdValue()
进行初始化。然后,可以使用生成的方法forEach_threshold()
应用主方法threshold()
。一旦应用了内核,就可以使用
copy2DRangeTo()
将结果复制到目标位图上,例如使用内核:... JNIEXPORT void JNICALL Java_com_packtpub_renderscript_RenderScriptActivity_threshold (JNIEnv* pEnv, jobject pClass, jstring pCacheDir, jobject pSrcImage, jobject pDstImage, jfloat pThreshold) { const char * cacheDir = pEnv->GetStringUTFChars(pCacheDir, NULL); sp<RS> rs = new RS(); rs->init(cacheDir); pEnv->ReleaseStringUTFChars(pCacheDir, cacheDir); AndroidBitmapInfo srcInfo; uint32_t* srcContent; AndroidBitmapInfo dstInfo; uint32_t* dstContent; lockBitmap(pEnv, pSrcImage, &srcInfo, &srcContent); lockBitmap(pEnv, pDstImage, &dstInfo, &dstContent); sp<const Type> srcType = Type::create(rs, Element::RGBA_8888(rs), srcInfo.width, srcInfo.height, 0); sp<Allocation> srcAlloc = Allocation::createTyped(rs, srcType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, srcContent); sp<const Type> dstType = Type::create(rs, Element::RGBA_8888(rs), dstInfo.width, dstInfo.height, 0); sp<Allocation> dstAlloc = Allocation::createTyped(rs, dstType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, dstContent); sp<ScriptC_threshold> thresholdKernel = new ScriptC_threshold(rs); thresholdKernel->set_thresholdValue(pThreshold); thresholdKernel->forEach_threshold(srcAlloc, dstAlloc); dstAlloc->copy2DRangeTo(0, 0, dstInfo.width, dstInfo.height, dstContent); unlockBitmap(pEnv, pSrcImage); unlockBitmap(pEnv, pDstImage); } }
运行项目,增加新的SeekBar
,点击阈值按钮。输出ImageView
应显示只有发光像素的过滤图像,如下所示:
我们已经编写并编译了第一个渲染脚本内核。内核脚本有一个.rs
扩展,是用一种受 C99 启发的语言编写的。它们的内容以 pragma 定义开始,这些定义带来了关于它们的附加“元”信息:语言版本(只能是 1)和 Java 包。我们也可以用它们来调整用 pragma 指令(#pragma rs_fp_full, #pragma rs_fp_relaxed
或#pragma rs_fp_imprecise
计算的浮点精度。
Java 包对于 RenderScript 运行时很重要,它需要在执行过程中解析编译后的内核。使用渲染脚本兼容性库时,用 NDK 编译的脚本(存储在jni
文件夹中)可能无法解析。在这种情况下,一个可能的解决方案是在适当的包中复制 Java src
文件夹中的.rs
文件。
内核在某种程度上类似于内部函数。事实上,一旦编译完成,同样的过程也适用于它们:创建分配、内核、设置一切、应用,最后复制结果。执行时,内核函数应用于输入的每个元素,并在相应的输出分配元素中并行返回。
您可以通过 NDK 绑定 API 和一个在编译时生成的附加绑定层(通常称为 反射层)来设置内核。每个编译好的脚本都由一个 C++ 类“反映”,该类的名称是根据以ScriptC_
为前缀的脚本文件名定义的。最终代码在同名头和obj
目录中的源文件中生成,每个 ABI 对应一个。作为一种包装,反射类是脚本文件的唯一接口。它们对内核的输入或输出中传递的分配类型执行一些运行时检查,以确保它们的元素类型与脚本文件中声明的类型相匹配。具体例子请看项目obj
目录中生成的ScriptC_threshold.cpp
。
内核输入参数通过全局变量从反射层传递到脚本文件。全局变量对应于所有非static
和非const
变量,例如:
float thresholdValue;
它们是在函数外部声明的,比如一个 C 变量。全局变量通过设置器在反射层中可用。在我们的项目中,thresholdValue
全局变量通过生成的方法set_thresholdValue()
传递。变量不必是基本类型。它们也可以是指针,在这种情况下,反射的方法名以bind_
为前缀。并期望分配。生成的类中也提供了 Getters。
另一方面,在与全局变量相同的范围内声明的静态变量在 NDK 反射层中不可访问,并且不能在脚本之外修改。当标记const
时,它们显然被视为常数,就像我们项目中的亮度向量一样:
static const float3 LUMINANCE_VECTOR = { 0.2125, 0.7154, 0.0721 };
主要的内核函数,通常被称为根函数,除了用__attribute__((kernel))
标记之外,它们被声明为一个 C 函数。它们将输入分配的元素类型作为参数,并返回输出分配的元素类型。输入参数和返回值都是可选的,但其中至少有一个必须存在。在我们的例子中,输入参数和输出返回值是一个像素元素(即一个 4 个无符号字符的向量;每个颜色通道 1 个字节):
uchar4 __attribute__((kernel)) threshold(uchar4 in) {
...
}
RenderScript 根函数还可以被赋予额外的索引参数,这些参数表示元素在其分配中的位置(或“坐标”)。例如,我们可以声明两个额外的uint32_t
参数来获取threshold()
中的像素元素坐标:
uchar4 __attribute__((kernel)) threshold(uchar4 in, uint32_t x, uint32_t y) {
...
}
可以在一个脚本中声明多个不同名称的根函数。编译后,它们作为前缀为forEach_
的函数反映在生成的类中,例如:
void forEach_threshold(android::RSC::sp<const android::RSC::Allocation> ain, android::RSC::sp<const android::RSC::Allocation> aout);
在引入__attribute__((kernel))
之前,RenderScript 文件只能包含一个名为 root 的主函数。这种形式现在仍然被允许。这样的函数接受一个指向输入的指针,输出参数中的分配,并且不允许返回值。因此threshold()
函数被重写为传统的根方法,如下所示:
void root(const uchar4 *in, uchar4 *out) {
float4 pixel = rsUnpackColor8888(*in);
float luminance = dot(LUMINANCE_VECTOR, pixel.rgb);
if (luminance < thresholdValue) {
pixel.rgb = BLACK;
}
*out = rsPackColorTo8888(pixel);
除了root()
函数,脚本还可以包含一个没有参数和返回值的init()
函数。当脚本被实例化时,这个函数只被调用一次。
void init() {
...
}
显然,RenderScript 语言的可能性比传统的 c 语言更加有限和受限。
-
直接分配资源。在运行内核之前,内存必须由客户端应用分配。
-
写低级汇编代码或者做花哨的 C 语言。然而,希望有大量熟悉的 C 语言元素可用,如
struct
、typedef
、enum
等;偶数指针! -
Use C libraries or runtime. However, RenderScript provides a full "runtime" library with plenty of math, conversion, atomic functions, and so on. Have a look at http://developer.android.com/guide/topics/renderscript/reference.html for more details about them.
RenderScript 提供的一个方法,您可能会发现特别有用的是
rsDebug()
,它将调试日志打印到 ADB。
即使有这些限制,RenderScript 约束仍然非常宽松。结果是,一些脚本可能无法从最大加速中受益,例如,在图形处理器上,这是非常有限的。为了克服这个问题,RenderScript 的一个有限子集 FilterScript 被设计为支持优化和兼容性。如果您需要最高性能,请考虑它。
有关 RenderScript 语言功能的更多信息,请查看http://developer . Android . com/guide/topics/RenderScript/advanced . html。
团结就是力量再真实不过了。只有内核和内核是强大的特性。然而,结合在一起,它们为 RenderScript 框架提供了全部的力量。
现在让我们看看如何将模糊和亮度阈值滤镜与混合内在结合在一起,以创建一个好看的图像效果。
由此产生的项目以RenderScript_Part3
的名称提供本书。
让我们改进我们的项目,应用一个新的组合过滤器。
-
在
res/activity_renderscript.xml
中新增组合Button
,如下:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" a:layout_width="fill_parent" a:layout_height="fill_parent" a:layout_weight="1" a:orientation="vertical" > <LinearLayout a:orientation="horizontal" a:layout_width="fill_parent" a:layout_height="wrap_content" > ... <Button a:"d="@+id/thresholdBut"on" a:te"t="Thresh"ld" a:layout_wid"h="wrap_cont"nt" a:layout_heig"t="wrap_cont"nt"/> <Button a:"d="@+id/combineBut"on" a:te"t="Comb"ne" a:layout_wid"h="wrap_cont"nt" a:layout_heig"t="wrap_cont"nt"/> </LinearLayout> <LinearLayout a:baselineAlign"d="t"ue" a:orientati"n="horizon"al" a:layout_wid"h="fill_par"nt" a:layout_heig"t="fill_par"nt" > ... </LinearLayout> </LinearLayout>
-
将 组合按钮绑定到新的原生方法
combine()
,该方法同时具有blur()
和threshold()
:... public class RenderScriptActivity extends Activity implements OnClickListener { ... private Button mThresholdButton, mBlurButton, mCombineButton; private SeekBar mBlurRadiusBar, mThresholdBar; private ImageView mSrcImageView, mDstImageView; private Bitmap mSrcImage, mDstImage; @Override protected void onCreate(Bundle savedInstanceState) { ... mBlurButton = (Button) findViewById(R.id.blurButton); mBlurButton.setOnClickListener(this); mThresholdButton = (Button) findViewById(R.id.thresholdButton); mThresholdButton.setOnClickListener(this); mCombineButton = (Button)findViewById(R.id.combineButton); mCombineButton.setOnClickListener(this); ... } @Override public void onClick(View pView) { float progressRadius = (float) mBlurRadiusBar.getProgress(); float radius = Math.max(progressRadius * 0.1f, 0.1f); float threshold = ((float) mThresholdBar.getProgress()) / 100.0f; switch(pView.getId()) { case R.id.blurButton: blur(getCacheDir().toString(), mSrcImage, mDstImage, radius); break; case R.id.thresholdButton: threshold(getCacheDir().toString(), mSrcImage, mDstImage, threshold); break; case R.id.combineButton: combine(getCacheDir().toString(), mSrcImage, mDstImage, radius, threshold); break; } mDstImageView.invalidate(); } ... private native void combine(String pCacheDir, Bitmap pSrcImage, Bitmap pDstImage, float pRadius, float pThreshold); }
的参数
-
再次遵循 JNI 惯例,编辑
jni/RenderScript.cpp
并添加新的combine()
方法。该方法的性能与我们之前看到的相似:-
渲染脚本引擎已初始化
-
位图被锁定
-
为输入和输出位图
... JNIEXPORT void JNICALL Java_com_packtpub_renderscript_RenderScriptActivity_combine (JNIEnv* pEnv, jobject pClass, jstring pCacheDir, jobject pSrcImage, jobject pDstImage, jfloat pRadius, jfloat pThreshold) { const char * cacheDir = pEnv->GetStringUTFChars(pCacheDir, NULL); sp<RS> rs = new RS(); rs->init(cacheDir); pEnv->ReleaseStringUTFChars(pCacheDir, cacheDir); AndroidBitmapInfo srcInfo; uint32_t* srcContent; AndroidBitmapInfo dstInfo; uint32_t* dstContent; lockBitmap(pEnv, pSrcImage, &srcInfo, &srcContent); lockBitmap(pEnv, pDstImage, &dstInfo, &dstContent); sp<const Type> srcType = Type::create(rs, Element::RGBA_8888(rs), srcInfo.width, srcInfo.height, 0); sp<Allocation> srcAlloc = Allocation::createTyped(rs, srcType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, srcContent); sp<const Type> dstType = Type::create(rs, Element::RGBA_8888(rs), dstInfo.width, dstInfo.height, 0); sp<Allocation> dstAlloc = Allocation::createTyped(rs, dstType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, dstContent); ...
创建适当的分配
-
-
我们还需要一个临时存储区来存储计算结果。让我们创建一个由内存缓冲区
tmpBuffer
:... sp<const Type> tmpType = Type::create(rs, Element::RGBA_8888(rs), dstInfo.width, dstInfo.height, 0);tmpType->getX(); uint8_t* tmpBuffer = new uint8_t[tmpType->getX() * tmpType->getY() * Element::RGBA_8888(rs)- >getSizeBytes()]; sp<Allocation> tmpAlloc = Allocation::createTyped(rs, tmpType, RS_ALLOCATION_MIPMAP_NONE, RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT, tmpBuffer); ...
支持的临时分配
-
初始化组合过滤器所需的内核和内部组件:
-
Threshold
内核 -
Blur
内在的 -
不需要参数
... sp<ScriptC_threshold> thresholdKernel = new ScriptC_threshold(rs); sp<ScriptIntrinsicBlur> blurIntrinsic = ScriptIntrinsicBlur::create(rs, Element::RGBA_8888(rs)); blurIntrinsic->setRadius(pRadius); sp<ScriptIntrinsicBlend> blendIntrinsic = ScriptIntrinsicBlend::create(rs, Element::RGBA_8888(rs)); thresholdKernel->set_thresholdValue(pThreshold); ...
的附加
Blend
内在
-
-
现在,将多个过滤器组合在一起:
-
首先,应用阈值过滤器并将结果保存到临时分配中。
-
其次,对临时分配应用模糊过滤器,并将结果保存在目标位图分配中。
-
最后,使用加法运算混合源位图和过滤位图以创建最终图像。混合可以“就地”完成,不需要额外的分配,因为每个像素只被读取和写入一次(与模糊滤镜相反)。
... thresholdKernel->forEach_threshold(srcAlloc, tmpAlloc); blurIntrinsic->setInput(tmpAlloc); blurIntrinsic->forEach(dstAlloc); blendIntrinsic->forEachAdd(srcAlloc, dstAlloc); ...
-
-
最后,保存结果并释放资源。包装在
sp<>
(即智能指针)模板中的所有值,如tmpAlloc
,都会自动释放:... dstAlloc->copy2DRangeTo(0, 0, dstInfo.width, dstInfo.height, dstContent); unlockBitmap(pEnv, pSrcImage); unlockBitmap(pEnv, pDstImage); delete[] tmpBuffer; } ...
运行项目,调整SeekBar
组件,点击组合按钮。输出ImageView
应显示“重新录制”的画面,其中发光部分突出显示:
我们将多个内函数和内核链接在一起,对图像应用组合滤镜。这样的链条很容易到位;我们基本上需要将一个脚本的输出 Allocation 连接到下一个脚本的输入 Allocation。将数据复制到输出存储区实际上是唯一必要的结尾。
这真的很令人难过,但是在安卓 NDK 应用编程接口上还没有脚本分组功能,只有在 Java 端才有。使用脚本分组功能,可以定义完整的脚本“图”,允许 RenderScript 进一步优化代码。如果你需要这个特性,那么你可以等待或者回到 Java。
如果需要的话,可以在多个脚本中重用分配,以避免分配无用的内存。如果脚本允许“就地”修改,甚至可以在输入和输出中重用相同的分配。例如,模糊滤镜就不是这种情况,它会重写模糊的像素,同时读取这些像素以模糊其他像素,从而导致奇怪的视觉假象。
说到重用,在执行之间重用 RenderSript 对象(即 RS 上下文对象、Intrinsics、Kernels 等)是一个很好的做法。如果您重复执行计算,例如处理来自相机的图像,这就更加重要。
内存是渲染脚本性能的一个重要方面。使用不当,会降低效率。在我们的项目中,我们提供了一个指向我们创建的分配的指针。这意味着我们在项目中创建的分配由本机内存“支持”,在我们的例子中,是位图内容:
...
sp<Allocation> srcAlloc = Allocation::createTyped(rs, srcType,
RS_ALLOCATION_MIPMAP_NONE,
RS_ALLOCATION_USAGE_SHARED | RS_ALLOCATION_USAGE_SCRIPT,
srcContent);
...
但是,数据也可以在处理之前通过copy***From()
方法从输入存储区复制到分配中,这是copy***To()
方法的附属部分。这对于 Java 绑定端尤其有用,它并不总是允许使用“支持分配”。NDK 绑定更加灵活,大多数情况下可以避免输入数据复制。
RenderScript 为其他人提供了从脚本中传递数据的机制。第一种是方法rsSendToClient()
和rsSendToClientBlocking()
。它们允许脚本向调用方传递一个“命令”,可选地带有一些数据。后一种方法在表演方面显然更危险一点,应该避免。
数据也可以通过指针进行通信。指针是动态内存,允许内核和调用者之间的双向通信。如前所述,它们反映在以bind_
为前缀的方法生成的类中。编译时应该在反射层中生成适当的获取器和设置器。
然而,NDK 渲染脚本框架还没有反映渲染脚本文件中声明的结构。所以声明一个指向脚本文件中定义的struct
的指针暂时不会起作用。不过,指向基元类型的指针使用分配来工作。因此,期待 NDK 方面在这个问题上令人讨厌的限制。
让我们以内存为主题结束,假设您需要一个脚本的多个输入或输出分配,有一个解决方案,一个rs_allocation
,它表示通过一个 getter 和 setter 反映的分配。你想吃多少就吃多少。然后,您可以通过rsAllocationGetDim*()
、rsGetElementAt*()
、rsSetElementAt*()
等方法访问尺寸和元素。
例如,threshold()
方法可以改写如下:
请注意,因为我们没有在参数中传递输入 Allocation,所以像往常一样返回一个
for
循环不像传递参数的分配那样是隐式的threshold()
函数不能是内核根。但是结合rs_allocation
使用输入分配是完全可能的。
#pragma version(1)
#pragma rs java_package_name(com.packtpub.renderscript)
float thresholdValue;
static const float3 BLACK = { 0.0, 0.0, 0.0 };
static const float3 LUMINANCE_VECTOR = { 0.2125, 0.7154, 0.0721 };
rs_allocation input;
rs_allocation output;
void threshold() {
uint32_t sizeX = rsAllocationGetDimX(input);
uint32_t sizeY = rsAllocationGetDimY(output);
for (uint32_t x = 0; x < sizeX; ++ x) {
for (uint32_t y = 0; y < sizeY; ++ y) {
uchar4 rawPixel = rsGetElementAt_uchar4(input, x, y);
// The algorithm itself remains the same.
float4 pixel = rsUnpackColor8888(rawPixel);
float luminance = dot(LUMINANCE_VECTOR, pixel.rgb);
if (luminance < thresholdValue) {
pixel.rgb = BLACK;
}
rawPixel = rsPackColorTo8888(pixel);
rsSetElementAt_uchar4(output, rawPixel, x, y);
}
}
}
此外,内核将以下列方式调用。注意应用效果的方法是如何以invoked_
为前缀的(而不是forEach_
)。这是因为threshold()
函数不是内核根:
...
thresholdKernel->set_input(srcAlloc);
thresholdKernel->set_output(dstAlloc);
thresholdKernel->invoke_threshold();
dstAlloc->copy2DRangeTo(0, 0, dstInfo.width, dstInfo.height,
dstContent);
...
关于渲染脚本语言功能的更多信息,请看一下。
本章介绍了 RenderScript,这是一种用于并行化密集计算任务的高级技术。更具体地说,我们看到了如何使用预定义的 RenderScript 内置的 Intrinsics,目前主要用于图像处理。我们还发现了如何使用受 c 语言启发的 RenderScript 定制语言来实现我们自己的内核。最后,我们看到了一个内部内核和内核相结合来执行更复杂计算的例子。
渲染脚本可以从 Java 或本机端获得。然而,让我们明确一点,除了由内存缓冲区支持的分配之外(尽管这对于性能来说是一个相当重要的特性),RenderScript 通过其 Java API 仍然更有用。分组不可用,struct
还没有反映出来,其他一些特性仍然有问题(例如 YUV Intrinsics)。
事实上,RenderScript 旨在为那些既没有时间也没有知识遵循原生路径的开发人员提供巨大的计算能力。因此,NDK 还没有得到很好的服务。虽然这在未来可能会改变,但是您应该准备好将 RenderScript 代码的至少一部分保留在 Java 端。