墨水几何结构和渲染管线
我们现在引入了不同的rendering modes。 该指南对管道设置和渲染每一步坐了详细介绍。
视觉结果取决于刷子配置、管道配置以及平台渲染配置。
Raster Ink
本节逐步详细介绍了最佳的几何图形设置方法和栅格墨水的渲染管道。
笔刷配置
要设置光栅颗粒刷子,需要两种纹理:
形状纹理和填充纹理:
shape
参数指定颗粒的形状。
它接受图像,可以不透明像素定义颗粒形状。
fill
参数指定形状的填充图案。
它接受图像,可从中采样图案。
两种纹理都可以在该参数中传递一系列图像。 这种情况下,该方法将它们解析为纹理映射。
- Kotlin
- C#
- JavaScript
...
fun pencil(context: Context): RasterBrush {
val opts = BitmapFactory.Options()
opts.inSampleSize = 1
opts.inScaled = false
// Texture for shape
val shapeTexture =
BitmapFactory.decodeResource(context.resources, R.drawable.essential_shape, opts)
// Texture for fill
val fillTexture =
BitmapFactory.decodeResource(context.resources, R.drawable.essential_fill_11, opts)
// Convert to PNG
val stream = ByteArrayOutputStream()
shapeTexture!!.compress(Bitmap.CompressFormat.PNG, 100, stream)
val shapeTextureByteArray = stream.toByteArray()
val stream2 = ByteArrayOutputStream()
fillTexture!!.compress(Bitmap.CompressFormat.PNG, 100, stream2)
val fillTextureByteArray = stream2.toByteArray()
// Create the raster brush
var brush = RasterBrush(
URIBuilder.getBrushURI("raster", "Pencil"), // name of the brush
0.15f, // spacing
0.15f, // scattering
RotationMode.RANDOM, // rotation mode
listOf(shapeTextureByteArray), // shape texture
listOf(), fillTextureByteArray, // fill texture
"", // fill texture URI
fillTexture.width.toFloat(), // width of texture
fillTexture.height.toFloat(), // height of texture
false, // randomized fill
BlendMode.MAX // mode of blending
)
shapeTexture.recycle()
fillTexture.recycle()
return brush
}
...
public PencilTool(Graphics graphics)
{
Fill = new PixelInfo(new Uri("ms-appx:///Assets/textures/essential_fill_11.png"));
Shape = new PixelInfo(new Uri("ms-appx:///Assets/textures/essential_shape.png"));
Brush.Scattering = 0.05f;
Brush.RotationMode = ParticleRotationMode.RotateRandom;
Brush.FillTileSize = new Size(32.0f, 32.0f);
Brush.FillTexture = graphics.CreateTexture(Fill.PixelData);
Brush.ShapeTexture = graphics.CreateTexture(Shape.PixelData);
ParticleSpacing = 0.3f;
}
/* **************** RASTER BRUSH configuration **************** */
pencil: new BrushGL(
URIBuilder.getBrushURI("raster", "Pencil"),
"/images/textures/essential_shape.png",
"/images/textures/essential_fill_11.png",
{
spacing: 0.15,
scattering: 0.15
}),
...
PathPoint 计算器
对于所有相关的刷子,需要配置PathPoint 计算器 ,它会影响传感器数据更改墨水笔划宽度、强度和倾斜度的方式。
首先,需要配置布局。 布局可根据输入提供者调整。 例如,对于触摸输入,不接收倾斜度或压力信息。
- Kotlin
- C#
- JavaScript
...
import com.wacom.ink.PathPointLayout
...
override fun getLayout(): PathPointLayout {
// Define different layouts for stylus and touch input
if (isStylus) {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.ALPHA,
PathPoint.Property.ROTATION,
PathPoint.Property.OFFSET_X
)
} else {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.ALPHA
)
}
}
...
...
using Wacom.Ink.Geometry;
using Wacom.Ink.Rendering;
...
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
return new PathPointLayout(PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.Size,
PathPoint.Property.Alpha);
case Windows.Devices.Input.PointerDeviceType.Pen:
return new PathPointLayout(PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.Size,
PathPoint.Property.Alpha,
PathPoint.Property.OffsetX,
PathPoint.Property.OffsetY);
default:
throw new Exception("Unknown input device type");
}
}
// In the JavaScript implementation layout is predefined within PathPointContext
let context = new PathPointContext();
触摸和触控笔输入可针对不同类型的输入传感器数据进行优化,因此,需要定义接收之前的、当前的和后续的传感器数据输入的计算器,并计算合适的@@@PathPoint
.
例如,触摸数据与触控笔数据一样,不包含压力信息和倾斜方向。
因此,要实现同样的α强度差值或墨水笔划宽度变动,只能使用速度信息。
宽度计算函数所基于输入数据的当前速度由引擎提供。
在此,最小值和最大值定义宽度范围,而最小速度和最大速度定义在该范围内,宽度如何改变。 其结果是介于0和1之间的标准化值。 最后,可通过重新映射功能修改标准化值,例如,将行为逆转。 因此,例如,缓慢移动会增大笔划的宽度,而非减小;如果手指移动加快,则笔划会变细。
- Kotlin
- C#
- JavaScript
...
companion object {
val uri = URIBuilder.getToolURI("raster", "pencil")
// Minimum size of the pencil tip
val MIN_PENCIL_SIZE = 4f
// Maximum size of the pencil tip
val MAX_PENCIL_SIZE = 25f
// Minimum alpha values for the particles
val MIN_ALPHA = 0.1f
// Maximum alpha values for the particles
val MAX_ALPHA = 0.7f
// Unit for speed is px/second.
// NOTE: This needs to be optimized for different Pixel densities of different devices
val MAX_SPEED = 15000f
}
...
override val touchCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
var size = current.computeValueBasedOnSpeed(
previous,
next,
minValue = MIN_PENCIL_SIZE,
maxValue = MAX_PENCIL_SIZE,
minSpeed = 80f,
maxSpeed = MAX_SPEED,
// reverse behaviour
remap = { 1f - it}
)!!
var alpha = current.computeValueBasedOnSpeed(
previous,
next,
minValue = MIN_ALPHA,
maxValue = MAX_ALPHA,
minSpeed = 80f,
maxSpeed = MAX_SPEED,
// reverse behaviour
remap = { 1f - it}
)
if (alpha == null) {
alpha = previousAlpha
} else {
previousAlpha = alpha
}
PathPoint(current.x, current.y, size = size, alpha = alpha)
}
...
private static readonly ToolConfig mSizeConfig = new ToolConfig()
{
initValue = 4,
finalValue = 10,
minValue = 4,
maxValue = 10,
minSpeed = 80,
maxSpeed = 1400,
};
private static readonly ToolConfig mAlphaConfig = new ToolConfig()
{
initValue = 0.1f,
finalValue = 0.5f,
minValue = 0.1f,
maxValue = 0.5f,
minSpeed = 80,
maxSpeed = 1400,
};
...
protected override ToolConfig SizeConfig => mSizeConfig;
protected override ToolConfig AlphaConfig => mAlphaConfig;
...
/// <summary>
/// Calculator delegate for input from mouse input
/// Calculates the path point properties based on pointer input.
/// </summary>
/// <param name="previous"></param>
/// <param name="current"></param>
/// <param name="next"></param>
/// <returns>PathPoint with calculated properties</returns>
protected PathPoint CalculatorForMouseAndTouch(PointerData previous, PointerData current, PointerData next)
{
var size = current.ComputeValueBasedOnSpeed(previous, next,
SizeConfig.minValue, SizeConfig.maxValue,
SizeConfig.initValue, SizeConfig.finalValue,
SizeConfig.minSpeed, SizeConfig.maxSpeed,
SizeConfig.remap);
if (size.HasValue)
{
PreviousSize = size.Value;
}
else
{
size = PreviousSize;
}
var alpha = current.ComputeValueBasedOnSpeed(previous, next,
AlphaConfig.minValue, AlphaConfig.maxValue,
AlphaConfig.initValue, AlphaConfig.finalValue,
AlphaConfig.minSpeed, AlphaConfig.maxSpeed,
AlphaConfig.remap);
if (alpha.HasValue)
{
PreviousAlpha = alpha.Value;
}
else
{
alpha = PreviousAlpha;
}
PathPoint pp = new PathPoint(current.X, current.Y)
{
Size = size,
Alpha = alpha
};
return pp;
}
// In the JavaScript implementation you need to define the dynamics for the PathPoint calculators
/* ******* RASTER TOOLS ******* */
pencil: {
brush: BrushPalette.pencil,
dynamics: {
size: {
value: {
min: 4,
max: 5
},
velocity: {
min: 80,
max: 1400,
remap: v => ValueTransformer.reverse(v)
}
},
alpha: {
value: {
min: 0.05,
max: 0.2
},
velocity: {
min: 80,
max: 1400
}
},
rotation: {
dependencies: [SensorChannel.Type.ROTATION, SensorChannel.Type.AZIMUTH]
},
scaleX: {
dependencies: [SensorChannel.Type.RADIUS_X, SensorChannel.Type.ALTITUDE],
value: {
min: 1,
max: 3
}
},
scaleY: {
dependencies: [SensorChannel.Type.RADIUS_Y],
value: {
min: 1,
max: 3
}
},
offsetX: {
dependencies: [SensorChannel.Type.ALTITUDE],
value: {
min: 2,
max: 5
}
}
}
}
触控笔输入配置可以更方便地配置现实世界书写工具的自然行为。 可在此通过压力传感器数据增加工具的宽度或更改颜色的强度。 另外,如果触控笔倾斜,将自然而然地更改接触面积区域。 如果想要实现笔的影线,水彩效果或墨水笔效果,需要在此设置。
**注意:**输入传感器质量各不相同,因此,需要通过不同的触控笔输入测试是否达到所需的行为效果。
- Kotlin
- C#
...
var previousAlpha = 0.2f
...
override val stylusCalculator: Calculator = { previous, current, next ->
// calculate the offset of the pencil tip due to tilted position
val cosAltitudeAngle = cos(current.altitudeAngle!!)
val sinAzimuthAngle = sin(current.azimuthAngle!!)
val cosAzimuthAngle = cos(current.azimuthAngle!!)
val x = sinAzimuthAngle * cosAltitudeAngle
val y = cosAltitudeAngle * cosAzimuthAngle
val offsetY = 5f * -x
val offsetX = 5f * -y
// compute the rotation
val rotation = current.computeNearestAzimuthAngle(previous)
// now, based on the tilt of the pencil the size of the brush size is increasing, as the
// pencil tip is covering a larger area
val size = max(MIN_PENCIL_SIZE,
min(MAX_PENCIL_SIZE, MIN_PENCIL_SIZE + 20f * abs(current.altitudeAngle!!)))
// Change the intensity of alpha value by pressure of speed, if available else use speed
var alpha = if (current.force == -1f) {
current.computeValueBasedOnSpeed(
previous,
next,
minValue = MIN_ALPHA,
maxValue = MAX_ALPHA,
minSpeed = 0f,
maxSpeed = 3500f,
// reverse behaviour
remap = { 1.0f - it}
)
} else {
current.computeValueBasedOnPressure(
minValue = MIN_ALPHA,
maxValue = MAX_ALPHA,
minPressure = 0.0f,
maxPressure = 1.0f,
remap = { v: Float -> v.toDouble().pow(1).toFloat() }
)
}
if (alpha == null) {
alpha = previousAlpha
} else {
previousAlpha = alpha
}
PathPoint(
current.x, current.y,
alpha = alpha, size = size, rotation = rotation,
offsetX = offsetX, offsetY = offsetY
)
}
...
/// <summary>
/// Calculator delegate for input from a stylus (pen)
/// Calculates the path point properties based on pointer input.
/// </summary>
/// <param name="previous"></param>
/// <param name="current"></param>
/// <param name="next"></param>
/// <returns>PathPoint with calculated properties</returns>
protected override PathPoint CalculatorForStylus(PointerData previous, PointerData current, PointerData next)
{
var cosAltitudeAngle = Math.Cos(current.AltitudeAngle.Value);
var sinAzimuthAngle = Math.Sin(current.AzimuthAngle.Value);
var cosAzimuthAngle = Math.Cos(current.AzimuthAngle.Value);
// calculate the offset of the pencil tip due to tilted position
var x = sinAzimuthAngle * cosAltitudeAngle;
var y = cosAltitudeAngle * cosAzimuthAngle;
var offsetY = -x;
var offsetX = -y;
// compute the rotation
var rotation = current.ComputeNearestAzimuthAngle(previous);
// now, based on the tilt of the pencil the size of the brush size is increasing, as the
// pencil tip is covering a larger area
var size = Math.Max(mSizeConfig.minValue, mSizeConfig.minValue + 20f * Math.Abs(current.AltitudeAngle.Value));
// Change the intensity of alpha value by pressure of speed, if available else use speed
var alpha = (!current.Force.HasValue)
? current.ComputeValueBasedOnSpeed(previous, next, 0.1f, 0.7f, null, null, 0f, 3500f)
: ComputeValueBasedOnPressure(current, 0.1f, 0.7f, 0.0f, 1.0f);
if (!alpha.HasValue)
{
alpha = PreviousAlpha;
}
else
{
PreviousAlpha = alpha.Value;
}
PathPoint pp = new PathPoint(current.X, current.Y)
{
Size = size,
Alpha = alpha,
OffsetX = (float)offsetX,
OffsetY = (float)offsetY
};
return pp;
}
管线
RasterInkBuilder
定义栅格墨水管道:
- Kotlin
- C#
- JavaScript
import com.wacom.ink.*
import com.wacom.ink.format.rendering.RasterBrush
import com.wacom.ink.pipeline.PathProducer
import com.wacom.ink.pipeline.SmoothingFilter
import com.wacom.ink.pipeline.SplineInterpolator
import com.wacom.ink.pipeline.SplineProducer
import com.wacom.ink.pipeline.base.ProcessorResult
...
class RasterInkBuilder() {
// The pipeline components:
lateinit var pathProducer: PathProducer
lateinit var smoother: SmoothingFilter
lateinit var splineProducer: SplineProducer
lateinit var splineInterpolator: SplineInterpolator
lateinit var pathSegment: PathSegment
lateinit var tool: RasterTool
...
fun updatePipeline(newTool: Tool) {
if (newTool !is RasterTool) {
throw IllegalArgumentException("Invalid tool")
} else {
tool = newTool
var layout = tool.getLayout()
// Path producer needs to know the layout
pathProducer = PathProducer(layout, tool.getCalculator())
smoother = SmoothingFilter(layout.size) // Dimension
splineProducer = SplineProducer(layout) // Layout
splineInterpolator = SplineInterpolator(
spacing = tool.brush.spacing, // Spacing between two successive sample.
splitCount = 1, // Determines the number of iterations for the
// discretization.
calculateDerivatives = true, // Calculate derivatives
interpolateByLength = true // Interpolate by length
)
pathSegment = PathSegment()
splineProducer.keepAllData = true
}
}
}
...
/// <summary>
/// Manages ink geometry pipeline for raster (particle) brushes.
/// </summary>
public class RasterInkBuilder : InkBuilder
{
#region Fields
private const float defaultSpacing = 0.15f;
private const int splitCount = 1;
#endregion
#region Constructors
public RasterInkBuilder()
{
}
#endregion
#region Properties
public event EventHandler LayoutUpdated;
#endregion
...
#region Public Interface
public void UpdatePipeline(PathPointLayout layout, Calculator calculator, float spacing)
{
bool layoutChanged = false;
bool otherChange = false;
if ((Layout == null) || (layout.ChannelMask != Layout.ChannelMask))
{
Layout = layout;
layoutChanged = true;
}
if (mPathProducer == null || calculator != mPathProducer.PathPointCalculator || layoutChanged)
{
mPathProducer = new PathProducer(Layout, calculator, true);
otherChange = true;
}
if (mSmoothingFilter == null || layoutChanged)
{
mSmoothingFilter = new SmoothingFilter(Layout.Count)
{
KeepAllData = true
};
otherChange = true;
}
if (SplineProducer == null || layoutChanged)
{
SplineProducer = new SplineProducer(Layout, true);
otherChange = true;
}
if (SplineInterpolator == null || layoutChanged)
{
SplineInterpolator = new DistanceBasedInterpolator(Layout, spacing, splitCount, true, true, true);
otherChange = true;
}
((DistanceBasedInterpolator) SplineInterpolator).Spacing = spacing;
if (layoutChanged || otherChange)
{
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
}
#endregion
}
}
// In the JavaScript implementation the setup of the pipeline within InkBuilderAbstract which is inheritat by InkBuilder
this.builder = new InkBuilder();
光栅化视图
接下来,设置光栅视图:
- Kotlin
- C#
- JavaScript
import com.wacom.ink.egl.EGLRenderingContext
import com.wacom.ink.format.tree.data.Stroke
import com.wacom.ink.format.tree.nodes.StrokeNode
import com.wacom.ink.rasterization.InkCanvas
import com.wacom.ink.rasterization.Layer
import com.wacom.ink.rasterization.StrokeRenderer
import com.wacom.ink.rendering.BlendMode
...
/**
* This is a surface for drawing raster inking.
* Extends from SurfaceView
*/
class RasterView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr) {
...
private var inkCanvas: InkCanvas? = null
private lateinit var viewLayer: Layer
private lateinit var strokesLayer: Layer
private lateinit var currentFrameLayer: Layer
private lateinit var strokeRenderer: StrokeRenderer
private var rasterInkBuilder = RasterInkBuilder() // The ink builder
var rasterTool: RasterTool = PencilTool(context) // The default tool
...
init {
rasterInkBuilder.updatePipeline(PencilTool(context))
setZOrderOnTop(true);
holder.setFormat(PixelFormat.TRANSPARENT);
// the drawing is going to be performer on background
// on a surface, so we initialize the surface
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, w: Int, h: Int) {
// here, once the surface is crated, we are going to initialize ink canvas
// firs we check that there is no other inkCanvas, in case there is we dispose it
if (inkCanvas?.isDisposed == false) {
releaseResources();
}
inkCanvas =
InkCanvas(holder, EGLRenderingContext.EGLConfiguration(8, 8, 8, 8, 8, 8))
viewLayer = inkCanvas!!.createViewLayer(w, h)
strokesLayer = inkCanvas!!.createLayer(w, h)
currentFrameLayer = inkCanvas!!.createLayer(w, h)
inkCanvas!!.clearLayer(currentFrameLayer)
strokeRenderer =
StrokeRenderer(inkCanvas, PencilTool(context).brush.toParticleBrush(), w, h)
drawStrokes(strokeNodeList)
renderView()
if (listener != null) {
listener.onSurfaceCreated()
}
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
releaseResources();
}
override fun surfaceCreated(p0: SurfaceHolder?) {
//we don't have to do anything here
}
})
}
}
...
/// <summary>
/// Creates and manages rendering brush and graphics objects and interactions between them;
/// Recieves pointer input events and passes them on to the rendering brush to feed into
/// the ink geometry pipeline
/// </summary>
public class Renderer
{
#region Fields
private SwapChainPanel m_swapChainPanel;
private PointerManager m_pointerManager;
private Graphics m_graphics = new Graphics();
#endregion
...
#region Constructor
...
/// <summary>
/// Constructor. Creates rendering brush; initializes graphics
/// </summary>
/// <param name="swapChainPanel">SwapChainPanel on which to render captured ink</param>
/// <param name="brushType">Type of brush to use</param>
/// <param name="thickness">Relative thickness of brush</param>
/// <param name="color">Color of ink</param>
public Renderer(SwapChainPanel swapChainPanel, RasterBrushStyle style, MediaColor color)
{
StrokeHandler = new RasterStrokeHandler(this, style, color, m_graphics);
m_swapChainPanel = swapChainPanel;
m_graphics.GraphicsReady += OnGraphicsReady;
m_graphics.Initialize(m_swapChainPanel, false);
}
#endregion
class InkCanvasRaster extends InkCanvas {
constructor(canvas, width, height) {
super();
this.canvas = InkCanvasGL.createInstance(canvas, width, height);
this.strokesLayer = this.canvas.createLayer();
this.strokeRenderer = new StrokeRendererGL(this.canvas);
}
}