コード例 #1
0
def test_render_orange_dots():
    """Render four orange dots and check that there are four orange square dots."""

    @python2shader
    def vertex_shader(
        index: (RES_INPUT, "VertexId", i32),
        pos: (RES_OUTPUT, "Position", vec4),
        pointsize: (RES_OUTPUT, "PointSize", f32),
    ):
        positions = [
            vec2(-0.5, -0.5),
            vec2(-0.5, +0.5),
            vec2(+0.5, -0.5),
            vec2(+0.5, +0.5),
        ]
        p = positions[index]
        pos = vec4(p, 0.0, 1.0)  # noqa
        pointsize = 16.0  # noqa

    device = get_default_device()

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    top = wgpu.PrimitiveTopology.point_list
    # render_to_screen(*render_args, topology=top)
    a = render_to_texture(*render_args, size=(64, 64), topology=top)

    # Check that the background is all zero
    bg = a.copy()
    bg[8:24, 8:24, :] = 0
    bg[8:24, 40:56, :] = 0
    bg[40:56, 8:24, :] = 0
    bg[40:56, 40:56, :] = 0
    assert np.all(bg == 0)

    # Check the square
    for dot in (
        a[8:24, 8:24, :],
        a[8:24, 40:56, :],
        a[40:56, 8:24, :],
        a[40:56, 40:56, :],
    ):
        assert np.all(dot[:, :, 0] == 255)  # red
        assert np.all(dot[:, :, 1] == 127)  # green
        assert np.all(dot[:, :, 2] == 0)  # blue
        assert np.all(dot[:, :, 3] == 255)  # alpha
コード例 #2
0
def test_render_orange_square_indexed_indirect():
    """Render an orange square, using an index buffer."""

    device = get_default_device()

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Index buffer
    indices = (ctypes.c_int32 * 6)(0, 1, 2, 2, 1, 3)
    ibo = device.create_buffer_with_data(
        data=indices,
        usage=wgpu.BufferUsage.INDEX | wgpu.BufferUsage.MAP_WRITE,
    )

    # Buffer with draw parameters for indirect draw call
    params = (ctypes.c_int32 * 5)(6, 1, 0, 0, 0)
    indirect_buffer = device.create_buffer_with_data(
        data=params,
        usage=wgpu.BufferUsage.INDIRECT,
    )

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, topology=wgpu.PrimitiveTopology.triangle_list, ibo=ibo, indirect_buffer=indirect_buffer)
    a = render_to_texture(
        *render_args,
        size=(64, 64),
        topology=wgpu.PrimitiveTopology.triangle_list,
        ibo=ibo,
        indirect_buffer=indirect_buffer,
    )

    # Check that the background is all zero
    bg = a.copy()
    bg[16:-16, 16:-16, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:-16, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #3
0
def test_render_orange_square_viewport():
    """Render an orange square, in a sub-viewport of the rendered area."""

    device = get_default_device()

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    def cb(renderpass):
        renderpass.set_viewport(10, 20, 32, 32, 0, 100)

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Fiddled in a small test to covers the raising of an exception
    with raises(TypeError):
        device.create_bind_group(
            layout=bind_group_layout, entries=[{"resource": device}]
        )

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, renderpass_callback=cb)
    a = render_to_texture(*render_args, size=(64, 64), renderpass_callback=cb)

    # Check that the background is all zero
    bg = a.copy()
    bg[20 + 8 : 52 - 8, 10 + 8 : 42 - 8, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[20 + 8 : 52 - 8, 10 + 8 : 42 - 8, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #4
0
def test_render_orange_square_color_attachment2():
    """Render an orange square on a blue background, testing the LoadOp.load,
    though in this case the result is the same as the normal square test.
    """

    device = get_default_device()

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    ca = {
        "resolve_target": None,
        "load_value": wgpu.LoadOp.load,  # LoadOp.load or color
        "store_op": wgpu.StoreOp.store,
    }

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, color_attachment=ca)
    a = render_to_texture(*render_args, size=(64, 64), color_attachment=ca)

    # Check the background
    bg = a.copy()
    bg[16:-16, 16:-16, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:-16, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #5
0
def test_render_orange_square_scissor():
    """Render an orange square, but scissor half the screen away."""

    device = get_default_device()

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    def cb(renderpass):
        renderpass.set_scissor_rect(0, 0, 32, 32)
        # Alse set blend color. Does not change outout, but covers the call.
        renderpass.set_blend_color((0, 0, 0, 1))

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, renderpass_callback=cb)
    a = render_to_texture(*render_args, size=(64, 64), renderpass_callback=cb)

    # Check that the background is all zero
    bg = a.copy()
    bg[16:32, 16:32, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:32, 16:32, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #6
0
def test_render_orange_square():
    """Render an orange square and check that there is an orange square."""

    device = get_default_device()

    # NOTE: the 0.499 instead of 0.5 is to make sure the resulting value is 127.
    # With 0.5 some drivers would produce 127 and others 128.

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args)
    a = render_to_texture(*render_args, size=(64, 64))

    # Check that the background is all zero
    bg = a.copy()
    bg[16:-16, 16:-16, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:-16, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #7
0
def test_render_orange_square_depth():
    """Render an orange square, but disable half of it using a depth test."""

    device = get_default_device()

    @python2shader
    def vertex_shader2(
        index: (RES_INPUT, "VertexId", i32),
        pos: (RES_OUTPUT, "Position", vec4),
    ):
        positions = [
            vec3(-0.5, -0.5, 0.0),
            vec3(-0.5, +0.5, 0.0),
            vec3(+0.5, -0.5, 0.2),
            vec3(+0.5, +0.5, 0.2),
        ]
        pos = vec4(positions[index], 1.0)  # noqa

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    def cb(renderpass):
        renderpass.set_stencil_reference(42)

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Create dept-stencil texture
    depth_stencil_texture = device.create_texture(
        size=(64, 64, 1),  # when rendering to texture
        # size=(640, 480, 1),  # when rendering to screen
        dimension=wgpu.TextureDimension.d2,
        format=wgpu.TextureFormat.depth24plus_stencil8,
        usage=wgpu.TextureUsage.OUTPUT_ATTACHMENT,
    )

    depth_stencil_state = dict(
        format=wgpu.TextureFormat.depth24plus_stencil8,
        depth_write_enabled=True,
        depth_compare=wgpu.CompareFunction.less_equal,
        stencil_front={
            "compare": wgpu.CompareFunction.equal,
            "fail_op": wgpu.StencilOperation.keep,
            "depth_fail_op": wgpu.StencilOperation.keep,
            "pass_op": wgpu.StencilOperation.keep,
        },
        stencil_back={
            "compare": wgpu.CompareFunction.equal,
            "fail_op": wgpu.StencilOperation.keep,
            "depth_fail_op": wgpu.StencilOperation.keep,
            "pass_op": wgpu.StencilOperation.keep,
        },
        stencil_read_mask=0,
        stencil_write_mask=0,
    )

    depth_stencil_attachment = dict(
        attachment=depth_stencil_texture.create_view(),
        depth_load_value=0.1,
        depth_store_op=wgpu.StoreOp.store,
        stencil_load_value=wgpu.LoadOp.load,
        stencil_store_op=wgpu.StoreOp.store,
    )

    # Render
    render_args = device, vertex_shader2, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, renderpass_callback=cb, depth_stencil_state=depth_stencil_state, depth_stencil_attachment=depth_stencil_attachment)
    a = render_to_texture(
        *render_args,
        size=(64, 64),
        renderpass_callback=cb,
        depth_stencil_state=depth_stencil_state,
        depth_stencil_attachment=depth_stencil_attachment,
    )

    # Check that the background is all zero
    bg = a.copy()
    bg[16:-16, 16:32, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:32, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #8
0
def test_render_orange_square_vbo():
    """Render an orange square, using a VBO."""

    device = get_default_device()

    @python2shader
    def vertex_shader(
        pos_in: (RES_INPUT, 0, vec2),
        pos: (RES_OUTPUT, "Position", vec4),
    ):
        pos = vec4(pos_in, 0.0, 1.0)  # noqa

    @python2shader
    def fragment_shader(
        out_color: (RES_OUTPUT, 0, vec4),
    ):
        out_color = vec4(1.0, 0.499, 0.0, 1.0)  # noqa

    # Bindings and layout
    bind_group_layout = device.create_bind_group_layout(entries=[])  # zero bindings
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=[])
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )

    # Vertex buffer
    pos_data = (ctypes.c_float * 8)(-0.5, -0.5, -0.5, +0.5, +0.5, -0.5, +0.5, +0.5)
    vbo = device.create_buffer_with_data(
        data=pos_data,
        usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.MAP_WRITE,
    )

    # Vertex buffer views
    vbo_view = {
        "array_stride": 4 * 2,
        "step_mode": "vertex",
        "attributes": [
            {
                "format": wgpu.VertexFormat.float2,
                "offset": 0,
                "shader_location": 0,
            },
        ],
    }

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args, vbos=[vbo], vbo_views=[vbo_view])
    a = render_to_texture(*render_args, size=(64, 64), vbos=[vbo], vbo_views=[vbo_view])

    # Check that the background is all zero
    bg = a.copy()
    bg[16:-16, 16:-16, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:-16, :]
    assert np.all(sq[:, :, 0] == 255)  # red
    assert np.all(sq[:, :, 1] == 127)  # green
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #9
0
def render_textured_square(fragment_shader, texture_format, texture_size,
                           texture_data):
    """Render, and test the result. The resulting image must be a
    gradient on R and B, zeros on G and ones on A.
    """
    nx, ny, nz = texture_size

    device = get_default_device()

    if can_use_vulkan_sdk:
        pyshader.dev.validate(vertex_shader)
        pyshader.dev.validate(fragment_shader)

    # Create texture
    texture = device.create_texture(
        size=(nx, ny, nz),
        dimension=wgpu.TextureDimension.d2,
        format=texture_format,
        usage=wgpu.TextureUsage.SAMPLED | wgpu.TextureUsage.COPY_DST,
    )
    upload_to_texture(device, texture, texture_data, nx, ny, nz)

    # texture_view = texture.create_view()
    # or:
    texture_view = texture.create_view(
        format=texture_format,
        dimension=wgpu.TextureDimension.d2,
    )
    # But not like these ...
    with raises(ValueError):
        texture_view = texture.create_view(
            dimension=wgpu.TextureDimension.d2, )
    with raises(ValueError):
        texture_view = texture.create_view(mip_level_count=1, )

    sampler = device.create_sampler(mag_filter="linear", min_filter="linear")

    # Determine texture component type from the format
    if texture_format.endswith(("norm", "float")):
        texture_component_type = wgpu.TextureComponentType.float
    elif "uint" in texture_format:
        texture_component_type = wgpu.TextureComponentType.uint
    else:
        texture_component_type = wgpu.TextureComponentType.sint

    # Bindings and layout
    bindings = [
        {
            "binding": 0,
            "resource": texture_view
        },
        {
            "binding": 1,
            "resource": sampler
        },
    ]
    binding_layouts = [
        {
            "binding": 0,
            "visibility": wgpu.ShaderStage.FRAGMENT,
            "type": wgpu.BindingType.sampled_texture,
            "view_dimension": wgpu.TextureViewDimension.d2,
            "texture_component_type": texture_component_type,
        },
        {
            "binding": 1,
            "visibility": wgpu.ShaderStage.FRAGMENT,
            "type": wgpu.BindingType.sampler,
        },
    ]
    bind_group_layout = device.create_bind_group_layout(
        entries=binding_layouts)
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout])
    bind_group = device.create_bind_group(layout=bind_group_layout,
                                          entries=bindings)

    # Render
    render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group
    # render_to_screen(*render_args)
    a = render_to_texture(*render_args, size=(64, 64))

    # print(a.max(), a[:,:,0].max())

    # Check that the background is all zero
    bg = a.copy()
    bg[16:-16, 16:-16, :] = 0
    assert np.all(bg == 0)

    # Check the square
    sq = a[16:-16, 16:-16, :]
    ref1 = [
        [150, 150, 150, 150, 150, 150, 150, 150, 152, 155, 158, 161],
        [164, 167, 170, 173, 177, 180, 183, 186, 189, 192],
        [195, 198, 200, 200, 200, 200, 200, 200, 200, 200],
    ]
    ref2 = [
        [150, 150, 150, 150, 150, 150, 150, 150, 147, 141, 134, 128],
        [122, 116, 109, 103, 97, 91, 84, 78, 72, 66],
        [59, 53, 50, 50, 50, 50, 50, 50, 50, 50],
    ]
    ref1, ref2 = sum(ref1, []), sum(ref2, [])

    assert np.allclose(sq[0, :, 0], ref1, atol=1)
    assert np.allclose(sq[:, 0, 0], ref2, atol=1)
    assert np.allclose(sq[0, :, 1], ref1, atol=1)
    assert np.allclose(sq[:, 0, 1], ref2, atol=1)
    assert np.all(sq[:, :, 2] == 0)  # blue
    assert np.all(sq[:, :, 3] == 255)  # alpha
コード例 #10
0
def _compute_texture(compute_shader, texture_format, texture_dim, texture_size, data1):
    """
    Apply a computation on a texture and validate the result. The shader should:
    * Add the x-coordinate to the red channel.
    * Add 1 to the green channel.
    * Multiply the blue channel by 2.
    * The alpha channel must remain equal.
    """

    nx, ny, nz, nc = texture_size
    nbytes = ctypes.sizeof(data1)
    bpp = nbytes // (nx * ny * nz)  # bytes per pixel

    if can_use_vulkan_sdk:
        pyshader.dev.validate(compute_shader)

    device = get_default_device()
    cshader = device.create_shader_module(code=compute_shader)

    # Create textures and views
    texture1 = device.create_texture(
        size=(nx, ny, nz),
        dimension=texture_dim,
        format=texture_format,
        usage=wgpu.TextureUsage.STORAGE | wgpu.TextureUsage.COPY_DST,
    )
    texture2 = device.create_texture(
        size=(nx, ny, nz),
        dimension=texture_dim,
        format=texture_format,
        usage=wgpu.TextureUsage.STORAGE | wgpu.TextureUsage.COPY_SRC,
    )
    texture_view1 = texture1.create_view()
    texture_view2 = texture2.create_view()

    # Determine texture component type from the format
    if texture_format.endswith(("norm", "float")):
        texture_component_type = wgpu.TextureComponentType.float
    elif "uint" in texture_format:
        texture_component_type = wgpu.TextureComponentType.uint
    else:
        texture_component_type = wgpu.TextureComponentType.sint

    # Create buffer that we need to upload the data
    buffer_usage = (
        wgpu.BufferUsage.MAP_READ
        | wgpu.BufferUsage.COPY_SRC
        | wgpu.BufferUsage.COPY_DST
    )
    buffer = device.create_buffer_with_data(data=data1, usage=buffer_usage)
    assert buffer.usage == buffer_usage

    # Define bindings
    # One can see here why we need 2 textures: one is readonly, one writeonly
    bindings = [
        {"binding": 0, "resource": texture_view1},
        {"binding": 1, "resource": texture_view2},
    ]
    binding_layouts = [
        {
            "binding": 0,
            "visibility": wgpu.ShaderStage.COMPUTE,
            "type": wgpu.BindingType.readonly_storage_texture,  # <-
            "view_dimension": texture_dim,
            "storage_texture_format": texture_format,
            "texture_component_type": texture_component_type,
        },
        {
            "binding": 1,
            "visibility": wgpu.ShaderStage.COMPUTE,
            "type": wgpu.BindingType.writeonly_storage_texture,  # <-
            "view_dimension": texture_dim,
            "storage_texture_format": texture_format,
            "texture_component_type": texture_component_type,
        },
    ]
    bind_group_layout = device.create_bind_group_layout(entries=binding_layouts)
    pipeline_layout = device.create_pipeline_layout(
        bind_group_layouts=[bind_group_layout]
    )
    bind_group = device.create_bind_group(layout=bind_group_layout, entries=bindings)

    # Create a pipeline and run it
    compute_pipeline = device.create_compute_pipeline(
        layout=pipeline_layout,
        compute_stage={"module": cshader, "entry_point": "main"},
    )
    assert compute_pipeline.layout is pipeline_layout
    assert compute_pipeline.get_bind_group_layout(0) is bind_group_layout
    command_encoder = device.create_command_encoder()
    command_encoder.copy_buffer_to_texture(
        {
            "buffer": buffer,
            "offset": 0,
            "bytes_per_row": bpp * nx,
            "rows_per_image": ny,
        },
        {"texture": texture1, "mip_level": 0, "origin": (0, 0, 0)},
        (nx, ny, nz),
    )
    compute_pass = command_encoder.begin_compute_pass()
    compute_pass.push_debug_group("foo")
    compute_pass.insert_debug_marker("setting pipeline")
    compute_pass.set_pipeline(compute_pipeline)
    compute_pass.insert_debug_marker("setting bind group")
    compute_pass.set_bind_group(
        0, bind_group, [], 0, 999999
    )  # last 2 elements not used
    compute_pass.insert_debug_marker("dispatch!")
    compute_pass.dispatch(nx, ny, nz)
    compute_pass.pop_debug_group()
    compute_pass.end_pass()
    command_encoder.copy_texture_to_buffer(
        {"texture": texture2, "mip_level": 0, "origin": (0, 0, 0)},
        {
            "buffer": buffer,
            "offset": 0,
            "bytes_per_row": bpp * nx,
            "rows_per_image": ny,
        },
        (nx, ny, nz),
    )
    device.default_queue.submit([command_encoder.finish()])

    # Read the current data of the output buffer
    data2 = data1.__class__.from_buffer(buffer.read_data())

    # Numpy arrays are easier to work with
    a1 = np.ctypeslib.as_array(data1).reshape(nz, ny, nx, nc)
    a2 = np.ctypeslib.as_array(data2).reshape(nz, ny, nx, nc)

    # Validate!
    for x in range(nx):
        assert np.all(a2[:, :, x, 0] == a1[:, :, x, 0] + x)
    if nc >= 2:
        assert np.all(a2[:, :, :, 1] == a1[:, :, :, 1] + 1)
    if nc >= 3:
        assert np.all(a2[:, :, :, 2] == a1[:, :, :, 2] * 2)
    if nc >= 4:
        assert np.all(a2[:, :, :, 3] == a1[:, :, :, 3])